Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions apps/backend/scripts/clean.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { fileManager } from '../src/utils/fileManager';
import { cleanupProject } from '../src/utils/fileManager';

async function main() {
const customPath = process.argv[2];

if (!customPath) {
console.error('Please provide a path to clean');
console.log('Usage: bun run clean-advanced -- /path/to/project');
process.exit(1);
}

try {
await fileManager.cleanupProject(customPath);
console.log(' Cleanup completed successfully');
await cleanupProject(customPath);
console.log('Advanced cleanup completed successfully');
} catch (error) {
console.error(' Cleanup failed:', error instanceof Error ? error.message : error);
console.error('Advanced cleanup failed:', error instanceof Error ? error.message : error);
process.exit(1);
}
}
Expand Down
52 changes: 44 additions & 8 deletions apps/backend/scripts/setup.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,51 @@
import { fileManager } from '../src/utils/fileManager';
import { setupProject } from '../src/utils/fileManager';
import { join } from 'node:path';
import { promises as fs } from 'node:fs';

async function main() {
try {
const projectPath = await fileManager.setupProject();
console.log('Project created at:', projectPath);
console.log('\nTo clean up later, run:');
console.log('bun run clean');
console.log('or to clean a specific path:');
console.log('bun run clean -- /path/to/project');
// Test with custom options
const project = await setupProject({
baseName: 'my-advanced-contract',
rustCode: `#![no_std]
use soroban_sdk::{contractimpl, Env, log};

pub struct Contract;

#[contractimpl]
impl Contract {
pub fn sum(env: Env, a: i32, b: i32) -> i32 {
log!(&env, "Adding {} and {}", a, b);
a + b
}

pub fn multiply(env: Env, a: i32, b: i32) -> i32 {
a * b
}
}`
});

console.log('Advanced project created at:', project.tempDir);

// Verify files were created
const libRsPath = join(project.tempDir, 'src', 'lib.rs');
const cargoTomlPath = join(project.tempDir, 'Cargo.toml');

console.log('\nFiles created:');
console.log('-', libRsPath);
console.log('-', cargoTomlPath);

console.log('\nlib.rs content:');
console.log(await fs.readFile(libRsPath, 'utf8'));

console.log('\nProject will be automatically cleaned up in 30 seconds...');
await new Promise(resolve => setTimeout(resolve, 30000));

// Cleanup will happen automatically when the cleanup function is called
await project.cleanup();
console.log('\nCleanup completed successfully');
} catch (error) {
console.error('Setup failed:', error instanceof Error ? error.message : error);
console.error('Advanced setup failed:', error instanceof Error ? error.message : error);
process.exit(1);
}
}
Expand Down
143 changes: 59 additions & 84 deletions apps/backend/src/utils/fileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,82 +22,96 @@ export interface ProjectSetupOptions {
baseName?: string;
/** Custom temporary directory root (defaults to OS temp dir) */
tempRoot?: string;
/** Custom Rust code for the contract */
rustCode?: string;
}

/**
* Default Rust contract template
* This is a simple Soroban contract that greets a user by name.
* It can be customized by passing a different `rustCode` in the options.
*/
const DEFAULT_RUST_CODE = `#![no_std]
use soroban_sdk::{contractimpl, Env};

pub struct Contract;

#[contractimpl]
impl Contract {
pub fn hello(env: Env, name: String) -> String {
format!("Hello, {}!", name)
}
}`;

/**
* Default Cargo.toml template for Soroban contracts
*/
const DEFAULT_CARGO_TOML = `[package]
name = "temp-contract"
version = "0.1.0"
edition = "2021"

[lib]
name = "temp_project"
path = "src/lib.rs"
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = "21.2.0"

[dev-dependencies]
soroban-sdk = { version = "21.2.0", features = ["testutils"] }

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

[profile.release-with-logs]
inherits = "release"
debug-assertions = true
`;

/**
* Sanitizes a directory name to prevent path traversal and ensure cross-platform compatibility
*
* @param baseName - The base name to sanitize
* @returns A sanitized directory name safe for use across platforms
*
* @example
* ```typescript
* getSanitizedDirName('../malicious') // returns 'malicious'
* getSanitizedDirName('CON') // returns '' (Windows reserved name)
* getSanitizedDirName('my-project') // returns 'my-project'
* ```
*/
export function getSanitizedDirName(baseName: string): string {
if (!baseName || typeof baseName !== 'string') {
return '';
}

const trimmed = baseName.trim();

// Handle whitespace-only strings
if (!trimmed) {
return '';
}

// Sanitize the filename to remove dangerous characters and reserved names
let sanitized = sanitizeFilename(trimmed, { replacement: '_' });

// Additional cleanup for path traversal attempts
sanitized = sanitized.replace(/\.\./g, '').replace(/^[._]+/, '');

// Ensure it's not too long (filesystem limit is usually 255, leave room for timestamp/random)
if (sanitized.length > 50) {
sanitized = sanitized.substring(0, 50);
}

// Additional safety: ensure it's not empty after sanitization
if (!sanitized || sanitized.length === 0) {
return 'project';
}

return sanitized;
return sanitized || 'project';
}

/**
* Creates a unique, sanitized temporary directory for Rust project compilation
*
* @param options - Configuration options for directory creation
* @returns Promise resolving to ProjectSetup with directory path and cleanup function
*
* @throws {Error} When directory creation fails
*
* @example
* ```typescript
* const project = await setupProject({ baseName: 'my-contract' });
* try {
* // Use project.tempDir for compilation
* console.log('Working in:', project.tempDir);
* } finally {
* await project.cleanup();
* }
* ```
*/
export async function setupProject(options: ProjectSetupOptions = {}): Promise<ProjectSetup> {
const { baseName = 'project', tempRoot = tmpdir() } = options;
const { baseName = 'project', tempRoot = tmpdir(), rustCode = DEFAULT_RUST_CODE } = options;

// Create a unique identifier to prevent collisions
const timestamp = Date.now();
const randomId = randomBytes(8).toString('hex');

// Sanitize the base name
const sanitizedBase = getSanitizedDirName(baseName);

// Create unique directory name - ensure we always have a base name
const finalBaseName = sanitizedBase || 'project';
const dirName = `${finalBaseName}_${timestamp}_${randomId}`;
const tempDir = join(tempRoot, dirName);
Expand All @@ -112,6 +126,9 @@ export async function setupProject(options: ProjectSetupOptions = {}): Promise<P
throw new Error(`Created path is not a directory: ${tempDir}`);
}

// Create the Rust project structure
await createRustProject(tempDir, rustCode);

return {
tempDir,
cleanup: () => cleanupProject(tempDir),
Expand All @@ -125,14 +142,6 @@ export async function setupProject(options: ProjectSetupOptions = {}): Promise<P

/**
* Safely removes a temporary project directory and all its contents
*
* @param tempDir - Absolute path to the temporary directory to remove
* @throws {Error} When cleanup fails or path validation fails
*
* @example
* ```typescript
* await cleanupProject('/tmp/project_1234567890_abcdef');
* ```
*/
export async function cleanupProject(tempDir: string): Promise<void> {
if (!tempDir || typeof tempDir !== 'string') {
Expand All @@ -149,15 +158,13 @@ export async function cleanupProject(tempDir: string): Promise<void> {
// Check if directory exists before attempting to remove
const stats = await fs.stat(tempDir).catch(() => null);
if (!stats) {
// Directory doesn't exist, nothing to clean
return;
}

if (!stats.isDirectory()) {
throw new Error(`Path is not a directory: ${tempDir}`);
}

// Remove the directory and all its contents
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
throw new Error(
Expand All @@ -168,10 +175,6 @@ export async function cleanupProject(tempDir: string): Promise<void> {

/**
* Creates a basic Rust project structure with Cargo.toml and lib.rs
*
* @param tempDir - The temporary directory to create the project in
* @param rustCode - The Rust code to write to lib.rs
* @throws {Error} When file creation fails
*/
export async function createRustProject(tempDir: string, rustCode: string): Promise<void> {
if (!tempDir || typeof tempDir !== 'string') {
Expand All @@ -184,41 +187,13 @@ export async function createRustProject(tempDir: string, rustCode: string): Prom

try {
// Create Cargo.toml for Soroban contract
const cargoToml = `[package]
name = "temp-contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = "21"

[dev-dependencies]
soroban-sdk = { version = "21", features = ["testutils"] }

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

[profile.release-with-logs]
inherits = "release"
debug-assertions = true
`;

// Create src directory
const srcDir = join(tempDir, 'src');
await fs.mkdir(srcDir, { recursive: true });

// Write Cargo.toml
await fs.writeFile(join(tempDir, 'Cargo.toml'), cargoToml, 'utf8');
await fs.writeFile(join(tempDir, 'Cargo.toml'), DEFAULT_CARGO_TOML, 'utf8');

// Write lib.rs
await fs.writeFile(join(srcDir, 'lib.rs'), rustCode, 'utf8');
Expand Down