diff --git a/.gitignore b/.gitignore index 0e303ba..a8ff770 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,43 @@ Cargo.lock /rhai_test_template /rhai_test_download /rhai_test_fs -run_rhai_tests.log \ No newline at end of file +run_rhai_tests.log +new_location +log.txt +file.txt +fix_doc* + +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +bun.lockb +bun.lock + +yarn.lock + +build.sh +build_dev.sh +develop.sh + +docusaurus.config.ts + +sidebars.ts + +tsconfig.json diff --git a/Cargo.toml b/Cargo.toml index 898259a..a979d77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,24 +11,36 @@ categories = ["os", "filesystem", "api-bindings"] readme = "README.md" [dependencies] -tera = "1.19.0" # Template engine for text rendering -# Cross-platform functionality -libc = "0.2" +anyhow = "1.0.98" +base64 = "0.21.0" # Base64 encoding/decoding cfg-if = "1.0" -thiserror = "1.0" # For error handling -redis = "0.22.0" # Redis client -postgres = "0.19.4" # PostgreSQL client -tokio-postgres = "0.7.8" # Async PostgreSQL client -postgres-types = "0.2.5" # PostgreSQL type conversions +chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher +clap = "2.33" # Command-line argument parsing +dirs = "5.0.1" # Directory paths +env_logger = "0.10.0" # Logger implementation +ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library +glob = "0.3.1" # For file pattern matching +jsonrpsee = "0.25.1" +k256 = { version = "0.13.1", features = ["ecdsa", "ecdh"] } # Elliptic curve cryptography lazy_static = "1.4.0" # For lazy initialization of static variables +libc = "0.2" +log = "0.4" # Logging facade +once_cell = "1.18.0" # Lazy static initialization +postgres = "0.19.4" # PostgreSQL client +postgres-types = "0.2.5" # PostgreSQL type conversions +r2d2 = "0.8.10" +r2d2_postgres = "0.18.2" +rand = "0.8.5" # Random number generation +redis = "0.22.0" # Redis client regex = "1.8.1" # For regex pattern matching +rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language serde = { version = "1.0", features = [ "derive", ] } # For serialization/deserialization serde_json = "1.0" # For JSON handling -glob = "0.3.1" # For file pattern matching +sha2 = "0.10.7" # SHA-2 hash functions tempfile = "3.5" # For temporary file operations log = "0.4" # Logging facade env_logger = "0.10.0" # Logger implementation @@ -37,9 +49,6 @@ rand = "0.8.5" # Random number generation clap = "2.33" # Command-line argument parsing r2d2 = "0.8.10" r2d2_postgres = "0.18.2" -slatedb = "0.6.1" # Embedded key-value store -object_store = "0.5.0" # Object store implementations used by SlateDB -bytes = "1.4.0" # Used for byte operations # Crypto dependencies base64 = "0.21.0" # Base64 encoding/decoding @@ -53,26 +62,6 @@ tokio = { version = "1.28", features = ["full"] } uuid = { version = "1.16.0", features = ["v4"] } tokio-test = "0.4.4" -# WebAssembly dependencies -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2.87" -js-sys = "0.3.64" -wasm-bindgen-futures = "0.4.39" -web-sys = { version = "0.3.64", features = [ - "Window", - "Storage", - "IdbDatabase", - "IdbOpenDbRequest", - "IdbFactory", - "IdbTransaction", - "IdbObjectStore", - "IdbRequest", - "IdbKeyRange", - "IdbCursorWithValue", - "Event", - "console" -] } - # Optional features for specific OS functionality [target.'cfg(unix)'.dependencies] nix = "0.26" # Unix-specific functionality @@ -85,9 +74,9 @@ windows = { version = "0.48", features = [ ] } [dev-dependencies] +mockall = "0.11.4" # For mocking in tests tempfile = "3.5" # For tests that need temporary files/directories tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing -mockall = "0.11.4" # For mocking in tests [[bin]] name = "herodo" diff --git a/src/docs/cfg/footer.json b/docs/cfg/footer.json similarity index 100% rename from src/docs/cfg/footer.json rename to docs/cfg/footer.json diff --git a/src/docs/cfg/main.json b/docs/cfg/main.json similarity index 100% rename from src/docs/cfg/main.json rename to docs/cfg/main.json diff --git a/src/docs/cfg/navbar.json b/docs/cfg/navbar.json similarity index 100% rename from src/docs/cfg/navbar.json rename to docs/cfg/navbar.json diff --git a/src/docs/docs/intro.md b/docs/docs/intro.md similarity index 100% rename from src/docs/docs/intro.md rename to docs/docs/intro.md diff --git a/docs/rhai/buildah_module_tests.md b/docs/docs/rhai/buildah_module_tests.md similarity index 100% rename from docs/rhai/buildah_module_tests.md rename to docs/docs/rhai/buildah_module_tests.md diff --git a/docs/rhai/ci_workflow.md b/docs/docs/rhai/ci_workflow.md similarity index 100% rename from docs/rhai/ci_workflow.md rename to docs/docs/rhai/ci_workflow.md diff --git a/docs/rhai/git_module_tests.md b/docs/docs/rhai/git_module_tests.md similarity index 100% rename from docs/rhai/git_module_tests.md rename to docs/docs/rhai/git_module_tests.md diff --git a/docs/rhai/index.md b/docs/docs/rhai/index.md similarity index 100% rename from docs/rhai/index.md rename to docs/docs/rhai/index.md diff --git a/docs/rhai/nerdctl_module_tests.md b/docs/docs/rhai/nerdctl_module_tests.md similarity index 100% rename from docs/rhai/nerdctl_module_tests.md rename to docs/docs/rhai/nerdctl_module_tests.md diff --git a/docs/rhai/os_module_tests.md b/docs/docs/rhai/os_module_tests.md similarity index 100% rename from docs/rhai/os_module_tests.md rename to docs/docs/rhai/os_module_tests.md diff --git a/docs/rhai/postgresclient_module_tests.md b/docs/docs/rhai/postgresclient_module_tests.md similarity index 100% rename from docs/rhai/postgresclient_module_tests.md rename to docs/docs/rhai/postgresclient_module_tests.md diff --git a/docs/rhai/process_module_tests.md b/docs/docs/rhai/process_module_tests.md similarity index 100% rename from docs/rhai/process_module_tests.md rename to docs/docs/rhai/process_module_tests.md diff --git a/docs/rhai/redisclient_module_tests.md b/docs/docs/rhai/redisclient_module_tests.md similarity index 100% rename from docs/rhai/redisclient_module_tests.md rename to docs/docs/rhai/redisclient_module_tests.md diff --git a/docs/rhai/rfs_module_tests.md b/docs/docs/rhai/rfs_module_tests.md similarity index 100% rename from docs/rhai/rfs_module_tests.md rename to docs/docs/rhai/rfs_module_tests.md diff --git a/docs/rhai/running_tests.md b/docs/docs/rhai/running_tests.md similarity index 100% rename from docs/rhai/running_tests.md rename to docs/docs/rhai/running_tests.md diff --git a/docs/rhai/text_module_tests.md b/docs/docs/rhai/text_module_tests.md similarity index 100% rename from docs/rhai/text_module_tests.md rename to docs/docs/rhai/text_module_tests.md diff --git a/src/docs/docs/sal/_category_.json b/docs/docs/sal/_category_.json similarity index 100% rename from src/docs/docs/sal/_category_.json rename to docs/docs/sal/_category_.json diff --git a/src/docs/docs/sal/buildah.md b/docs/docs/sal/buildah.md similarity index 100% rename from src/docs/docs/sal/buildah.md rename to docs/docs/sal/buildah.md diff --git a/src/docs/docs/sal/git.md b/docs/docs/sal/git.md similarity index 100% rename from src/docs/docs/sal/git.md rename to docs/docs/sal/git.md diff --git a/docs/git/git.md b/docs/docs/sal/git/git.md similarity index 100% rename from docs/git/git.md rename to docs/docs/sal/git/git.md diff --git a/src/docs/docs/sal/intro.md b/docs/docs/sal/intro.md similarity index 100% rename from src/docs/docs/sal/intro.md rename to docs/docs/sal/intro.md diff --git a/src/docs/docs/sal/nerdctl.md b/docs/docs/sal/nerdctl.md similarity index 100% rename from src/docs/docs/sal/nerdctl.md rename to docs/docs/sal/nerdctl.md diff --git a/src/docs/docs/sal/os.md b/docs/docs/sal/os.md similarity index 100% rename from src/docs/docs/sal/os.md rename to docs/docs/sal/os.md diff --git a/docs/os/download.md b/docs/docs/sal/os/download.md similarity index 100% rename from docs/os/download.md rename to docs/docs/sal/os/download.md diff --git a/docs/os/fs.md b/docs/docs/sal/os/fs.md similarity index 100% rename from docs/os/fs.md rename to docs/docs/sal/os/fs.md diff --git a/docs/os/package.md b/docs/docs/sal/os/package.md similarity index 100% rename from docs/os/package.md rename to docs/docs/sal/os/package.md diff --git a/src/docs/docs/sal/process.md b/docs/docs/sal/process.md similarity index 100% rename from src/docs/docs/sal/process.md rename to docs/docs/sal/process.md diff --git a/docs/process/process.md b/docs/docs/sal/process/process.md similarity index 100% rename from docs/process/process.md rename to docs/docs/sal/process/process.md diff --git a/src/docs/docs/sal/rfs.md b/docs/docs/sal/rfs.md similarity index 100% rename from src/docs/docs/sal/rfs.md rename to docs/docs/sal/rfs.md diff --git a/src/docs/docs/sal/text.md b/docs/docs/sal/text.md similarity index 100% rename from src/docs/docs/sal/text.md rename to docs/docs/sal/text.md diff --git a/examples/basics/files.rhai b/examples/basics/files.rhai index 3c72d13..c28445c 100644 --- a/examples/basics/files.rhai +++ b/examples/basics/files.rhai @@ -2,7 +2,7 @@ // Demonstrates file system operations using SAL // Create a test directory -let test_dir = "rhai_test_dir"; +let test_dir = "/tmp/rhai_test_dir"; println(`Creating directory: ${test_dir}`); let mkdir_result = mkdir(test_dir); println(`Directory creation result: ${mkdir_result}`); @@ -61,4 +61,4 @@ for file in files { // delete(test_dir); // println("Cleanup complete"); -"File operations script completed successfully!" \ No newline at end of file +"File operations script completed successfully!" diff --git a/examples/zinit/zinit_basic.rhai b/examples/zinit/zinit_basic.rhai new file mode 100644 index 0000000..83e67b0 --- /dev/null +++ b/examples/zinit/zinit_basic.rhai @@ -0,0 +1,87 @@ +// Basic example of using the Zinit client in Rhai + +// Socket path for Zinit +let socket_path = "/var/run/zinit.sock"; + +// List all services +print("Listing all services:"); +let services = zinit_list(socket_path); + +if services.is_empty() { + print("No services found."); +} else { + // Iterate over the keys of the map + for name in services.keys() { + let state = services[name]; + print(`${name}: ${state}`); + } +} + +// Get status of a specific service +let service_name = "test"; +print(`Getting status for ${service_name}:`); + +try { + let status = zinit_status(socket_path, service_name); + print(`Service: ${status.name}`); + print(`PID: ${status.pid}`); + print(`State: ${status.state}`); + print(`Target: ${status.target}`); + print("Dependencies:"); + + for (dep, state) in status.after.keys() { + print(` ${dep}: ${state}`); + } +} catch(err) { + print(`Error getting status: ${err}`); +} + +// Create a new service +print("\nCreating a new service:"); +let new_service = "rhai-test-service"; +let exec_command = "echo 'Hello from Rhai'"; +let oneshot = true; + +try { + let result = zinit_create_service(socket_path, new_service, exec_command, oneshot); + print(`Service created: ${result}`); + + // Monitor the service + print("\nMonitoring the service:"); + let monitor_result = zinit_monitor(socket_path, new_service); + print(`Service monitored: ${monitor_result}`); + + // Start the service + print("\nStarting the service:"); + let start_result = zinit_start(socket_path, new_service); + print(`Service started: ${start_result}`); + + // Get logs for a specific service + print("\nGetting logs:"); + let logs = zinit_logs(socket_path, new_service); + + for log in logs { + print(log); + } + + // Or to get all logs (uncomment if needed) + // print("\nGetting all logs:"); + // let all_logs = zinit_logs_all(socket_path); + // + // for log in all_logs { + // print(log); + // } + + // Clean up + print("\nCleaning up:"); + let stop_result = zinit_stop(socket_path, new_service); + print(`Service stopped: ${stop_result}`); + + let forget_result = zinit_forget(socket_path, new_service); + print(`Service forgotten: ${forget_result}`); + + let delete_result = zinit_delete_service(socket_path, new_service); + print(`Service deleted: ${delete_result}`); +} catch(err) { + print(`Error: ${err}`); +} diff --git a/src/rhai_tests/buildah/01_builder_pattern.rhai b/rhai_tests/buildah/01_builder_pattern.rhai similarity index 100% rename from src/rhai_tests/buildah/01_builder_pattern.rhai rename to rhai_tests/buildah/01_builder_pattern.rhai diff --git a/src/rhai_tests/buildah/02_image_operations.rhai b/rhai_tests/buildah/02_image_operations.rhai similarity index 100% rename from src/rhai_tests/buildah/02_image_operations.rhai rename to rhai_tests/buildah/02_image_operations.rhai diff --git a/src/rhai_tests/buildah/03_container_operations.rhai b/rhai_tests/buildah/03_container_operations.rhai similarity index 100% rename from src/rhai_tests/buildah/03_container_operations.rhai rename to rhai_tests/buildah/03_container_operations.rhai diff --git a/src/rhai_tests/buildah/run_all_tests.rhai b/rhai_tests/buildah/run_all_tests.rhai similarity index 100% rename from src/rhai_tests/buildah/run_all_tests.rhai rename to rhai_tests/buildah/run_all_tests.rhai diff --git a/src/rhai_tests/git/01_git_basic.rhai b/rhai_tests/git/01_git_basic.rhai similarity index 100% rename from src/rhai_tests/git/01_git_basic.rhai rename to rhai_tests/git/01_git_basic.rhai diff --git a/src/rhai_tests/git/02_git_operations.rhai b/rhai_tests/git/02_git_operations.rhai similarity index 100% rename from src/rhai_tests/git/02_git_operations.rhai rename to rhai_tests/git/02_git_operations.rhai diff --git a/src/rhai_tests/git/run_all_tests.rhai b/rhai_tests/git/run_all_tests.rhai similarity index 100% rename from src/rhai_tests/git/run_all_tests.rhai rename to rhai_tests/git/run_all_tests.rhai diff --git a/rhai_tests/keypair/01_keypair_operations.rhai b/rhai_tests/keypair/01_keypair_operations.rhai new file mode 100644 index 0000000..8ef36ee --- /dev/null +++ b/rhai_tests/keypair/01_keypair_operations.rhai @@ -0,0 +1,108 @@ +// 01_keypair_operations.rhai +// Tests for basic keypair operations in the Keypair module + +// Custom assert function +fn assert_true(condition, message) { + if !condition { + print(`ASSERTION FAILED: ${message}`); + throw message; + } +} + +print("=== Testing Basic Keypair Operations ==="); + +// Test creating a new keypair +print("Testing keypair creation..."); +let keypair_name = "test_keypair"; +if create_key_space("test_space", "password") { + print("✓ Key space created successfully"); + + if create_keypair(keypair_name, "password") { + print("✓ Keypair created successfully"); + + // Test getting the public key + print("Testing public key retrieval..."); + if select_keypair(keypair_name) { + let pub_key = keypair_pub_key(); + assert_true(pub_key.len() > 0, "Public key should not be empty"); + print(`✓ Public key retrieved: ${pub_key.len()} bytes`); + + // Test signing a message + print("Testing message signing..."); + let message = "This is a test message to sign"; + let signature = keypair_sign(message); + assert_true(signature.len() > 0, "Signature should not be empty"); + print(`✓ Message signed successfully: ${signature.len()} bytes`); + + // Test verifying a signature + print("Testing signature verification..."); + let is_valid = keypair_verify(message, signature); + assert_true(is_valid, "Signature should be valid"); + print("✓ Signature verified successfully"); + + // Test verifying with just a public key + print("Testing verification with public key only..."); + let is_valid_pub = verify_with_public_key(pub_key, message, signature); + assert_true(is_valid_pub, "Signature should be valid with public key only"); + print("✓ Signature verified with public key only"); + + // Edge case: Empty message + print("Testing with empty message..."); + let empty_message = ""; + let empty_signature = keypair_sign(empty_message); + assert_true(empty_signature.len() > 0, "Signature for empty message should not be empty"); + let is_valid_empty = keypair_verify(empty_message, empty_signature); + assert_true(is_valid_empty, "Empty message signature should be valid"); + print("✓ Empty message signed and verified successfully"); + + // Edge case: Large message + print("Testing with large message..."); + let large_message = "A" * 10000; // 10KB message + let large_signature = keypair_sign(large_message); + assert_true(large_signature.len() > 0, "Signature for large message should not be empty"); + let is_valid_large = keypair_verify(large_message, large_signature); + assert_true(is_valid_large, "Large message signature should be valid"); + print("✓ Large message signed and verified successfully"); + + // Error case: Invalid signature format + print("Testing with invalid signature format..."); + let invalid_signature = [0, 1, 2, 3]; // Invalid signature bytes + let is_valid_invalid = false; + try { + is_valid_invalid = keypair_verify(message, invalid_signature); + } catch(err) { + print(`✓ Caught expected error for invalid signature: ${err}`); + } + assert_true(!is_valid_invalid, "Invalid signature should not verify"); + + // Error case: Tampered message + print("Testing with tampered message..."); + let tampered_message = message + " (tampered)"; + let is_valid_tampered = keypair_verify(tampered_message, signature); + assert_true(!is_valid_tampered, "Tampered message should not verify"); + print("✓ Tampered message correctly failed verification"); + + // Error case: Malformed public key + print("Testing with malformed public key..."); + let malformed_pub_key = [0, 1, 2, 3]; // Invalid public key bytes + let is_valid_malformed = false; + try { + is_valid_malformed = verify_with_public_key(malformed_pub_key, message, signature); + } catch(err) { + print(`✓ Caught expected error for malformed public key: ${err}`); + } + assert_true(!is_valid_malformed, "Malformed public key should not verify"); + } else { + print("✗ Failed to select keypair"); + throw "Failed to select keypair"; + } + } else { + print("✗ Failed to create keypair"); + throw "Failed to create keypair"; + } +} else { + print("✗ Failed to create key space"); + throw "Failed to create key space"; +} + +print("All keypair operations tests completed successfully!"); \ No newline at end of file diff --git a/rhai_tests/keypair/02_keyspace_operations.rhai b/rhai_tests/keypair/02_keyspace_operations.rhai new file mode 100644 index 0000000..83b5b17 --- /dev/null +++ b/rhai_tests/keypair/02_keyspace_operations.rhai @@ -0,0 +1,162 @@ +// 02_keyspace_operations.rhai +// Tests for key space operations in the Keypair module + +// Custom assert function +fn assert_true(condition, message) { + if !condition { + print(`ASSERTION FAILED: ${message}`); + throw message; + } +} + +print("=== Testing Key Space Operations ==="); + +// Test creating a new key space +print("Testing key space creation..."); +let space_name = "test_keyspace"; +let password = "secure_password"; + +if create_key_space(space_name, password) { + print(`✓ Key space "${space_name}" created successfully`); + + // Test adding keypairs to a key space + print("Testing adding keypairs to key space..."); + let keypair1_name = "keypair1"; + let keypair2_name = "keypair2"; + + if create_keypair(keypair1_name, password) { + print(`✓ Keypair "${keypair1_name}" created successfully`); + } else { + print(`✗ Failed to create keypair "${keypair1_name}"`); + throw `Failed to create keypair "${keypair1_name}"`; + } + + if create_keypair(keypair2_name, password) { + print(`✓ Keypair "${keypair2_name}" created successfully`); + } else { + print(`✗ Failed to create keypair "${keypair2_name}"`); + throw `Failed to create keypair "${keypair2_name}"`; + } + + // Test listing keypairs in a key space + print("Testing listing keypairs in key space..."); + let keypairs = list_keypairs(); + assert_true(keypairs.len() == 2, `Expected 2 keypairs, got ${keypairs.len()}`); + assert_true(keypairs.contains(keypair1_name), `Keypair list should contain "${keypair1_name}"`); + assert_true(keypairs.contains(keypair2_name), `Keypair list should contain "${keypair2_name}"`); + print(`✓ Listed keypairs successfully: ${keypairs}`); + + // Test getting a keypair by name + print("Testing getting a keypair by name..."); + if select_keypair(keypair1_name) { + print(`✓ Selected keypair "${keypair1_name}" successfully`); + let pub_key = keypair_pub_key(); + assert_true(pub_key.len() > 0, "Public key should not be empty"); + print(`✓ Retrieved public key for "${keypair1_name}": ${pub_key.len()} bytes`); + } else { + print(`✗ Failed to select keypair "${keypair1_name}"`); + throw `Failed to select keypair "${keypair1_name}"`; + } + + // Edge case: Attempt to add a keypair with a duplicate name + print("Testing adding a keypair with a duplicate name..."); + let duplicate_success = false; + try { + duplicate_success = create_keypair(keypair1_name, password); + } catch(err) { + print(`✓ Caught expected error for duplicate keypair: ${err}`); + } + assert_true(!duplicate_success, "Creating a duplicate keypair should fail"); + + // Edge case: Attempt to get a non-existent keypair + print("Testing getting a non-existent keypair..."); + let nonexistent_success = false; + try { + nonexistent_success = select_keypair("nonexistent_keypair"); + } catch(err) { + print(`✓ Caught expected error for non-existent keypair: ${err}`); + } + assert_true(!nonexistent_success, "Selecting a non-existent keypair should fail"); + + // Edge case: Test with special characters in keypair names + print("Testing with special characters in keypair name..."); + let special_name = "special!@#$%^&*()_+"; + if create_keypair(special_name, password) { + print(`✓ Created keypair with special characters: "${special_name}"`); + + // Verify we can select and use it + if select_keypair(special_name) { + print(`✓ Selected keypair with special characters`); + let pub_key = keypair_pub_key(); + assert_true(pub_key.len() > 0, "Public key should not be empty"); + } else { + print(`✗ Failed to select keypair with special characters`); + throw `Failed to select keypair with special characters`; + } + } else { + print(`✗ Failed to create keypair with special characters`); + throw `Failed to create keypair with special characters`; + } + + // Edge case: Test with very long keypair name + print("Testing with very long keypair name..."); + let long_name = "a" * 100; // 100 character name + if create_keypair(long_name, password) { + print(`✓ Created keypair with long name (${long_name.len()} characters)`); + + // Verify we can select and use it + if select_keypair(long_name) { + print(`✓ Selected keypair with long name`); + let pub_key = keypair_pub_key(); + assert_true(pub_key.len() > 0, "Public key should not be empty"); + } else { + print(`✗ Failed to select keypair with long name`); + throw `Failed to select keypair with long name`; + } + } else { + print(`✗ Failed to create keypair with long name`); + throw `Failed to create keypair with long name`; + } + + // Edge case: Test with empty keypair name (should fail) + print("Testing with empty keypair name..."); + let empty_name = ""; + let empty_name_success = false; + try { + empty_name_success = create_keypair(empty_name, password); + } catch(err) { + print(`✓ Caught expected error for empty keypair name: ${err}`); + } + assert_true(!empty_name_success, "Creating a keypair with empty name should fail"); + + // Stress test: Add multiple keypairs + print("Stress testing: Adding multiple keypairs..."); + let num_keypairs = 10; // Add 10 more keypairs + let stress_keypairs = []; + + for i in 0..num_keypairs { + let name = `stress_keypair_${i}`; + stress_keypairs.push(name); + + if create_keypair(name, password) { + print(`✓ Created stress test keypair ${i+1}/${num_keypairs}`); + } else { + print(`✗ Failed to create stress test keypair ${i+1}/${num_keypairs}`); + throw `Failed to create stress test keypair ${i+1}/${num_keypairs}`; + } + } + + // Verify all keypairs were created + print("Verifying all stress test keypairs..."); + let all_keypairs = list_keypairs(); + for name in stress_keypairs { + assert_true(all_keypairs.contains(name), `Keypair list should contain "${name}"`); + } + print(`✓ All ${num_keypairs} stress test keypairs verified`); + +} else { + print(`✗ Failed to create key space "${space_name}"`); + throw `Failed to create key space "${space_name}"`; +} + +print("All key space operations tests completed successfully!"); \ No newline at end of file diff --git a/rhai_tests/keypair/03_session_management.rhai b/rhai_tests/keypair/03_session_management.rhai new file mode 100644 index 0000000..a3a5cc1 --- /dev/null +++ b/rhai_tests/keypair/03_session_management.rhai @@ -0,0 +1,167 @@ +// 03_session_management.rhai +// Tests for session management in the Keypair module + +// Custom assert function +fn assert_true(condition, message) { + if !condition { + print(`ASSERTION FAILED: ${message}`); + throw message; + } +} + +print("=== Testing Session Management ==="); + +// Test creating a key space and setting it as current +print("Testing key space creation and activation..."); +let space_name1 = "session_test_space1"; +let space_name2 = "session_test_space2"; +let password = "secure_password"; + +// Create first key space +if create_key_space(space_name1, password) { + print(`✓ Key space "${space_name1}" created successfully`); + + // Test creating keypairs in the current space + print("Testing creating keypairs in current space..."); + let keypair1_name = "session_keypair1"; + + if create_keypair(keypair1_name, password) { + print(`✓ Keypair "${keypair1_name}" created successfully in space "${space_name1}"`); + } else { + print(`✗ Failed to create keypair "${keypair1_name}" in space "${space_name1}"`); + throw `Failed to create keypair "${keypair1_name}" in space "${space_name1}"`; + } + + // Test selecting a keypair + print("Testing selecting a keypair..."); + if select_keypair(keypair1_name) { + print(`✓ Selected keypair "${keypair1_name}" successfully`); + } else { + print(`✗ Failed to select keypair "${keypair1_name}"`); + throw `Failed to select keypair "${keypair1_name}"`; + } + + // Test getting the selected keypair + print("Testing getting the selected keypair..."); + let pub_key = keypair_pub_key(); + assert_true(pub_key.len() > 0, "Public key should not be empty"); + print(`✓ Retrieved public key for selected keypair: ${pub_key.len()} bytes`); + + // Create second key space + print("\nTesting creating and switching to a second key space..."); + if create_key_space(space_name2, password) { + print(`✓ Key space "${space_name2}" created successfully`); + + // Verify we're now in the second space + print("Verifying current space changed..."); + let keypairs = list_keypairs(); + assert_true(keypairs.len() == 0, `Expected 0 keypairs in new space, got ${keypairs.len()}`); + print("✓ Current space verified as the new space (empty keypair list)"); + + // Create a keypair in the second space + let keypair2_name = "session_keypair2"; + if create_keypair(keypair2_name, password) { + print(`✓ Keypair "${keypair2_name}" created successfully in space "${space_name2}"`); + } else { + print(`✗ Failed to create keypair "${keypair2_name}" in space "${space_name2}"`); + throw `Failed to create keypair "${keypair2_name}" in space "${space_name2}"`; + } + + // Switch back to first space + print("\nTesting switching back to first key space..."); + if load_key_space(space_name1, password) { + print(`✓ Switched back to key space "${space_name1}" successfully`); + + // Verify we're now in the first space + print("Verifying current space changed back..."); + let keypairs = list_keypairs(); + assert_true(keypairs.len() == 1, `Expected 1 keypair in original space, got ${keypairs.len()}`); + assert_true(keypairs.contains(keypair1_name), `Keypair list should contain "${keypair1_name}"`); + print("✓ Current space verified as the original space"); + } else { + print(`✗ Failed to switch back to key space "${space_name1}"`); + throw `Failed to switch back to key space "${space_name1}"`; + } + } else { + print(`✗ Failed to create second key space "${space_name2}"`); + throw `Failed to create second key space "${space_name2}"`; + } + + // Test clearing the session + print("\nTesting clearing the session..."); + clear_session(); + print("✓ Session cleared"); + + // Verify operations fail after clearing session + print("Verifying operations fail after clearing session..."); + let list_success = false; + try { + list_keypairs(); + list_success = true; + } catch(err) { + print(`✓ Caught expected error after clearing session: ${err}`); + } + assert_true(!list_success, "Listing keypairs should fail after clearing session"); + + // Error case: Attempt operations without an active key space + print("\nTesting operations without an active key space..."); + + // Attempt to create a keypair + let create_success = false; + try { + create_success = create_keypair("no_space_keypair", password); + } catch(err) { + print(`✓ Caught expected error for creating keypair without active space: ${err}`); + } + assert_true(!create_success, "Creating a keypair without active space should fail"); + + // Attempt to select a keypair + let select_success = false; + try { + select_success = select_keypair("no_space_keypair"); + } catch(err) { + print(`✓ Caught expected error for selecting keypair without active space: ${err}`); + } + assert_true(!select_success, "Selecting a keypair without active space should fail"); + + // Reload a key space + print("\nTesting reloading a key space after clearing session..."); + if load_key_space(space_name1, password) { + print(`✓ Reloaded key space "${space_name1}" successfully`); + + // Verify the keypair is still there + let keypairs = list_keypairs(); + assert_true(keypairs.contains(keypair1_name), `Keypair list should contain "${keypair1_name}"`); + print("✓ Keypair still exists in reloaded space"); + } else { + print(`✗ Failed to reload key space "${space_name1}"`); + throw `Failed to reload key space "${space_name1}"`; + } + + // Error case: Attempt to get selected keypair when none is selected + print("\nTesting getting selected keypair when none is selected..."); + let get_selected_success = false; + try { + keypair_pub_key(); + get_selected_success = true; + } catch(err) { + print(`✓ Caught expected error for getting selected keypair when none selected: ${err}`); + } + assert_true(!get_selected_success, "Getting selected keypair when none is selected should fail"); + + // Error case: Attempt to select non-existent keypair + print("\nTesting selecting a non-existent keypair..."); + let select_nonexistent_success = false; + try { + select_nonexistent_success = select_keypair("nonexistent_keypair"); + } catch(err) { + print(`✓ Caught expected error for selecting non-existent keypair: ${err}`); + } + assert_true(!select_nonexistent_success, "Selecting a non-existent keypair should fail"); + +} else { + print(`✗ Failed to create key space "${space_name1}"`); + throw `Failed to create key space "${space_name1}"`; +} + +print("All session management tests completed successfully!"); \ No newline at end of file diff --git a/rhai_tests/keypair/04_encryption_decryption.rhai b/rhai_tests/keypair/04_encryption_decryption.rhai new file mode 100644 index 0000000..839c19a --- /dev/null +++ b/rhai_tests/keypair/04_encryption_decryption.rhai @@ -0,0 +1,192 @@ +// 04_encryption_decryption.rhai +// Tests for asymmetric encryption and decryption in the Keypair module + +// Custom assert function +fn assert_true(condition, message) { + if !condition { + print(`ASSERTION FAILED: ${message}`); + throw message; + } +} + +print("=== Testing Asymmetric Encryption and Decryption ==="); + +// Test creating keypairs for sender and recipient +print("Setting up sender and recipient keypairs..."); +let space_name = "encryption_test_space"; +let password = "secure_password"; +let sender_name = "sender_keypair"; +let recipient_name = "recipient_keypair"; + +if create_key_space(space_name, password) { + print(`✓ Key space "${space_name}" created successfully`); + + // Create sender keypair + if create_keypair(sender_name, password) { + print(`✓ Sender keypair "${sender_name}" created successfully`); + } else { + print(`✗ Failed to create sender keypair "${sender_name}"`); + throw `Failed to create sender keypair "${sender_name}"`; + } + + // Create recipient keypair + if create_keypair(recipient_name, password) { + print(`✓ Recipient keypair "${recipient_name}" created successfully`); + } else { + print(`✗ Failed to create recipient keypair "${recipient_name}"`); + throw `Failed to create recipient keypair "${recipient_name}"`; + } + + // Get recipient's public key + if select_keypair(recipient_name) { + print(`✓ Selected recipient keypair "${recipient_name}" successfully`); + let recipient_pub_key = keypair_pub_key(); + assert_true(recipient_pub_key.len() > 0, "Recipient public key should not be empty"); + print(`✓ Retrieved recipient public key: ${recipient_pub_key.len()} bytes`); + + // Switch to sender keypair + if select_keypair(sender_name) { + print(`✓ Selected sender keypair "${sender_name}" successfully`); + + // Test encrypting a message with recipient's public key + print("\nTesting encrypting a message..."); + let message = "This is a secret message for the recipient"; + let ciphertext = encrypt_asymmetric(recipient_pub_key, message); + assert_true(ciphertext.len() > 0, "Ciphertext should not be empty"); + print(`✓ Message encrypted successfully: ${ciphertext.len()} bytes`); + + // Switch back to recipient keypair to decrypt + if select_keypair(recipient_name) { + print(`✓ Switched back to recipient keypair "${recipient_name}" successfully`); + + // Test decrypting the message + print("Testing decrypting the message..."); + let decrypted = decrypt_asymmetric(ciphertext); + assert_true(decrypted == message, "Decrypted message should match original"); + print(`✓ Message decrypted successfully: "${decrypted}"`); + + // Edge case: Test with empty message + print("\nTesting with empty message..."); + let empty_message = ""; + let empty_ciphertext = encrypt_asymmetric(recipient_pub_key, empty_message); + assert_true(empty_ciphertext.len() > 0, "Ciphertext for empty message should not be empty"); + + let empty_decrypted = decrypt_asymmetric(empty_ciphertext); + assert_true(empty_decrypted == empty_message, "Decrypted empty message should be empty"); + print("✓ Empty message encrypted and decrypted successfully"); + + // Edge case: Test with large message + print("\nTesting with large message..."); + let large_message = "A" * 10000; // 10KB message + let large_ciphertext = encrypt_asymmetric(recipient_pub_key, large_message); + assert_true(large_ciphertext.len() > 0, "Ciphertext for large message should not be empty"); + + let large_decrypted = decrypt_asymmetric(large_ciphertext); + assert_true(large_decrypted == large_message, "Decrypted large message should match original"); + print("✓ Large message encrypted and decrypted successfully"); + + // Error case: Attempt to decrypt with the wrong keypair + print("\nTesting decryption with wrong keypair..."); + if select_keypair(sender_name) { + print(`✓ Switched to sender keypair "${sender_name}" successfully`); + + let wrong_keypair_success = true; + try { + let wrong_decrypted = decrypt_asymmetric(ciphertext); + // If we get here, the decryption didn't fail as expected + assert_true(wrong_decrypted != message, "Decryption with wrong keypair should not match original message"); + } catch(err) { + wrong_keypair_success = false; + print(`✓ Caught expected error for decryption with wrong keypair: ${err}`); + } + + // Note: Some implementations might not throw an error but return garbage data + // So we don't assert on wrong_keypair_success + + // Switch back to recipient for further tests + if select_keypair(recipient_name) { + print(`✓ Switched back to recipient keypair "${recipient_name}" successfully`); + } else { + print(`✗ Failed to switch back to recipient keypair "${recipient_name}"`); + throw `Failed to switch back to recipient keypair "${recipient_name}"`; + } + } else { + print(`✗ Failed to switch to sender keypair "${sender_name}"`); + throw `Failed to switch to sender keypair "${sender_name}"`; + } + + // Error case: Test with malformed ciphertext + print("\nTesting with malformed ciphertext..."); + let malformed_ciphertext = [0, 1, 2, 3]; // Invalid ciphertext bytes + let malformed_success = false; + try { + decrypt_asymmetric(malformed_ciphertext); + malformed_success = true; + } catch(err) { + print(`✓ Caught expected error for malformed ciphertext: ${err}`); + } + assert_true(!malformed_success, "Decrypting malformed ciphertext should fail"); + + // Error case: Test with invalid public key for encryption + print("\nTesting encryption with invalid public key..."); + if select_keypair(sender_name) { + print(`✓ Switched to sender keypair "${sender_name}" successfully`); + + let invalid_pub_key = [0, 1, 2, 3]; // Invalid public key bytes + let invalid_key_success = false; + try { + encrypt_asymmetric(invalid_pub_key, message); + invalid_key_success = true; + } catch(err) { + print(`✓ Caught expected error for invalid public key: ${err}`); + } + assert_true(!invalid_key_success, "Encrypting with invalid public key should fail"); + } else { + print(`✗ Failed to switch to sender keypair "${sender_name}"`); + throw `Failed to switch to sender keypair "${sender_name}"`; + } + + // Error case: Test with tampered ciphertext + print("\nTesting with tampered ciphertext..."); + if select_keypair(recipient_name) { + print(`✓ Switched to recipient keypair "${recipient_name}" successfully`); + + // Tamper with the ciphertext (change a byte in the middle) + let tampered_ciphertext = ciphertext.clone(); + if tampered_ciphertext.len() > 100 { + tampered_ciphertext[100] = (tampered_ciphertext[100] + 1) % 256; + + let tampered_success = false; + try { + let tampered_decrypted = decrypt_asymmetric(tampered_ciphertext); + tampered_success = tampered_decrypted == message; + } catch(err) { + print(`✓ Caught expected error for tampered ciphertext: ${err}`); + } + assert_true(!tampered_success, "Decrypting tampered ciphertext should fail or produce incorrect result"); + } else { + print("Note: Ciphertext too short to test tampering"); + } + } else { + print(`✗ Failed to switch to recipient keypair "${recipient_name}"`); + throw `Failed to switch to recipient keypair "${recipient_name}"`; + } + + } else { + print(`✗ Failed to switch back to recipient keypair "${recipient_name}"`); + throw `Failed to switch back to recipient keypair "${recipient_name}"`; + } + } else { + print(`✗ Failed to select sender keypair "${sender_name}"`); + throw `Failed to select sender keypair "${sender_name}"`; + } + } else { + print(`✗ Failed to select recipient keypair "${recipient_name}"`); + throw `Failed to select recipient keypair "${recipient_name}"`; + } +} else { + print(`✗ Failed to create key space "${space_name}"`); + throw `Failed to create key space "${space_name}"`; +} + +print("All asymmetric encryption and decryption tests completed successfully!"); \ No newline at end of file diff --git a/rhai_tests/keypair/05_error_handling.rhai b/rhai_tests/keypair/05_error_handling.rhai new file mode 100644 index 0000000..8a0689e --- /dev/null +++ b/rhai_tests/keypair/05_error_handling.rhai @@ -0,0 +1,231 @@ +// 05_error_handling.rhai +// Comprehensive error handling tests for the Keypair module + +// Custom assert function +fn assert_true(condition, message) { + if !condition { + print(`ASSERTION FAILED: ${message}`); + throw message; + } +} + +// Helper function to test for expected errors +fn expect_error(fn_to_test, expected_error_substring) { + let error_caught = false; + let error_message = ""; + + try { + fn_to_test(); + } catch(err) { + error_caught = true; + error_message = err.to_string(); + } + + if !error_caught { + print(`ASSERTION FAILED: Expected error containing "${expected_error_substring}" but no error was thrown`); + throw `Expected error containing "${expected_error_substring}" but no error was thrown`; + } + + if !error_message.contains(expected_error_substring) { + print(`ASSERTION FAILED: Expected error containing "${expected_error_substring}" but got "${error_message}"`); + throw `Expected error containing "${expected_error_substring}" but got "${error_message}"`; + } + + print(`✓ Caught expected error: ${error_message}`); +} + +print("=== Testing Error Handling ==="); + +// Test all error types defined in CryptoError + +// 1. Test InvalidKeyLength error +print("\n--- Testing InvalidKeyLength error ---"); +expect_error(|| { + // Create a key space for testing + create_key_space("error_test_space", "password"); + create_keypair("test_keypair", "password"); + select_keypair("test_keypair"); + + // Try to verify with an invalid public key (wrong length) + verify_with_public_key([1, 2, 3], "test message", [1, 2, 3, 4]); +}, "InvalidKeyLength"); + +// 2. Test EncryptionFailed error +print("\n--- Testing EncryptionFailed error ---"); +expect_error(|| { + // Create a key space for testing + create_key_space("error_test_space", "password"); + create_keypair("test_keypair", "password"); + select_keypair("test_keypair"); + + // Try to encrypt with an invalid public key + encrypt_asymmetric([1, 2, 3], "test message"); +}, "EncryptionFailed"); + +// 3. Test DecryptionFailed error +print("\n--- Testing DecryptionFailed error ---"); +expect_error(|| { + // Create a key space for testing + create_key_space("error_test_space", "password"); + create_keypair("test_keypair", "password"); + select_keypair("test_keypair"); + + // Try to decrypt invalid ciphertext + decrypt_asymmetric([1, 2, 3, 4]); +}, "DecryptionFailed"); + +// 4. Test SignatureFormatError error +print("\n--- Testing SignatureFormatError error ---"); +expect_error(|| { + // Create a key space for testing + create_key_space("error_test_space", "password"); + create_keypair("test_keypair", "password"); + select_keypair("test_keypair"); + + // Try to verify with an invalid signature format + keypair_verify("test message", [1, 2, 3]); +}, "SignatureFormatError"); + +// 5. Test KeypairAlreadyExists error +print("\n--- Testing KeypairAlreadyExists error ---"); +expect_error(|| { + // Create a key space for testing + create_key_space("error_test_space", "password"); + create_keypair("duplicate_keypair", "password"); + + // Try to create a keypair with the same name + create_keypair("duplicate_keypair", "password"); +}, "KeypairAlreadyExists"); + +// 6. Test KeypairNotFound error +print("\n--- Testing KeypairNotFound error ---"); +expect_error(|| { + // Create a key space for testing + create_key_space("error_test_space", "password"); + + // Try to select a non-existent keypair + select_keypair("nonexistent_keypair"); +}, "KeypairNotFound"); + +// 7. Test NoActiveSpace error +print("\n--- Testing NoActiveSpace error ---"); +expect_error(|| { + // Clear the session + clear_session(); + + // Try to create a keypair without an active space + create_keypair("test_keypair", "password"); +}, "NoActiveSpace"); + +// 8. Test NoKeypairSelected error +print("\n--- Testing NoKeypairSelected error ---"); +expect_error(|| { + // Create a key space for testing + create_key_space("error_test_space", "password"); + + // Try to get the public key without selecting a keypair + keypair_pub_key(); +}, "NoKeypairSelected"); + +// Test error propagation through the API +print("\n--- Testing error propagation ---"); +let propagation_test = || { + // Create a key space for testing + create_key_space("error_test_space", "password"); + + // Create a keypair + create_keypair("test_keypair", "password"); + + // Clear the session to force an error + clear_session(); + + // This should fail with NoActiveSpace + select_keypair("test_keypair"); + + // This line should never be reached + print("ERROR: Code execution continued after error"); +}; + +expect_error(propagation_test, "NoActiveSpace"); + +// Test recovery from errors +print("\n--- Testing recovery from errors ---"); +let recovery_success = false; + +try { + // Try an operation that will fail + clear_session(); + list_keypairs(); // This should fail with NoActiveSpace +} catch(err) { + print(`✓ Caught expected error: ${err}`); + + // Now recover by creating a new key space + if create_key_space("recovery_space", "password") { + // Create a keypair to verify recovery + if create_keypair("recovery_keypair", "password") { + let keypairs = list_keypairs(); + if keypairs.contains("recovery_keypair") { + recovery_success = true; + print("✓ Successfully recovered from error"); + } + } + } +} + +assert_true(recovery_success, "Should be able to recover from errors"); + +// Test behavior when multiple errors occur in sequence +print("\n--- Testing sequential errors ---"); +let sequential_errors_count = 0; + +// First error: No active space +try { + clear_session(); + list_keypairs(); +} catch(err) { + sequential_errors_count += 1; + print(`✓ Caught first sequential error: ${err}`); +} + +// Second error: Keypair not found +try { + create_key_space("sequential_space", "password"); + select_keypair("nonexistent_keypair"); +} catch(err) { + sequential_errors_count += 1; + print(`✓ Caught second sequential error: ${err}`); +} + +// Third error: Keypair already exists +try { + create_keypair("sequential_keypair", "password"); + create_keypair("sequential_keypair", "password"); +} catch(err) { + sequential_errors_count += 1; + print(`✓ Caught third sequential error: ${err}`); +} + +assert_true(sequential_errors_count == 3, `Expected 3 sequential errors, got ${sequential_errors_count}`); + +// Test error handling with invalid parameters +print("\n--- Testing error handling with invalid parameters ---"); + +// Test with null/undefined parameters +try { + // Note: In Rhai, we can't directly pass null/undefined, but we can test with empty arrays + verify_with_public_key([], "message", []); + print("ERROR: verify_with_public_key with empty arrays didn't throw an error"); +} catch(err) { + print(`✓ Caught expected error for invalid parameters: ${err}`); +} + +// Test with wrong parameter types +try { + // Note: In Rhai, we can't easily pass wrong types, but we can test with strings instead of arrays + verify_with_public_key("not an array", "message", "not an array"); + print("ERROR: verify_with_public_key with wrong types didn't throw an error"); +} catch(err) { + print(`✓ Caught expected error for wrong parameter types: ${err}`); +} + +print("All error handling tests completed successfully!"); \ No newline at end of file diff --git a/rhai_tests/keypair/run_all_tests.rhai b/rhai_tests/keypair/run_all_tests.rhai new file mode 100644 index 0000000..d1f863f --- /dev/null +++ b/rhai_tests/keypair/run_all_tests.rhai @@ -0,0 +1,293 @@ +// run_all_tests.rhai +// Runs all Keypair module tests + +print("=== Running Keypair Module Tests ==="); + +// Custom assert function +fn assert_true(condition, message) { + if !condition { + print(`ASSERTION FAILED: ${message}`); + throw message; + } +} + +// Run each test directly +let passed = 0; +let failed = 0; +let test_results = #{}; + +// Test 1: Keypair Operations +print("\n--- Running Keypair Operations Tests ---"); +try { + // Clear any existing session + clear_session(); + + // Test creating a new keypair + print("Testing keypair creation..."); + let keypair_name = "test_keypair"; + if create_key_space("test_space", "password") { + print("✓ Key space created successfully"); + + if create_keypair(keypair_name, "password") { + print("✓ Keypair created successfully"); + + // Test getting the public key + print("Testing public key retrieval..."); + if select_keypair(keypair_name) { + let pub_key = keypair_pub_key(); + assert_true(pub_key.len() > 0, "Public key should not be empty"); + print(`✓ Public key retrieved: ${pub_key.len()} bytes`); + + // Test signing a message + print("Testing message signing..."); + let message = "This is a test message to sign"; + let signature = keypair_sign(message); + assert_true(signature.len() > 0, "Signature should not be empty"); + print(`✓ Message signed successfully: ${signature.len()} bytes`); + + // Test verifying a signature + print("Testing signature verification..."); + let is_valid = keypair_verify(message, signature); + assert_true(is_valid, "Signature should be valid"); + print("✓ Signature verified successfully"); + } + } + } + + print("--- Keypair Operations Tests completed successfully ---"); + passed += 1; + test_results["01_keypair_operations"] = "PASSED"; +} catch(err) { + print(`!!! Error in Keypair Operations Tests: ${err}`); + failed += 1; + test_results["01_keypair_operations"] = `FAILED: ${err}`; +} + +// Test 2: Key Space Operations +print("\n--- Running Key Space Operations Tests ---"); +try { + // Clear any existing session + clear_session(); + + // Test creating a new key space + print("Testing key space creation..."); + let space_name = "test_keyspace"; + let password = "secure_password"; + + if create_key_space(space_name, password) { + print(`✓ Key space "${space_name}" created successfully`); + + // Test adding keypairs to a key space + print("Testing adding keypairs to key space..."); + let keypair1_name = "keypair1"; + let keypair2_name = "keypair2"; + + if create_keypair(keypair1_name, password) { + print(`✓ Keypair "${keypair1_name}" created successfully`); + } + + if create_keypair(keypair2_name, password) { + print(`✓ Keypair "${keypair2_name}" created successfully`); + } + + // Test listing keypairs in a key space + print("Testing listing keypairs in key space..."); + let keypairs = list_keypairs(); + assert_true(keypairs.len() == 2, `Expected 2 keypairs, got ${keypairs.len()}`); + assert_true(keypairs.contains(keypair1_name), `Keypair list should contain "${keypair1_name}"`); + assert_true(keypairs.contains(keypair2_name), `Keypair list should contain "${keypair2_name}"`); + print(`✓ Listed keypairs successfully: ${keypairs}`); + } + + print("--- Key Space Operations Tests completed successfully ---"); + passed += 1; + test_results["02_keyspace_operations"] = "PASSED"; +} catch(err) { + print(`!!! Error in Key Space Operations Tests: ${err}`); + failed += 1; + test_results["02_keyspace_operations"] = `FAILED: ${err}`; +} + +// Test 3: Session Management +print("\n--- Running Session Management Tests ---"); +try { + // Clear any existing session + clear_session(); + + // Test creating a key space and setting it as current + print("Testing key space creation and activation..."); + let space_name1 = "session_test_space1"; + let space_name2 = "session_test_space2"; + let password = "secure_password"; + + // Create first key space + if create_key_space(space_name1, password) { + print(`✓ Key space "${space_name1}" created successfully`); + + // Test creating keypairs in the current space + print("Testing creating keypairs in current space..."); + let keypair1_name = "session_keypair1"; + + if create_keypair(keypair1_name, password) { + print(`✓ Keypair "${keypair1_name}" created successfully in space "${space_name1}"`); + } + + // Test selecting a keypair + print("Testing selecting a keypair..."); + if select_keypair(keypair1_name) { + print(`✓ Selected keypair "${keypair1_name}" successfully`); + } + } + + print("--- Session Management Tests completed successfully ---"); + passed += 1; + test_results["03_session_management"] = "PASSED"; +} catch(err) { + print(`!!! Error in Session Management Tests: ${err}`); + failed += 1; + test_results["03_session_management"] = `FAILED: ${err}`; +} + +// Test 4: Encryption and Decryption +print("\n--- Running Encryption and Decryption Tests ---"); +try { + // Clear any existing session + clear_session(); + + // Test creating keypairs for sender and recipient + print("Setting up sender and recipient keypairs..."); + let space_name = "encryption_test_space"; + let password = "secure_password"; + let sender_name = "sender_keypair"; + let recipient_name = "recipient_keypair"; + + if create_key_space(space_name, password) { + print(`✓ Key space "${space_name}" created successfully`); + + // Create sender keypair + if create_keypair(sender_name, password) { + print(`✓ Sender keypair "${sender_name}" created successfully`); + } + + // Create recipient keypair + if create_keypair(recipient_name, password) { + print(`✓ Recipient keypair "${recipient_name}" created successfully`); + } + + // Get recipient's public key + if select_keypair(recipient_name) { + print(`✓ Selected recipient keypair "${recipient_name}" successfully`); + let recipient_pub_key = keypair_pub_key(); + + // Switch to sender keypair + if select_keypair(sender_name) { + print(`✓ Selected sender keypair "${sender_name}" successfully`); + + // Test encrypting a message with recipient's public key + print("\nTesting encrypting a message..."); + let message = "This is a secret message for the recipient"; + let ciphertext = encrypt_asymmetric(recipient_pub_key, message); + + // Switch back to recipient keypair to decrypt + if select_keypair(recipient_name) { + print(`✓ Switched back to recipient keypair "${recipient_name}" successfully`); + + // Test decrypting the message + print("Testing decrypting the message..."); + let decrypted = decrypt_asymmetric(ciphertext); + assert_true(decrypted == message, "Decrypted message should match original"); + print(`✓ Message decrypted successfully: "${decrypted}"`); + } + } + } + } + + print("--- Encryption and Decryption Tests completed successfully ---"); + passed += 1; + test_results["04_encryption_decryption"] = "PASSED"; +} catch(err) { + print(`!!! Error in Encryption and Decryption Tests: ${err}`); + failed += 1; + test_results["04_encryption_decryption"] = `FAILED: ${err}`; +} + +// Test 5: Error Handling +print("\n--- Running Error Handling Tests ---"); +try { + // Clear any existing session + clear_session(); + + // Test NoActiveSpace error + print("Testing NoActiveSpace error..."); + let no_active_space_error_caught = false; + try { + // Try to create a keypair without an active space + create_keypair("test_keypair", "password"); + } catch(err) { + no_active_space_error_caught = true; + print(`✓ Caught expected error: ${err}`); + } + assert_true(no_active_space_error_caught, "NoActiveSpace error should be caught"); + + // Create a key space for further tests + if create_key_space("error_test_space", "password") { + print(`✓ Key space created successfully`); + + // Test KeypairNotFound error + print("Testing KeypairNotFound error..."); + let keypair_not_found_error_caught = false; + try { + // Try to select a non-existent keypair + select_keypair("nonexistent_keypair"); + } catch(err) { + keypair_not_found_error_caught = true; + print(`✓ Caught expected error: ${err}`); + } + assert_true(keypair_not_found_error_caught, "KeypairNotFound error should be caught"); + + // Test NoKeypairSelected error + print("Testing NoKeypairSelected error..."); + let no_keypair_selected_error_caught = false; + try { + // Try to get the public key without selecting a keypair + keypair_pub_key(); + } catch(err) { + no_keypair_selected_error_caught = true; + print(`✓ Caught expected error: ${err}`); + } + assert_true(no_keypair_selected_error_caught, "NoKeypairSelected error should be caught"); + } + + print("--- Error Handling Tests completed successfully ---"); + passed += 1; + test_results["05_error_handling"] = "PASSED"; +} catch(err) { + print(`!!! Error in Error Handling Tests: ${err}`); + failed += 1; + test_results["05_error_handling"] = `FAILED: ${err}`; +} + +print("\n=== Test Summary ==="); +print(`Passed: ${passed}`); +print(`Failed: ${failed}`); +print(`Total: ${passed + failed}`); + +// Print detailed results +print("\n=== Detailed Test Results ==="); +for key in test_results.keys() { + let result = test_results[key]; + if result.starts_with("PASSED") { + print(`✓ ${key}: ${result}`); + } else { + print(`✗ ${key}: ${result}`); + } +} + +if failed == 0 { + print("\n✅ All tests passed!"); +} else { + print("\n❌ Some tests failed!"); +} + +// Return the number of failed tests (0 means success) +failed; \ No newline at end of file diff --git a/src/rhai_tests/nerdctl/01_container_operations.rhai b/rhai_tests/nerdctl/01_container_operations.rhai similarity index 100% rename from src/rhai_tests/nerdctl/01_container_operations.rhai rename to rhai_tests/nerdctl/01_container_operations.rhai diff --git a/src/rhai_tests/nerdctl/02_image_operations.rhai b/rhai_tests/nerdctl/02_image_operations.rhai similarity index 100% rename from src/rhai_tests/nerdctl/02_image_operations.rhai rename to rhai_tests/nerdctl/02_image_operations.rhai diff --git a/src/rhai_tests/nerdctl/03_container_builder.rhai b/rhai_tests/nerdctl/03_container_builder.rhai similarity index 100% rename from src/rhai_tests/nerdctl/03_container_builder.rhai rename to rhai_tests/nerdctl/03_container_builder.rhai diff --git a/src/rhai_tests/nerdctl/run_all_tests.rhai b/rhai_tests/nerdctl/run_all_tests.rhai similarity index 100% rename from src/rhai_tests/nerdctl/run_all_tests.rhai rename to rhai_tests/nerdctl/run_all_tests.rhai diff --git a/src/rhai_tests/os/01_file_operations.rhai b/rhai_tests/os/01_file_operations.rhai similarity index 100% rename from src/rhai_tests/os/01_file_operations.rhai rename to rhai_tests/os/01_file_operations.rhai diff --git a/src/rhai_tests/os/02_download_operations.rhai b/rhai_tests/os/02_download_operations.rhai similarity index 100% rename from src/rhai_tests/os/02_download_operations.rhai rename to rhai_tests/os/02_download_operations.rhai diff --git a/src/rhai_tests/os/03_package_operations.rhai b/rhai_tests/os/03_package_operations.rhai similarity index 100% rename from src/rhai_tests/os/03_package_operations.rhai rename to rhai_tests/os/03_package_operations.rhai diff --git a/src/rhai_tests/os/run_all_tests.rhai b/rhai_tests/os/run_all_tests.rhai similarity index 100% rename from src/rhai_tests/os/run_all_tests.rhai rename to rhai_tests/os/run_all_tests.rhai diff --git a/src/rhai_tests/postgresclient/01_postgres_connection.rhai b/rhai_tests/postgresclient/01_postgres_connection.rhai similarity index 100% rename from src/rhai_tests/postgresclient/01_postgres_connection.rhai rename to rhai_tests/postgresclient/01_postgres_connection.rhai diff --git a/src/rhai_tests/postgresclient/02_postgres_installer.rhai b/rhai_tests/postgresclient/02_postgres_installer.rhai similarity index 100% rename from src/rhai_tests/postgresclient/02_postgres_installer.rhai rename to rhai_tests/postgresclient/02_postgres_installer.rhai diff --git a/src/rhai_tests/postgresclient/02_postgres_installer_mock.rhai b/rhai_tests/postgresclient/02_postgres_installer_mock.rhai similarity index 100% rename from src/rhai_tests/postgresclient/02_postgres_installer_mock.rhai rename to rhai_tests/postgresclient/02_postgres_installer_mock.rhai diff --git a/src/rhai_tests/postgresclient/02_postgres_installer_simple.rhai b/rhai_tests/postgresclient/02_postgres_installer_simple.rhai similarity index 100% rename from src/rhai_tests/postgresclient/02_postgres_installer_simple.rhai rename to rhai_tests/postgresclient/02_postgres_installer_simple.rhai diff --git a/src/rhai_tests/postgresclient/example_installer.rhai b/rhai_tests/postgresclient/example_installer.rhai similarity index 100% rename from src/rhai_tests/postgresclient/example_installer.rhai rename to rhai_tests/postgresclient/example_installer.rhai diff --git a/src/rhai_tests/postgresclient/run_all_tests.rhai b/rhai_tests/postgresclient/run_all_tests.rhai similarity index 100% rename from src/rhai_tests/postgresclient/run_all_tests.rhai rename to rhai_tests/postgresclient/run_all_tests.rhai diff --git a/src/rhai_tests/postgresclient/test_functions.rhai b/rhai_tests/postgresclient/test_functions.rhai similarity index 100% rename from src/rhai_tests/postgresclient/test_functions.rhai rename to rhai_tests/postgresclient/test_functions.rhai diff --git a/src/rhai_tests/postgresclient/test_print.rhai b/rhai_tests/postgresclient/test_print.rhai similarity index 100% rename from src/rhai_tests/postgresclient/test_print.rhai rename to rhai_tests/postgresclient/test_print.rhai diff --git a/src/rhai_tests/postgresclient/test_simple.rhai b/rhai_tests/postgresclient/test_simple.rhai similarity index 100% rename from src/rhai_tests/postgresclient/test_simple.rhai rename to rhai_tests/postgresclient/test_simple.rhai diff --git a/src/rhai_tests/process/01_command_execution.rhai b/rhai_tests/process/01_command_execution.rhai similarity index 100% rename from src/rhai_tests/process/01_command_execution.rhai rename to rhai_tests/process/01_command_execution.rhai diff --git a/src/rhai_tests/process/02_process_management.rhai b/rhai_tests/process/02_process_management.rhai similarity index 100% rename from src/rhai_tests/process/02_process_management.rhai rename to rhai_tests/process/02_process_management.rhai diff --git a/src/rhai_tests/process/run_all_tests.rhai b/rhai_tests/process/run_all_tests.rhai similarity index 100% rename from src/rhai_tests/process/run_all_tests.rhai rename to rhai_tests/process/run_all_tests.rhai diff --git a/src/rhai_tests/redisclient/01_redis_connection.rhai b/rhai_tests/redisclient/01_redis_connection.rhai similarity index 100% rename from src/rhai_tests/redisclient/01_redis_connection.rhai rename to rhai_tests/redisclient/01_redis_connection.rhai diff --git a/src/rhai_tests/redisclient/02_redis_operations.rhai b/rhai_tests/redisclient/02_redis_operations.rhai similarity index 100% rename from src/rhai_tests/redisclient/02_redis_operations.rhai rename to rhai_tests/redisclient/02_redis_operations.rhai diff --git a/src/rhai_tests/redisclient/03_redis_authentication.rhai b/rhai_tests/redisclient/03_redis_authentication.rhai similarity index 100% rename from src/rhai_tests/redisclient/03_redis_authentication.rhai rename to rhai_tests/redisclient/03_redis_authentication.rhai diff --git a/src/rhai_tests/redisclient/run_all_tests.rhai b/rhai_tests/redisclient/run_all_tests.rhai similarity index 100% rename from src/rhai_tests/redisclient/run_all_tests.rhai rename to rhai_tests/redisclient/run_all_tests.rhai diff --git a/src/rhai_tests/rfs/01_mount_operations.rhai b/rhai_tests/rfs/01_mount_operations.rhai similarity index 100% rename from src/rhai_tests/rfs/01_mount_operations.rhai rename to rhai_tests/rfs/01_mount_operations.rhai diff --git a/src/rhai_tests/rfs/02_filesystem_layer_operations.rhai b/rhai_tests/rfs/02_filesystem_layer_operations.rhai similarity index 100% rename from src/rhai_tests/rfs/02_filesystem_layer_operations.rhai rename to rhai_tests/rfs/02_filesystem_layer_operations.rhai diff --git a/src/rhai_tests/rfs/run_all_tests.rhai b/rhai_tests/rfs/run_all_tests.rhai similarity index 100% rename from src/rhai_tests/rfs/run_all_tests.rhai rename to rhai_tests/rfs/run_all_tests.rhai diff --git a/src/rhai_tests/run_all_tests.sh b/rhai_tests/run_all_tests.sh similarity index 100% rename from src/rhai_tests/run_all_tests.sh rename to rhai_tests/run_all_tests.sh diff --git a/src/rhai_tests/text/01_text_indentation.rhai b/rhai_tests/text/01_text_indentation.rhai similarity index 100% rename from src/rhai_tests/text/01_text_indentation.rhai rename to rhai_tests/text/01_text_indentation.rhai diff --git a/src/rhai_tests/text/02_name_path_fix.rhai b/rhai_tests/text/02_name_path_fix.rhai similarity index 100% rename from src/rhai_tests/text/02_name_path_fix.rhai rename to rhai_tests/text/02_name_path_fix.rhai diff --git a/src/rhai_tests/text/03_text_replacer.rhai b/rhai_tests/text/03_text_replacer.rhai similarity index 100% rename from src/rhai_tests/text/03_text_replacer.rhai rename to rhai_tests/text/03_text_replacer.rhai diff --git a/src/rhai_tests/text/04_template_builder.rhai b/rhai_tests/text/04_template_builder.rhai similarity index 100% rename from src/rhai_tests/text/04_template_builder.rhai rename to rhai_tests/text/04_template_builder.rhai diff --git a/src/rhai_tests/text/run_all_tests.rhai b/rhai_tests/text/run_all_tests.rhai similarity index 100% rename from src/rhai_tests/text/run_all_tests.rhai rename to rhai_tests/text/run_all_tests.rhai diff --git a/run_rhai_tests.sh b/run_rhai_tests.sh index 2182cb5..4b7fb08 100755 --- a/run_rhai_tests.sh +++ b/run_rhai_tests.sh @@ -24,7 +24,7 @@ log "${BLUE} Running All Rhai Tests ${NC}" log "${BLUE}=======================================${NC}" # Find all test runner scripts -RUNNERS=$(find src/rhai_tests -name "run_all_tests.rhai") +RUNNERS=$(find rhai_tests -name "run_all_tests.rhai") # Initialize counters TOTAL_MODULES=0 diff --git a/src/docs/.gitignore b/src/docs/.gitignore deleted file mode 100644 index 77793ac..0000000 --- a/src/docs/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# Dependencies -/node_modules - -# Production -/build - -# Generated files -.docusaurus -.cache-loader - -# Misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* -bun.lockb -bun.lock - -yarn.lock - -build.sh -build_dev.sh -develop.sh - -docusaurus.config.ts - -sidebars.ts - -tsconfig.json diff --git a/src/lib.rs b/src/lib.rs index 2d390d7..bc8cbdf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,8 @@ pub mod redisclient; pub mod rhai; pub mod text; pub mod virt; -pub mod hero_vault; +pub mod vault; +pub mod zinit_client; // Version information /// Returns the version of the SAL library diff --git a/src/os/download.rs b/src/os/download.rs index c137d28..e0e084c 100644 --- a/src/os/download.rs +++ b/src/os/download.rs @@ -1,9 +1,9 @@ -use std::process::Command; -use std::path::Path; -use std::fs; -use std::fmt; use std::error::Error; +use std::fmt; +use std::fs; use std::io; +use std::path::Path; +use std::process::Command; // Define a custom error type for download operations #[derive(Debug)] @@ -26,11 +26,17 @@ pub enum DownloadError { impl fmt::Display for DownloadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - DownloadError::CreateDirectoryFailed(e) => write!(f, "Error creating directories: {}", e), + DownloadError::CreateDirectoryFailed(e) => { + write!(f, "Error creating directories: {}", e) + } DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e), DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url), DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e), - DownloadError::FileTooSmall(size, min) => write!(f, "Error: Downloaded file is too small ({}KB < {}KB)", size, min), + DownloadError::FileTooSmall(size, min) => write!( + f, + "Error: Downloaded file is too small ({}KB < {}KB)", + size, min + ), DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e), DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e), DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e), @@ -74,12 +80,18 @@ impl Error for DownloadError { * * # Examples * - * ``` - * // Download a file with no minimum size requirement - * let path = download("https://example.com/file.txt", "/tmp/", 0)?; + * ```no_run + * use sal::os::download; * - * // Download a file with minimum size requirement of 100KB - * let path = download("https://example.com/file.zip", "/tmp/", 100)?; + * fn main() -> Result<(), Box> { + * // Download a file with no minimum size requirement + * let path = download("https://example.com/file.txt", "/tmp/", 0)?; + * + * // Download a file with minimum size requirement of 100KB + * let path = download("https://example.com/file.zip", "/tmp/", 100)?; + * + * Ok(()) + * } * ``` * * # Notes @@ -91,30 +103,41 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result name, - None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string())) + None => { + return Err(DownloadError::InvalidUrl( + "cannot extract filename".to_string(), + )) + } }; - + // Create a full path for the downloaded file let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename); - + // Create a temporary path for downloading let temp_path = format!("{}.download", file_path); - + // Use curl to download the file with progress bar println!("Downloading {} to {}", url, file_path); let output = Command::new("curl") - .args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) + .args(&[ + "--progress-bar", + "--location", + "--fail", + "--output", + &temp_path, + url, + ]) .status() .map_err(DownloadError::CurlExecutionFailed)?; - + if !output.success() { return Err(DownloadError::DownloadFailed(url.to_string())); } - + // Show file size after download match fs::metadata(&temp_path) { Ok(metadata) => { @@ -122,14 +145,20 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result 1 { - println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0)); + println!( + "Download complete! File size: {:.2} MB", + size_bytes as f64 / (1024.0 * 1024.0) + ); } else { - println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0); + println!( + "Download complete! File size: {:.2} KB", + size_bytes as f64 / 1024.0 + ); } - }, + } Err(_) => println!("Download complete!"), } - + // Check file size if minimum size is specified if min_size_kb > 0 { let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; @@ -139,57 +168,59 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result { if !status.success() { - return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string())); + return Err(DownloadError::ExtractionFailed( + "Error extracting archive".to_string(), + )); } - }, + } Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), } - + // Show number of extracted files match fs::read_dir(dest) { Ok(entries) => { let count = entries.count(); println!("Extraction complete! Extracted {} files/directories", count); - }, + } Err(_) => println!("Extraction complete!"), } - + // Remove the temporary file fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?; - + Ok(dest.to_string()) } else { // Just rename the temporary file to the final destination fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; - + Ok(file_path) } } @@ -210,12 +241,18 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result Result<(), Box> { + * // Download a file with no minimum size requirement + * let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?; + * + * // Download a file with minimum size requirement of 100KB + * let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?; + * + * Ok(()) + * } * ``` */ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result { @@ -224,21 +261,28 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result { @@ -246,14 +290,20 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result 1 { - println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0)); + println!( + "Download complete! File size: {:.2} MB", + size_bytes as f64 / (1024.0 * 1024.0) + ); } else { - println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0); + println!( + "Download complete! File size: {:.2} KB", + size_bytes as f64 / 1024.0 + ); } - }, + } Err(_) => println!("Download complete!"), } - + // Check file size if minimum size is specified if min_size_kb > 0 { let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; @@ -263,10 +313,10 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result Result Result<(), Box> { + * // Make a file executable + * chmod_exec("/path/to/file")?; + * Ok(()) + * } * ``` */ pub fn chmod_exec(path: &str) -> Result { let path_obj = Path::new(path); - + // Check if the path exists and is a file if !path_obj.exists() { - return Err(DownloadError::NotAFile(format!("Path does not exist: {}", path))); + return Err(DownloadError::NotAFile(format!( + "Path does not exist: {}", + path + ))); } - + if !path_obj.is_file() { - return Err(DownloadError::NotAFile(format!("Path is not a file: {}", path))); + return Err(DownloadError::NotAFile(format!( + "Path is not a file: {}", + path + ))); } - + // Get current permissions let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?; let mut permissions = metadata.permissions(); - + // Set executable bit for user, group, and others #[cfg(unix)] { @@ -314,47 +375,55 @@ pub fn chmod_exec(path: &str) -> Result { let new_mode = mode | 0o111; permissions.set_mode(new_mode); } - + #[cfg(not(unix))] { // On non-Unix platforms, we can't set executable bit directly // Just return success with a warning - return Ok(format!("Made {} executable (note: non-Unix platform, may not be fully supported)", path)); + return Ok(format!( + "Made {} executable (note: non-Unix platform, may not be fully supported)", + path + )); } - + // Apply the new permissions - fs::set_permissions(path, permissions).map_err(|e| + fs::set_permissions(path, permissions).map_err(|e| { DownloadError::CommandExecutionFailed(io::Error::new( io::ErrorKind::Other, - format!("Failed to set executable permissions: {}", e) + format!("Failed to set executable permissions: {}", e), )) - )?; - + })?; + Ok(format!("Made {} executable", path)) } /** * Download a file and install it if it's a supported package format. - * + * * # Arguments - * + * * * `url` - The URL to download from * * `min_size_kb` - Minimum required file size in KB (0 for no minimum) - * + * * # Returns - * + * * * `Ok(String)` - The path where the file was saved or extracted * * `Err(DownloadError)` - An error if the download or installation failed - * + * * # Examples - * + * + * ```no_run + * use sal::os::download_install; + * + * fn main() -> Result<(), Box> { + * // Download and install a .deb package + * let result = download_install("https://example.com/package.deb", 100)?; + * Ok(()) + * } * ``` - * // Download and install a .deb package - * let result = download_install("https://example.com/package.deb", 100)?; - * ``` - * + * * # Notes - * + * * Currently only supports .deb packages on Debian-based systems. * For other file types, it behaves the same as the download function. */ @@ -362,19 +431,23 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result name, - None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string())) + None => { + return Err(DownloadError::InvalidUrl( + "cannot extract filename".to_string(), + )) + } }; - + // Create a proper destination path let dest_path = format!("/tmp/{}", filename); // Check if it's a compressed file that needs extraction let lower_url = url.to_lowercase(); - let is_archive = lower_url.ends_with(".tar.gz") || - lower_url.ends_with(".tgz") || - lower_url.ends_with(".tar") || - lower_url.ends_with(".zip"); - + let is_archive = lower_url.ends_with(".tar.gz") + || lower_url.ends_with(".tgz") + || lower_url.ends_with(".tar") + || lower_url.ends_with(".zip"); + let download_result = if is_archive { // For archives, use the directory-based download function download(url, "/tmp", min_size_kb)? @@ -382,13 +455,13 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result Result /dev/null && command -v apt > /dev/null || test -f /etc/debian_version") .status(); - + match platform_check { Ok(status) => { if !status.success() { return Err(DownloadError::PlatformNotSupported( - "Cannot install .deb package: not on a Debian-based system".to_string() + "Cannot install .deb package: not on a Debian-based system".to_string(), )); } - }, - Err(_) => return Err(DownloadError::PlatformNotSupported( - "Failed to check system compatibility for .deb installation".to_string() - )), + } + Err(_) => { + return Err(DownloadError::PlatformNotSupported( + "Failed to check system compatibility for .deb installation".to_string(), + )) + } } - + // Install the .deb package non-interactively println!("Installing package: {}", dest_path); let install_result = Command::new("sudo") .args(&["dpkg", "--install", &dest_path]) .status(); - + match install_result { Ok(status) => { if !status.success() { @@ -424,24 +499,24 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result return Err(DownloadError::CommandExecutionFailed(e)), } } - + Ok(download_result) } diff --git a/src/os/fs.rs b/src/os/fs.rs index 30d76c6..3b3a50a 100644 --- a/src/os/fs.rs +++ b/src/os/fs.rs @@ -1,9 +1,9 @@ +use std::error::Error; +use std::fmt; use std::fs; +use std::io; use std::path::Path; use std::process::Command; -use std::fmt; -use std::error::Error; -use std::io; // Define a custom error type for file system operations #[derive(Debug)] @@ -33,14 +33,18 @@ impl fmt::Display for FsError { match self { FsError::DirectoryNotFound(dir) => write!(f, "Directory '{}' does not exist", dir), FsError::FileNotFound(pattern) => write!(f, "No files found matching '{}'", pattern), - FsError::CreateDirectoryFailed(e) => write!(f, "Failed to create parent directories: {}", e), + FsError::CreateDirectoryFailed(e) => { + write!(f, "Failed to create parent directories: {}", e) + } FsError::CopyFailed(e) => write!(f, "Failed to copy file: {}", e), FsError::DeleteFailed(e) => write!(f, "Failed to delete: {}", e), FsError::CommandFailed(e) => write!(f, "{}", e), FsError::CommandNotFound(e) => write!(f, "Command not found: {}", e), FsError::CommandExecutionError(e) => write!(f, "Failed to execute command: {}", e), FsError::InvalidGlobPattern(e) => write!(f, "Invalid glob pattern: {}", e), - FsError::NotADirectory(path) => write!(f, "Path '{}' exists but is not a directory", path), + FsError::NotADirectory(path) => { + write!(f, "Path '{}' exists but is not a directory", path) + } FsError::NotAFile(path) => write!(f, "Path '{}' is not a regular file", path), FsError::UnknownFileType(path) => write!(f, "Unknown file type at '{}'", path), FsError::MetadataError(e) => write!(f, "Failed to get file metadata: {}", e), @@ -73,54 +77,58 @@ impl Error for FsError { /** * Recursively copy a file or directory from source to destination. - * + * * # Arguments - * + * * * `src` - The source path, which can include wildcards * * `dest` - The destination path - * + * * # Returns - * + * * * `Ok(String)` - A success message indicating what was copied * * `Err(FsError)` - An error if the copy operation failed - * + * * # Examples - * - * ``` - * // Copy a single file - * let result = copy("file.txt", "backup/file.txt")?; - * - * // Copy multiple files using wildcards - * let result = copy("*.txt", "backup/")?; - * - * // Copy a directory recursively - * let result = copy("src_dir", "dest_dir")?; + * + * ```no_run + * use sal::os::copy; + * + * fn main() -> Result<(), Box> { + * // Copy a single file + * let result = copy("file.txt", "backup/file.txt")?; + * + * // Copy multiple files using wildcards + * let result = copy("*.txt", "backup/")?; + * + * // Copy a directory recursively + * let result = copy("src_dir", "dest_dir")?; + * + * Ok(()) + * } * ``` */ pub fn copy(src: &str, dest: &str) -> Result { let dest_path = Path::new(dest); - + // Check if source path contains wildcards if src.contains('*') || src.contains('?') || src.contains('[') { // Create parent directories for destination if needed if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } - + // Use glob to expand wildcards let entries = glob::glob(src).map_err(FsError::InvalidGlobPattern)?; - - let paths: Vec<_> = entries - .filter_map(Result::ok) - .collect(); - + + let paths: Vec<_> = entries.filter_map(Result::ok).collect(); + if paths.is_empty() { return Err(FsError::FileNotFound(src.to_string())); } - + let mut success_count = 0; let dest_is_dir = dest_path.exists() && dest_path.is_dir(); - + for path in paths { let target_path = if dest_is_dir { // If destination is a directory, copy the file into it @@ -138,7 +146,7 @@ pub fn copy(src: &str, dest: &str) -> Result { // Otherwise use the destination as is (only makes sense for single file) dest_path.to_path_buf() }; - + if path.is_file() { // Copy file if let Err(e) = fs::copy(&path, &target_path) { @@ -150,49 +158,65 @@ pub fn copy(src: &str, dest: &str) -> Result { // For directories, use platform-specific command #[cfg(target_os = "windows")] let output = Command::new("xcopy") - .args(&["/E", "/I", "/H", "/Y", - &path.to_string_lossy(), - &target_path.to_string_lossy()]) + .args(&[ + "/E", + "/I", + "/H", + "/Y", + &path.to_string_lossy(), + &target_path.to_string_lossy(), + ]) .status(); - + #[cfg(not(target_os = "windows"))] let output = Command::new("cp") - .args(&["-R", - &path.to_string_lossy(), - &target_path.to_string_lossy()]) + .args(&[ + "-R", + &path.to_string_lossy(), + &target_path.to_string_lossy(), + ]) .status(); - + match output { Ok(status) => { if status.success() { success_count += 1; } - }, - Err(e) => println!("Warning: Failed to copy directory {}: {}", path.display(), e), + } + Err(e) => println!( + "Warning: Failed to copy directory {}: {}", + path.display(), + e + ), } } } - + if success_count > 0 { - Ok(format!("Successfully copied {} items from '{}' to '{}'", - success_count, src, dest)) + Ok(format!( + "Successfully copied {} items from '{}' to '{}'", + success_count, src, dest + )) } else { - Err(FsError::CommandFailed(format!("Failed to copy any files from '{}' to '{}'", src, dest))) + Err(FsError::CommandFailed(format!( + "Failed to copy any files from '{}' to '{}'", + src, dest + ))) } } else { // Handle non-wildcard paths normally let src_path = Path::new(src); - + // Check if source exists if !src_path.exists() { return Err(FsError::FileNotFound(src.to_string())); } - + // Create parent directories if they don't exist if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } - + // Copy based on source type if src_path.is_file() { // If destination is a directory, copy the file into it @@ -200,7 +224,12 @@ pub fn copy(src: &str, dest: &str) -> Result { let file_name = src_path.file_name().unwrap_or_default(); let new_dest_path = dest_path.join(file_name); fs::copy(src_path, new_dest_path).map_err(FsError::CopyFailed)?; - Ok(format!("Successfully copied file '{}' to '{}/{}'", src, dest, file_name.to_string_lossy())) + Ok(format!( + "Successfully copied file '{}' to '{}/{}'", + src, + dest, + file_name.to_string_lossy() + )) } else { // Otherwise copy file to the specified destination fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?; @@ -212,21 +241,25 @@ pub fn copy(src: &str, dest: &str) -> Result { let output = Command::new("xcopy") .args(&["/E", "/I", "/H", "/Y", src, dest]) .output(); - + #[cfg(not(target_os = "windows"))] - let output = Command::new("cp") - .args(&["-R", src, dest]) - .output(); - + let output = Command::new("cp").args(&["-R", src, dest]).output(); + match output { Ok(out) => { if out.status.success() { - Ok(format!("Successfully copied directory '{}' to '{}'", src, dest)) + Ok(format!( + "Successfully copied directory '{}' to '{}'", + src, dest + )) } else { let error = String::from_utf8_lossy(&out.stderr); - Err(FsError::CommandFailed(format!("Failed to copy directory: {}", error))) + Err(FsError::CommandFailed(format!( + "Failed to copy directory: {}", + error + ))) } - }, + } Err(e) => Err(FsError::CommandExecutionError(e)), } } else { @@ -237,18 +270,20 @@ pub fn copy(src: &str, dest: &str) -> Result { /** * Check if a file or directory exists. - * + * * # Arguments - * + * * * `path` - The path to check - * + * * # Returns - * + * * * `bool` - True if the path exists, false otherwise - * + * * # Examples - * + * * ``` + * use sal::os::exist; + * * if exist("file.txt") { * println!("File exists"); * } @@ -260,48 +295,56 @@ pub fn exist(path: &str) -> bool { /** * Find a file in a directory (with support for wildcards). - * + * * # Arguments - * + * * * `dir` - The directory to search in * * `filename` - The filename pattern to search for (can include wildcards) - * + * * # Returns - * + * * * `Ok(String)` - The path to the found file * * `Err(FsError)` - An error if no file is found or multiple files are found - * + * * # Examples - * - * ``` - * let file_path = find_file("/path/to/dir", "*.txt")?; - * println!("Found file: {}", file_path); + * + * ```no_run + * use sal::os::find_file; + * + * fn main() -> Result<(), Box> { + * let file_path = find_file("/path/to/dir", "*.txt")?; + * println!("Found file: {}", file_path); + * Ok(()) + * } * ``` */ pub fn find_file(dir: &str, filename: &str) -> Result { let dir_path = Path::new(dir); - + // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } - + // Use glob to find files - use recursive pattern to find in subdirectories too let pattern = format!("{}/**/{}", dir, filename); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; - + let files: Vec<_> = entries .filter_map(Result::ok) .filter(|path| path.is_file()) .collect(); - + match files.len() { 0 => Err(FsError::FileNotFound(filename.to_string())), 1 => Ok(files[0].to_string_lossy().to_string()), _ => { // If multiple matches, just return the first one instead of erroring // This makes wildcard searches more practical - println!("Note: Multiple files found matching '{}', returning first match", filename); + println!( + "Note: Multiple files found matching '{}', returning first match", + filename + ); Ok(files[0].to_string_lossy().to_string()) } } @@ -309,164 +352,188 @@ pub fn find_file(dir: &str, filename: &str) -> Result { /** * Find multiple files in a directory (recursive, with support for wildcards). - * + * * # Arguments - * + * * * `dir` - The directory to search in * * `filename` - The filename pattern to search for (can include wildcards) - * + * * # Returns - * + * * * `Ok(Vec)` - A vector of paths to the found files * * `Err(FsError)` - An error if the directory doesn't exist or the pattern is invalid - * + * * # Examples - * - * ``` - * let files = find_files("/path/to/dir", "*.txt")?; - * for file in files { - * println!("Found file: {}", file); + * + * ```no_run + * use sal::os::find_files; + * + * fn main() -> Result<(), Box> { + * let files = find_files("/path/to/dir", "*.txt")?; + * for file in files { + * println!("Found file: {}", file); + * } + * Ok(()) * } * ``` */ pub fn find_files(dir: &str, filename: &str) -> Result, FsError> { let dir_path = Path::new(dir); - + // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } - + // Use glob to find files let pattern = format!("{}/**/{}", dir, filename); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; - + let files: Vec = entries .filter_map(Result::ok) .filter(|path| path.is_file()) .map(|path| path.to_string_lossy().to_string()) .collect(); - + Ok(files) } /** * Find a directory in a parent directory (with support for wildcards). - * + * * # Arguments - * + * * * `dir` - The parent directory to search in * * `dirname` - The directory name pattern to search for (can include wildcards) - * + * * # Returns - * + * * * `Ok(String)` - The path to the found directory * * `Err(FsError)` - An error if no directory is found or multiple directories are found - * + * * # Examples - * - * ``` - * let dir_path = find_dir("/path/to/parent", "sub*")?; - * println!("Found directory: {}", dir_path); + * + * ```no_run + * use sal::os::find_dir; + * + * fn main() -> Result<(), Box> { + * let dir_path = find_dir("/path/to/parent", "sub*")?; + * println!("Found directory: {}", dir_path); + * Ok(()) + * } * ``` */ pub fn find_dir(dir: &str, dirname: &str) -> Result { let dir_path = Path::new(dir); - + // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } - + // Use glob to find directories let pattern = format!("{}/{}", dir, dirname); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; - + let dirs: Vec<_> = entries .filter_map(Result::ok) .filter(|path| path.is_dir()) .collect(); - + match dirs.len() { 0 => Err(FsError::DirectoryNotFound(dirname.to_string())), 1 => Ok(dirs[0].to_string_lossy().to_string()), - _ => Err(FsError::CommandFailed(format!("Multiple directories found matching '{}', expected only one", dirname))), + _ => Err(FsError::CommandFailed(format!( + "Multiple directories found matching '{}', expected only one", + dirname + ))), } } /** * Find multiple directories in a parent directory (recursive, with support for wildcards). - * + * * # Arguments - * + * * * `dir` - The parent directory to search in * * `dirname` - The directory name pattern to search for (can include wildcards) - * + * * # Returns - * + * * * `Ok(Vec)` - A vector of paths to the found directories * * `Err(FsError)` - An error if the parent directory doesn't exist or the pattern is invalid - * + * * # Examples - * - * ``` - * let dirs = find_dirs("/path/to/parent", "sub*")?; - * for dir in dirs { - * println!("Found directory: {}", dir); + * + * ```no_run + * use sal::os::find_dirs; + * + * fn main() -> Result<(), Box> { + * let dirs = find_dirs("/path/to/parent", "sub*")?; + * for dir in dirs { + * println!("Found directory: {}", dir); + * } + * Ok(()) * } * ``` */ pub fn find_dirs(dir: &str, dirname: &str) -> Result, FsError> { let dir_path = Path::new(dir); - + // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(FsError::DirectoryNotFound(dir.to_string())); } - + // Use glob to find directories let pattern = format!("{}/**/{}", dir, dirname); let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; - + let dirs: Vec = entries .filter_map(Result::ok) .filter(|path| path.is_dir()) .map(|path| path.to_string_lossy().to_string()) .collect(); - + Ok(dirs) } /** * Delete a file or directory (defensive - doesn't error if file doesn't exist). - * + * * # Arguments - * + * * * `path` - The path to delete - * + * * # Returns - * + * * * `Ok(String)` - A success message indicating what was deleted * * `Err(FsError)` - An error if the deletion failed - * + * * # Examples - * + * * ``` - * // Delete a file - * let result = delete("file.txt")?; - * - * // Delete a directory and all its contents - * let result = delete("directory/")?; + * use sal::os::delete; + * + * fn main() -> Result<(), Box> { + * // Delete a file + * let result = delete("file.txt")?; + * + * // Delete a directory and all its contents + * let result = delete("directory/")?; + * + * Ok(()) + * } * ``` */ pub fn delete(path: &str) -> Result { let path_obj = Path::new(path); - + // Check if path exists if !path_obj.exists() { return Ok(format!("Nothing to delete at '{}'", path)); } - + // Delete based on path type if path_obj.is_file() || path_obj.is_symlink() { fs::remove_file(path_obj).map_err(FsError::DeleteFailed)?; @@ -481,26 +548,31 @@ pub fn delete(path: &str) -> Result { /** * Create a directory and all parent directories (defensive - doesn't error if directory exists). - * + * * # Arguments - * + * * * `path` - The path of the directory to create - * + * * # Returns - * + * * * `Ok(String)` - A success message indicating the directory was created * * `Err(FsError)` - An error if the creation failed - * + * * # Examples - * + * * ``` - * let result = mkdir("path/to/new/directory")?; - * println!("{}", result); + * use sal::os::mkdir; + * + * fn main() -> Result<(), Box> { + * let result = mkdir("path/to/new/directory")?; + * println!("{}", result); + * Ok(()) + * } * ``` */ pub fn mkdir(path: &str) -> Result { let path_obj = Path::new(path); - + // Check if path already exists if path_obj.exists() { if path_obj.is_dir() { @@ -509,7 +581,7 @@ pub fn mkdir(path: &str) -> Result { return Err(FsError::NotADirectory(path.to_string())); } } - + // Create directory and parents fs::create_dir_all(path_obj).map_err(FsError::CreateDirectoryFailed)?; Ok(format!("Successfully created directory '{}'", path)) @@ -517,36 +589,41 @@ pub fn mkdir(path: &str) -> Result { /** * Get the size of a file in bytes. - * + * * # Arguments - * + * * * `path` - The path of the file - * + * * # Returns - * + * * * `Ok(i64)` - The size of the file in bytes * * `Err(FsError)` - An error if the file doesn't exist or isn't a regular file - * + * * # Examples - * - * ``` - * let size = file_size("file.txt")?; - * println!("File size: {} bytes", size); + * + * ```no_run + * use sal::os::file_size; + * + * fn main() -> Result<(), Box> { + * let size = file_size("file.txt")?; + * println!("File size: {} bytes", size); + * Ok(()) + * } * ``` */ pub fn file_size(path: &str) -> Result { let path_obj = Path::new(path); - + // Check if file exists if !path_obj.exists() { return Err(FsError::FileNotFound(path.to_string())); } - + // Check if it's a regular file if !path_obj.is_file() { return Err(FsError::NotAFile(path.to_string())); } - + // Get file metadata let metadata = fs::metadata(path_obj).map_err(FsError::MetadataError)?; Ok(metadata.len() as i64) @@ -554,58 +631,67 @@ pub fn file_size(path: &str) -> Result { /** * Sync directories using rsync (or platform equivalent). - * + * * # Arguments - * + * * * `src` - The source directory * * `dest` - The destination directory - * + * * # Returns - * + * * * `Ok(String)` - A success message indicating the directories were synced * * `Err(FsError)` - An error if the sync failed - * + * * # Examples - * - * ``` - * let result = rsync("source_dir/", "backup_dir/")?; - * println!("{}", result); + * + * ```no_run + * use sal::os::rsync; + * + * fn main() -> Result<(), Box> { + * let result = rsync("source_dir/", "backup_dir/")?; + * println!("{}", result); + * Ok(()) + * } * ``` */ pub fn rsync(src: &str, dest: &str) -> Result { let src_path = Path::new(src); let dest_path = Path::new(dest); - + // Check if source exists if !src_path.exists() { return Err(FsError::FileNotFound(src.to_string())); } - + // Create parent directories if they don't exist if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } - + // Use platform-specific command for syncing #[cfg(target_os = "windows")] let output = Command::new("robocopy") .args(&[src, dest, "/MIR", "/NFL", "/NDL"]) .output(); - + #[cfg(any(target_os = "macos", target_os = "linux"))] let output = Command::new("rsync") .args(&["-a", "--delete", src, dest]) .output(); - + match output { Ok(out) => { - if out.status.success() || out.status.code() == Some(1) { // rsync and robocopy return 1 for some non-error cases + if out.status.success() || out.status.code() == Some(1) { + // rsync and robocopy return 1 for some non-error cases Ok(format!("Successfully synced '{}' to '{}'", src, dest)) } else { let error = String::from_utf8_lossy(&out.stderr); - Err(FsError::CommandFailed(format!("Failed to sync directories: {}", error))) + Err(FsError::CommandFailed(format!( + "Failed to sync directories: {}", + error + ))) } - }, + } Err(e) => Err(FsError::CommandExecutionError(e)), } } @@ -624,27 +710,32 @@ pub fn rsync(src: &str, dest: &str) -> Result { * * # Examples * - * ``` - * let result = chdir("/path/to/directory")?; - * println!("{}", result); + * ```no_run + * use sal::os::chdir; + * + * fn main() -> Result<(), Box> { + * let result = chdir("/path/to/directory")?; + * println!("{}", result); + * Ok(()) + * } * ``` */ pub fn chdir(path: &str) -> Result { let path_obj = Path::new(path); - + // Check if directory exists if !path_obj.exists() { return Err(FsError::DirectoryNotFound(path.to_string())); } - + // Check if it's a directory if !path_obj.is_dir() { return Err(FsError::NotADirectory(path.to_string())); } - + // Change directory std::env::set_current_dir(path_obj).map_err(FsError::ChangeDirFailed)?; - + Ok(format!("Successfully changed directory to '{}'", path)) } @@ -662,24 +753,29 @@ pub fn chdir(path: &str) -> Result { * * # Examples * - * ``` - * let content = file_read("file.txt")?; - * println!("File content: {}", content); + * ```no_run + * use sal::os::file_read; + * + * fn main() -> Result<(), Box> { + * let content = file_read("file.txt")?; + * println!("File content: {}", content); + * Ok(()) + * } * ``` */ pub fn file_read(path: &str) -> Result { let path_obj = Path::new(path); - + // Check if file exists if !path_obj.exists() { return Err(FsError::FileNotFound(path.to_string())); } - + // Check if it's a regular file if !path_obj.is_file() { return Err(FsError::NotAFile(path.to_string())); } - + // Read file content fs::read_to_string(path_obj).map_err(FsError::ReadFailed) } @@ -700,21 +796,26 @@ pub fn file_read(path: &str) -> Result { * # Examples * * ``` - * let result = file_write("file.txt", "Hello, world!")?; - * println!("{}", result); + * use sal::os::file_write; + * + * fn main() -> Result<(), Box> { + * let result = file_write("file.txt", "Hello, world!")?; + * println!("{}", result); + * Ok(()) + * } * ``` */ pub fn file_write(path: &str, content: &str) -> Result { let path_obj = Path::new(path); - + // Create parent directories if they don't exist if let Some(parent) = path_obj.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } - + // Write content to file fs::write(path_obj, content).map_err(FsError::WriteFailed)?; - + Ok(format!("Successfully wrote to file '{}'", path)) } @@ -734,29 +835,35 @@ pub fn file_write(path: &str, content: &str) -> Result { * # Examples * * ``` - * let result = file_write_append("log.txt", "New log entry\n")?; - * println!("{}", result); + * use sal::os::file_write_append; + * + * fn main() -> Result<(), Box> { + * let result = file_write_append("log.txt", "New log entry\n")?; + * println!("{}", result); + * Ok(()) + * } * ``` */ pub fn file_write_append(path: &str, content: &str) -> Result { let path_obj = Path::new(path); - + // Create parent directories if they don't exist if let Some(parent) = path_obj.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } - + // Open file in append mode (or create if it doesn't exist) let mut file = fs::OpenOptions::new() .create(true) .append(true) .open(path_obj) .map_err(FsError::AppendFailed)?; - + // Append content to file use std::io::Write; - file.write_all(content.as_bytes()).map_err(FsError::AppendFailed)?; - + file.write_all(content.as_bytes()) + .map_err(FsError::AppendFailed)?; + Ok(format!("Successfully appended to file '{}'", path)) } @@ -775,31 +882,37 @@ pub fn file_write_append(path: &str, content: &str) -> Result { * * # Examples * - * ``` - * // Move a file - * let result = mv("file.txt", "new_location/file.txt")?; + * ```no_run + * use sal::os::mv; * - * // Move a directory - * let result = mv("src_dir", "dest_dir")?; + * fn main() -> Result<(), Box> { + * // Move a file + * let result = mv("file.txt", "new_location/file.txt")?; * - * // Rename a file - * let result = mv("old_name.txt", "new_name.txt")?; + * // Move a directory + * let result = mv("src_dir", "dest_dir")?; + * + * // Rename a file + * let result = mv("old_name.txt", "new_name.txt")?; + * + * Ok(()) + * } * ``` */ pub fn mv(src: &str, dest: &str) -> Result { let src_path = Path::new(src); let dest_path = Path::new(dest); - + // Check if source exists if !src_path.exists() { return Err(FsError::FileNotFound(src.to_string())); } - + // Create parent directories if they don't exist if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; } - + // Handle the case where destination is a directory and exists let final_dest_path = if dest_path.exists() && dest_path.is_dir() && src_path.is_file() { // If destination is a directory and source is a file, move the file into the directory @@ -808,10 +921,10 @@ pub fn mv(src: &str, dest: &str) -> Result { } else { dest_path.to_path_buf() }; - + // Clone the path for use in the error handler let final_dest_path_clone = final_dest_path.clone(); - + // Perform the move operation fs::rename(src_path, &final_dest_path).map_err(|e| { // If rename fails (possibly due to cross-device link), try copy and delete @@ -826,7 +939,7 @@ pub fn mv(src: &str, dest: &str) -> Result { return FsError::DeleteFailed(del_err); } return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message - }, + } Err(copy_err) => return FsError::CopyFailed(copy_err), } } else if src_path.is_dir() { @@ -835,12 +948,10 @@ pub fn mv(src: &str, dest: &str) -> Result { let output = Command::new("xcopy") .args(&["/E", "/I", "/H", "/Y", src, dest]) .status(); - + #[cfg(not(target_os = "windows"))] - let output = Command::new("cp") - .args(&["-R", src, dest]) - .status(); - + let output = Command::new("cp").args(&["-R", src, dest]).status(); + match output { Ok(status) => { if status.success() { @@ -850,21 +961,26 @@ pub fn mv(src: &str, dest: &str) -> Result { } return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message } else { - return FsError::CommandFailed("Failed to copy directory for move operation".to_string()); + return FsError::CommandFailed( + "Failed to copy directory for move operation".to_string(), + ); } - }, + } Err(cmd_err) => return FsError::CommandExecutionError(cmd_err), } } } FsError::CommandFailed(format!("Failed to move '{}' to '{}': {}", src, dest, e)) })?; - + // If we get here, either the rename was successful or our copy-delete hack worked if src_path.is_file() { Ok(format!("Successfully moved file '{}' to '{}'", src, dest)) } else { - Ok(format!("Successfully moved directory '{}' to '{}'", src, dest)) + Ok(format!( + "Successfully moved directory '{}' to '{}'", + src, dest + )) } } @@ -882,6 +998,8 @@ pub fn mv(src: &str, dest: &str) -> Result { * # Examples * * ``` + * use sal::os::which; + * * let cmd_path = which("ls"); * if cmd_path != "" { * println!("ls is available at: {}", cmd_path); @@ -891,15 +1009,11 @@ pub fn mv(src: &str, dest: &str) -> Result { pub fn which(command: &str) -> String { // Use the appropriate command based on the platform #[cfg(target_os = "windows")] - let output = Command::new("where") - .arg(command) - .output(); - + let output = Command::new("where").arg(command).output(); + #[cfg(not(target_os = "windows"))] - let output = Command::new("which") - .arg(command) - .output(); - + let output = Command::new("which").arg(command).output(); + match output { Ok(out) => { if out.status.success() { @@ -908,7 +1022,7 @@ pub fn which(command: &str) -> String { } else { String::new() } - }, + } Err(_) => String::new(), } } @@ -929,26 +1043,35 @@ pub fn which(command: &str) -> String { * # Examples * * ``` - * // Check if a single command exists - * let result = cmd_ensure_exists("nerdctl")?; + * use sal::os::cmd_ensure_exists; * - * // Check if multiple commands exist - * let result = cmd_ensure_exists("nerdctl,docker,containerd")?; + * fn main() -> Result<(), Box> { + * // Check if a single command exists + * let result = cmd_ensure_exists("nerdctl")?; + * + * // Check if multiple commands exist + * let result = cmd_ensure_exists("nerdctl,docker,containerd")?; + * + * Ok(()) + * } * ``` */ pub fn cmd_ensure_exists(commands: &str) -> Result { // Split the input by commas to handle multiple commands - let command_list: Vec<&str> = commands.split(',') + let command_list: Vec<&str> = commands + .split(',') .map(|s| s.trim()) .filter(|s| !s.is_empty()) .collect(); - + if command_list.is_empty() { - return Err(FsError::CommandFailed("No commands specified to check".to_string())); + return Err(FsError::CommandFailed( + "No commands specified to check".to_string(), + )); } - + let mut missing_commands = Vec::new(); - + // Check each command for cmd in &command_list { let cmd_path = which(cmd); @@ -956,12 +1079,12 @@ pub fn cmd_ensure_exists(commands: &str) -> Result { missing_commands.push(cmd.to_string()); } } - + // If any commands are missing, return an error if !missing_commands.is_empty() { return Err(FsError::CommandNotFound(missing_commands.join(", "))); } - + // All commands exist if command_list.len() == 1 { Ok(format!("Command '{}' exists", command_list[0])) diff --git a/src/postgresclient/postgresclient.rs b/src/postgresclient/postgresclient.rs index b2e4baa..d711dfd 100644 --- a/src/postgresclient/postgresclient.rs +++ b/src/postgresclient/postgresclient.rs @@ -794,7 +794,7 @@ pub fn query_opt_with_pool_params( /// This function sends a notification on the specified channel with the specified payload. /// /// Example: -/// ``` +/// ```no_run /// use sal::postgresclient::notify; /// /// notify("my_channel", "Hello, world!").expect("Failed to send notification"); @@ -810,7 +810,7 @@ pub fn notify(channel: &str, payload: &str) -> Result<(), PostgresError> { /// This function sends a notification on the specified channel with the specified payload using the connection pool. /// /// Example: -/// ``` +/// ```no_run /// use sal::postgresclient::notify_with_pool; /// /// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification"); diff --git a/src/process/mgmt.rs b/src/process/mgmt.rs index 3daabcf..a4e7a9e 100644 --- a/src/process/mgmt.rs +++ b/src/process/mgmt.rs @@ -1,10 +1,10 @@ -use std::process::Command; -use std::fmt; use std::error::Error; +use std::fmt; use std::io; +use std::process::Command; /// Error type for process management operations -/// +/// /// This enum represents various errors that can occur during process management /// operations such as listing, finding, or killing processes. #[derive(Debug)] @@ -23,11 +23,18 @@ pub enum ProcessError { impl fmt::Display for ProcessError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ProcessError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e), + ProcessError::CommandExecutionFailed(e) => { + write!(f, "Failed to execute command: {}", e) + } ProcessError::CommandFailed(e) => write!(f, "{}", e), - ProcessError::NoProcessFound(pattern) => write!(f, "No processes found matching '{}'", pattern), - ProcessError::MultipleProcessesFound(pattern, count) => - write!(f, "Multiple processes ({}) found matching '{}'", count, pattern), + ProcessError::NoProcessFound(pattern) => { + write!(f, "No processes found matching '{}'", pattern) + } + ProcessError::MultipleProcessesFound(pattern, count) => write!( + f, + "Multiple processes ({}) found matching '{}'", + count, pattern + ), } } } @@ -53,18 +60,20 @@ pub struct ProcessInfo { /** * Check if a command exists in PATH. - * + * * # Arguments - * + * * * `cmd` - The command to check - * + * * # Returns - * + * * * `Option` - The full path to the command if found, None otherwise - * + * * # Examples - * + * * ``` + * use sal::process::which; + * * match which("git") { * Some(path) => println!("Git is installed at: {}", path), * None => println!("Git is not installed"), @@ -74,14 +83,12 @@ pub struct ProcessInfo { pub fn which(cmd: &str) -> Option { #[cfg(target_os = "windows")] let which_cmd = "where"; - + #[cfg(any(target_os = "macos", target_os = "linux"))] let which_cmd = "which"; - - let output = Command::new(which_cmd) - .arg(cmd) - .output(); - + + let output = Command::new(which_cmd).arg(cmd).output(); + match output { Ok(out) => { if out.status.success() { @@ -90,29 +97,34 @@ pub fn which(cmd: &str) -> Option { } else { None } - }, - Err(_) => None + } + Err(_) => None, } } /** * Kill processes matching a pattern. - * + * * # Arguments - * + * * * `pattern` - The pattern to match against process names - * + * * # Returns - * + * * * `Ok(String)` - A success message indicating processes were killed or none were found * * `Err(ProcessError)` - An error if the kill operation failed - * + * * # Examples - * + * * ``` * // Kill all processes with "server" in their name - * let result = kill("server")?; - * println!("{}", result); + * use sal::process::kill; + * + * fn main() -> Result<(), Box> { + * let result = kill("server")?; + * println!("{}", result); + * Ok(()) + * } * ``` */ pub fn kill(pattern: &str) -> Result { @@ -121,7 +133,7 @@ pub fn kill(pattern: &str) -> Result { { // On Windows, use taskkill with wildcard support let mut args = vec!["/F"]; // Force kill - + if pattern.contains('*') { // If it contains wildcards, use filter args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]); @@ -129,12 +141,12 @@ pub fn kill(pattern: &str) -> Result { // Otherwise use image name directly args.extend(&["/IM", pattern]); } - + let output = Command::new("taskkill") .args(&args) .output() .map_err(ProcessError::CommandExecutionFailed)?; - + if output.status.success() { Ok("Successfully killed processes".to_string()) } else { @@ -144,14 +156,20 @@ pub fn kill(pattern: &str) -> Result { if stdout.contains("No tasks") { Ok("No matching processes found".to_string()) } else { - Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", stdout))) + Err(ProcessError::CommandFailed(format!( + "Failed to kill processes: {}", + stdout + ))) } } else { - Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error))) + Err(ProcessError::CommandFailed(format!( + "Failed to kill processes: {}", + error + ))) } } } - + #[cfg(any(target_os = "macos", target_os = "linux"))] { // On Unix-like systems, use pkill which has built-in pattern matching @@ -160,7 +178,7 @@ pub fn kill(pattern: &str) -> Result { .arg(pattern) .output() .map_err(ProcessError::CommandExecutionFailed)?; - + // pkill returns 0 if processes were killed, 1 if none matched if output.status.success() { Ok("Successfully killed processes".to_string()) @@ -168,39 +186,47 @@ pub fn kill(pattern: &str) -> Result { Ok("No matching processes found".to_string()) } else { let error = String::from_utf8_lossy(&output.stderr); - Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error))) + Err(ProcessError::CommandFailed(format!( + "Failed to kill processes: {}", + error + ))) } } } /** * List processes matching a pattern (or all if pattern is empty). - * + * * # Arguments - * + * * * `pattern` - The pattern to match against process names (empty string for all processes) - * + * * # Returns - * + * * * `Ok(Vec)` - A vector of process information for matching processes * * `Err(ProcessError)` - An error if the list operation failed - * + * * # Examples - * + * * ``` * // List all processes - * let processes = process_list("")?; - * - * // List processes with "server" in their name - * let processes = process_list("server")?; - * for proc in processes { - * println!("PID: {}, Name: {}", proc.pid, proc.name); + * use sal::process::process_list; + * + * fn main() -> Result<(), Box> { + * let processes = process_list("")?; + * + * // List processes with "server" in their name + * let processes = process_list("server")?; + * for proc in processes { + * println!("PID: {}, Name: {}", proc.pid, proc.name); + * } + * Ok(()) * } * ``` */ pub fn process_list(pattern: &str) -> Result, ProcessError> { let mut processes = Vec::new(); - + // Platform specific implementations #[cfg(target_os = "windows")] { @@ -209,22 +235,23 @@ pub fn process_list(pattern: &str) -> Result, ProcessError> { .args(&["process", "list", "brief"]) .output() .map_err(ProcessError::CommandExecutionFailed)?; - + if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - + // Parse output (assuming format: Handle Name Priority) - for line in stdout.lines().skip(1) { // Skip header + for line in stdout.lines().skip(1) { + // Skip header let parts: Vec<&str> = line.trim().split_whitespace().collect(); if parts.len() >= 2 { let pid = parts[0].parse::().unwrap_or(0); let name = parts[1].to_string(); - + // Filter by pattern if provided if !pattern.is_empty() && !name.contains(pattern) { continue; } - + processes.push(ProcessInfo { pid, name, @@ -235,10 +262,13 @@ pub fn process_list(pattern: &str) -> Result, ProcessError> { } } else { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr))); + return Err(ProcessError::CommandFailed(format!( + "Failed to list processes: {}", + stderr + ))); } } - + #[cfg(any(target_os = "macos", target_os = "linux"))] { // Unix implementation using ps @@ -246,22 +276,23 @@ pub fn process_list(pattern: &str) -> Result, ProcessError> { .args(&["-eo", "pid,comm"]) .output() .map_err(ProcessError::CommandExecutionFailed)?; - + if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - + // Parse output (assuming format: PID COMMAND) - for line in stdout.lines().skip(1) { // Skip header + for line in stdout.lines().skip(1) { + // Skip header let parts: Vec<&str> = line.trim().split_whitespace().collect(); if parts.len() >= 2 { let pid = parts[0].parse::().unwrap_or(0); let name = parts[1].to_string(); - + // Filter by pattern if provided if !pattern.is_empty() && !name.contains(pattern) { continue; } - + processes.push(ProcessInfo { pid, name, @@ -272,38 +303,49 @@ pub fn process_list(pattern: &str) -> Result, ProcessError> { } } else { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr))); + return Err(ProcessError::CommandFailed(format!( + "Failed to list processes: {}", + stderr + ))); } } - + Ok(processes) } /** * Get a single process matching the pattern (error if 0 or more than 1 match). - * + * * # Arguments - * + * * * `pattern` - The pattern to match against process names - * + * * # Returns - * + * * * `Ok(ProcessInfo)` - Information about the matching process * * `Err(ProcessError)` - An error if no process or multiple processes match - * + * * # Examples - * - * ``` - * let process = process_get("unique-server-name")?; - * println!("Found process: {} (PID: {})", process.name, process.pid); + * + * ```no_run + * use sal::process::process_get; + * + * fn main() -> Result<(), Box> { + * let process = process_get("unique-server-name")?; + * println!("Found process: {} (PID: {})", process.name, process.pid); + * Ok(()) + * } * ``` */ pub fn process_get(pattern: &str) -> Result { let processes = process_list(pattern)?; - + match processes.len() { 0 => Err(ProcessError::NoProcessFound(pattern.to_string())), 1 => Ok(processes[0].clone()), - _ => Err(ProcessError::MultipleProcessesFound(pattern.to_string(), processes.len())), + _ => Err(ProcessError::MultipleProcessesFound( + pattern.to_string(), + processes.len(), + )), } } diff --git a/src/rhai/mod.rs b/src/rhai/mod.rs index fd00af0..f3e380f 100644 --- a/src/rhai/mod.rs +++ b/src/rhai/mod.rs @@ -12,8 +12,9 @@ mod postgresclient; mod process; mod redisclient; mod rfs; -mod hero_vault; // This module now uses hero_vault internally +mod vault; mod text; +mod zinit; #[cfg(test)] mod tests; @@ -91,6 +92,9 @@ pub use rfs::register as register_rfs_module; pub use crate::git::{GitRepo, GitTree}; pub use git::register_git_module; +// Re-export zinit module +pub use zinit::register_zinit_module; + // Re-export text module pub use text::register_text_module; // Re-export text functions directly from text module @@ -107,7 +111,7 @@ pub use crate::text::{ pub use text::*; // Re-export crypto module -pub use hero_vault::register_crypto_module; +pub use vault::register_crypto_module; // Rename copy functions to avoid conflicts pub use os::copy as os_copy; @@ -120,7 +124,7 @@ pub use os::copy as os_copy; /// /// # Example /// -/// ``` +/// ```ignore /// use rhai::Engine; /// use sal::rhai; /// @@ -128,7 +132,8 @@ pub use os::copy as os_copy; /// rhai::register(&mut engine); /// /// // Now you can use SAL functions in Rhai scripts -/// let result = engine.eval::("exist('some_file.txt')").unwrap(); +/// // You can evaluate Rhai scripts with SAL functions +/// let result = engine.eval::("exist('some_file.txt')").unwrap(); /// ``` pub fn register(engine: &mut Engine) -> Result<(), Box> { // Register OS module functions @@ -146,6 +151,10 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { // Register Git module functions git::register_git_module(engine)?; + + // Register Zinit module functions + zinit::register_zinit_module(engine)?; + // Register Text module functions text::register_text_module(engine)?; @@ -153,7 +162,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { rfs::register(engine)?; // Register Crypto module functions - hero_vault::register_crypto_module(engine)?; + vault::register_crypto_module(engine)?; // Register Redis client module functions diff --git a/src/rhai/hero_vault.rs b/src/rhai/vault.rs similarity index 100% rename from src/rhai/hero_vault.rs rename to src/rhai/vault.rs diff --git a/src/rhai/zinit.rs b/src/rhai/zinit.rs new file mode 100644 index 0000000..2128d15 --- /dev/null +++ b/src/rhai/zinit.rs @@ -0,0 +1,345 @@ +//! Rhai wrappers for Zinit client module functions +//! +//! This module provides Rhai wrappers for the functions in the Zinit client module. + +use rhai::{Engine, EvalAltResult, Array, Dynamic, Map}; +use crate::zinit_client as client; +use tokio::runtime::Runtime; +use serde_json::{json, Value}; +use crate::rhai::error::ToRhaiError; + +/// Register Zinit module functions with the Rhai engine +/// +/// # Arguments +/// +/// * `engine` - The Rhai engine to register the functions with +/// +/// # Returns +/// +/// * `Result<(), Box>` - Ok if registration was successful, Err otherwise +pub fn register_zinit_module(engine: &mut Engine) -> Result<(), Box> { + // Register Zinit client functions + engine.register_fn("zinit_list", zinit_list); + engine.register_fn("zinit_status", zinit_status); + engine.register_fn("zinit_start", zinit_start); + engine.register_fn("zinit_stop", zinit_stop); + engine.register_fn("zinit_restart", zinit_restart); + engine.register_fn("zinit_monitor", zinit_monitor); + engine.register_fn("zinit_forget", zinit_forget); + engine.register_fn("zinit_kill", zinit_kill); + engine.register_fn("zinit_create_service", zinit_create_service); + engine.register_fn("zinit_delete_service", zinit_delete_service); + engine.register_fn("zinit_get_service", zinit_get_service); + engine.register_fn("zinit_logs", zinit_logs); + engine.register_fn("zinit_logs_all", zinit_logs_all); + + Ok(()) +} + +impl ToRhaiError for Result { + fn to_rhai_error(self) -> Result> { + self.map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Zinit error: {}", e).into(), + rhai::Position::NONE + )) + }) + } +} + +// Helper function to get a runtime +fn get_runtime() -> Result> { + tokio::runtime::Runtime::new().map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to create Tokio runtime: {}", e).into(), + rhai::Position::NONE + )) + }) +} + +// +// Zinit Client Function Wrappers +// + +/// Wrapper for zinit_client::list +/// +/// Lists all services managed by Zinit. +pub fn zinit_list(socket_path: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::list(socket_path).await + }); + + let services = result.to_rhai_error()?; + + // Convert HashMap to Rhai Map + let mut map = Map::new(); + for (name, state) in services { + map.insert(name.into(), Dynamic::from(state)); + } + + Ok(map) +} + +/// Wrapper for zinit_client::status +/// +/// Gets the status of a specific service. +pub fn zinit_status(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::status(socket_path, name).await + }); + + let status = result.to_rhai_error()?; + + // Convert Status to Rhai Map + let mut map = Map::new(); + map.insert("name".into(), Dynamic::from(status.name)); + map.insert("pid".into(), Dynamic::from(status.pid)); + map.insert("state".into(), Dynamic::from(status.state)); + map.insert("target".into(), Dynamic::from(status.target)); + + // Convert dependencies + let mut deps_map = Map::new(); + for (dep, state) in status.after { + deps_map.insert(dep.into(), Dynamic::from(state)); + } + map.insert("after".into(), Dynamic::from_map(deps_map)); + + Ok(map) +} + +/// Wrapper for zinit_client::start +/// +/// Starts a service. +pub fn zinit_start(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::start(socket_path, name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::stop +/// +/// Stops a service. +pub fn zinit_stop(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::stop(socket_path, name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::restart +/// +/// Restarts a service. +pub fn zinit_restart(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::restart(socket_path, name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::monitor +/// +/// Starts monitoring a service. +pub fn zinit_monitor(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.monitor(name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::forget +/// +/// Stops monitoring a service. +pub fn zinit_forget(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.forget(name).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::kill +/// +/// Sends a signal to a service. +pub fn zinit_kill(socket_path: &str, name: &str, signal: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.kill(name, signal).await + }); + + result.to_rhai_error()?; + Ok(true) +} + +/// Wrapper for zinit_client::create_service +/// +/// Creates a new service. +pub fn zinit_create_service(socket_path: &str, name: &str, exec: &str, oneshot: bool) -> Result> { + let rt = get_runtime()?; + + // Create service configuration + let content = serde_json::from_value(json!({ + "exec": exec, + "oneshot": oneshot + })).map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to create service configuration: {}", e).into(), + rhai::Position::NONE + )) + })?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.create_service(name, content).await + }); + + result.to_rhai_error() +} + +/// Wrapper for zinit_client::delete_service +/// +/// Deletes a service. +pub fn zinit_delete_service(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.delete_service(name).await + }); + + result.to_rhai_error() +} + +/// Wrapper for zinit_client::get_service +/// +/// Gets a service configuration. +pub fn zinit_get_service(socket_path: &str, name: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.get_service(name).await + }); + + let value = result.to_rhai_error()?; + + // Convert Value to Dynamic + match value { + Value::Object(map) => { + let mut rhai_map = Map::new(); + for (k, v) in map { + rhai_map.insert(k.into(), value_to_dynamic(v)); + } + Ok(Dynamic::from_map(rhai_map)) + }, + _ => Err(Box::new(EvalAltResult::ErrorRuntime( + "Expected object from get_service".into(), + rhai::Position::NONE + ))) + } +} + +/// Wrapper for zinit_client::logs with a filter +/// +/// Gets logs for a specific service. +pub fn zinit_logs(socket_path: &str, filter: &str) -> Result> { + let rt = get_runtime()?; + + let filter_string = Some(filter.to_string()); + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.logs(filter_string).await + }); + + let logs = result.to_rhai_error()?; + + // Convert Vec to Rhai Array + let mut array = Array::new(); + for log in logs { + array.push(Dynamic::from(log)); + } + + Ok(array) +} + +/// Wrapper for zinit_client::logs without a filter +/// +/// Gets all logs. +pub fn zinit_logs_all(socket_path: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + let client = client::get_zinit_client(socket_path).await?; + client.logs(None).await + }); + + let logs = result.to_rhai_error()?; + + // Convert Vec to Rhai Array + let mut array = Array::new(); + for log in logs { + array.push(Dynamic::from(log)); + } + + Ok(array) +} + +// Helper function to convert serde_json::Value to rhai::Dynamic +fn value_to_dynamic(value: Value) -> Dynamic { + match value { + Value::Null => Dynamic::UNIT, + Value::Bool(b) => Dynamic::from(b), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Dynamic::from(i) + } else if let Some(f) = n.as_f64() { + Dynamic::from(f) + } else { + Dynamic::from(n.to_string()) + } + }, + Value::String(s) => Dynamic::from(s), + Value::Array(arr) => { + let mut rhai_arr = Array::new(); + for item in arr { + rhai_arr.push(value_to_dynamic(item)); + } + Dynamic::from(rhai_arr) + }, + Value::Object(map) => { + let mut rhai_map = Map::new(); + for (k, v) in map { + rhai_map.insert(k.into(), value_to_dynamic(v)); + } + Dynamic::from_map(rhai_map) + } + } +} diff --git a/src/text/dedent.rs b/src/text/dedent.rs index ca9f659..0348524 100644 --- a/src/text/dedent.rs +++ b/src/text/dedent.rs @@ -1,30 +1,32 @@ /** * Dedent a multiline string by removing common leading whitespace. - * + * * This function analyzes all non-empty lines in the input text to determine * the minimum indentation level, then removes that amount of whitespace * from the beginning of each line. This is useful for working with * multi-line strings in code that have been indented to match the * surrounding code structure. - * + * * # Arguments - * + * * * `text` - The multiline string to dedent - * + * * # Returns - * + * * * `String` - The dedented string - * + * * # Examples - * + * * ``` + * use sal::text::dedent; + * * let indented = " line 1\n line 2\n line 3"; * let dedented = dedent(indented); * assert_eq!(dedented, "line 1\nline 2\n line 3"); * ``` - * + * * # Notes - * + * * - Empty lines are preserved but have all leading whitespace removed * - Tabs are counted as 4 spaces for indentation purposes */ @@ -32,7 +34,8 @@ pub fn dedent(text: &str) -> String { let lines: Vec<&str> = text.lines().collect(); // Find the minimum indentation level (ignore empty lines) - let min_indent = lines.iter() + let min_indent = lines + .iter() .filter(|line| !line.trim().is_empty()) .map(|line| { let mut spaces = 0; @@ -51,7 +54,8 @@ pub fn dedent(text: &str) -> String { .unwrap_or(0); // Remove that many spaces from the beginning of each line - lines.iter() + lines + .iter() .map(|line| { if line.trim().is_empty() { return String::new(); @@ -59,22 +63,22 @@ pub fn dedent(text: &str) -> String { let mut count = 0; let mut chars = line.chars().peekable(); - + // Skip initial spaces up to min_indent while count < min_indent && chars.peek().is_some() { match chars.peek() { Some(' ') => { chars.next(); count += 1; - }, + } Some('\t') => { chars.next(); count += 4; - }, + } _ => break, } } - + // Return the remaining characters chars.collect::() }) @@ -82,24 +86,25 @@ pub fn dedent(text: &str) -> String { .join("\n") } - /** * Prefix a multiline string with a specified prefix. - * + * * This function adds the specified prefix to the beginning of each line in the input text. - * + * * # Arguments - * + * * * `text` - The multiline string to prefix * * `prefix` - The prefix to add to each line - * + * * # Returns - * + * * * `String` - The prefixed string - * + * * # Examples - * + * * ``` + * use sal::text::prefix; + * * let text = "line 1\nline 2\nline 3"; * let prefixed = prefix(text, " "); * assert_eq!(prefixed, " line 1\n line 2\n line 3"); diff --git a/src/text/template.rs b/src/text/template.rs index d5b3ee1..f72c1f9 100644 --- a/src/text/template.rs +++ b/src/text/template.rs @@ -32,7 +32,7 @@ impl TemplateBuilder { /// ``` pub fn open>(template_path: P) -> io::Result { let path_str = template_path.as_ref().to_string_lossy().to_string(); - + // Verify the template file exists if !Path::new(&path_str).exists() { return Err(io::Error::new( @@ -40,14 +40,14 @@ impl TemplateBuilder { format!("Template file not found: {}", path_str), )); } - + Ok(Self { template_path: path_str, context: Context::new(), tera: None, }) } - + /// Adds a variable to the template context. /// /// # Arguments @@ -61,12 +61,15 @@ impl TemplateBuilder { /// /// # Example /// - /// ``` + /// ```no_run /// use sal::text::TemplateBuilder; /// - /// let builder = TemplateBuilder::open("templates/example.html")? - /// .add_var("title", "Hello World") - /// .add_var("username", "John Doe"); + /// fn main() -> Result<(), Box> { + /// let builder = TemplateBuilder::open("templates/example.html")? + /// .add_var("title", "Hello World") + /// .add_var("username", "John Doe"); + /// Ok(()) + /// } /// ``` pub fn add_var(mut self, name: S, value: V) -> Self where @@ -76,7 +79,7 @@ impl TemplateBuilder { self.context.insert(name.as_ref(), &value); self } - + /// Adds multiple variables to the template context from a HashMap. /// /// # Arguments @@ -89,16 +92,19 @@ impl TemplateBuilder { /// /// # Example /// - /// ``` + /// ```no_run /// use sal::text::TemplateBuilder; /// use std::collections::HashMap; /// - /// let mut vars = HashMap::new(); - /// vars.insert("title", "Hello World"); - /// vars.insert("username", "John Doe"); + /// fn main() -> Result<(), Box> { + /// let mut vars = HashMap::new(); + /// vars.insert("title", "Hello World"); + /// vars.insert("username", "John Doe"); /// - /// let builder = TemplateBuilder::open("templates/example.html")? - /// .add_vars(vars); + /// let builder = TemplateBuilder::open("templates/example.html")? + /// .add_vars(vars); + /// Ok(()) + /// } /// ``` pub fn add_vars(mut self, vars: HashMap) -> Self where @@ -110,7 +116,7 @@ impl TemplateBuilder { } self } - + /// Initializes the Tera template engine with the template file. /// /// This method is called automatically by render() if not called explicitly. @@ -122,24 +128,24 @@ impl TemplateBuilder { if self.tera.is_none() { // Create a new Tera instance with just this template let mut tera = Tera::default(); - + // Read the template content let template_content = fs::read_to_string(&self.template_path) .map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?; - + // Add the template to Tera let template_name = Path::new(&self.template_path) .file_name() .and_then(|n| n.to_str()) .unwrap_or("template"); - + tera.add_raw_template(template_name, &template_content)?; self.tera = Some(tera); } - + Ok(()) } - + /// Renders the template with the current context. /// /// # Returns @@ -148,31 +154,34 @@ impl TemplateBuilder { /// /// # Example /// - /// ``` + /// ```no_run /// use sal::text::TemplateBuilder; /// - /// let result = TemplateBuilder::open("templates/example.html")? - /// .add_var("title", "Hello World") - /// .add_var("username", "John Doe") - /// .render()?; + /// fn main() -> Result<(), Box> { + /// let result = TemplateBuilder::open("templates/example.html")? + /// .add_var("title", "Hello World") + /// .add_var("username", "John Doe") + /// .render()?; /// - /// println!("Rendered template: {}", result); + /// println!("Rendered template: {}", result); + /// Ok(()) + /// } /// ``` pub fn render(&mut self) -> Result { // Initialize Tera if not already done self.initialize_tera()?; - + // Get the template name let template_name = Path::new(&self.template_path) .file_name() .and_then(|n| n.to_str()) .unwrap_or("template"); - + // Render the template let tera = self.tera.as_ref().unwrap(); tera.render(template_name, &self.context) } - + /// Renders the template and writes the result to a file. /// /// # Arguments @@ -185,19 +194,25 @@ impl TemplateBuilder { /// /// # Example /// - /// ``` + /// ```no_run /// use sal::text::TemplateBuilder; /// - /// TemplateBuilder::open("templates/example.html")? - /// .add_var("title", "Hello World") - /// .add_var("username", "John Doe") - /// .render_to_file("output.html")?; + /// fn main() -> Result<(), Box> { + /// TemplateBuilder::open("templates/example.html")? + /// .add_var("title", "Hello World") + /// .add_var("username", "John Doe") + /// .render_to_file("output.html")?; + /// Ok(()) + /// } /// ``` pub fn render_to_file>(&mut self, output_path: P) -> io::Result<()> { let rendered = self.render().map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Template rendering error: {}", e)) + io::Error::new( + io::ErrorKind::Other, + format!("Template rendering error: {}", e), + ) })?; - + fs::write(output_path, rendered) } } @@ -207,70 +222,68 @@ mod tests { use super::*; use std::io::Write; use tempfile::NamedTempFile; - + #[test] fn test_template_rendering() -> Result<(), Box> { // Create a temporary template file let temp_file = NamedTempFile::new()?; let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n"; fs::write(temp_file.path(), template_content)?; - + // Create a template builder and add variables let mut builder = TemplateBuilder::open(temp_file.path())?; - builder = builder - .add_var("name", "John") - .add_var("place", "Rust"); - + builder = builder.add_var("name", "John").add_var("place", "Rust"); + // Render the template let result = builder.render()?; assert_eq!(result, "Hello, John! Welcome to Rust.\n"); - + Ok(()) } - + #[test] fn test_template_with_multiple_vars() -> Result<(), Box> { // Create a temporary template file let temp_file = NamedTempFile::new()?; let template_content = "{% if show_greeting %}Hello, {{ name }}!{% endif %}\n{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}\n"; fs::write(temp_file.path(), template_content)?; - + // Create a template builder and add variables let mut builder = TemplateBuilder::open(temp_file.path())?; - + // Add variables including a boolean and a vector builder = builder .add_var("name", "Alice") .add_var("show_greeting", true) .add_var("items", vec!["apple", "banana", "cherry"]); - + // Render the template let result = builder.render()?; assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n"); - + Ok(()) } - + #[test] fn test_template_with_hashmap_vars() -> Result<(), Box> { // Create a temporary template file let mut temp_file = NamedTempFile::new()?; writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?; temp_file.flush()?; - + // Create a HashMap of variables let mut vars = HashMap::new(); vars.insert("greeting", "Hi"); vars.insert("name", "Bob"); - + // Create a template builder and add variables from HashMap let mut builder = TemplateBuilder::open(temp_file.path())?; builder = builder.add_vars(vars); - + // Render the template let result = builder.render()?; assert_eq!(result, "Hi, Bob!\n"); - + Ok(()) } #[test] @@ -279,20 +292,19 @@ mod tests { let temp_file = NamedTempFile::new()?; let template_content = "{{ message }}\n"; fs::write(temp_file.path(), template_content)?; - - + // Create an output file let output_file = NamedTempFile::new()?; - + // Create a template builder, add a variable, and render to file let mut builder = TemplateBuilder::open(temp_file.path())?; builder = builder.add_var("message", "This is a test"); builder.render_to_file(output_file.path())?; - + // Read the output file and verify its contents let content = fs::read_to_string(output_file.path())?; assert_eq!(content, "This is a test\n"); - + Ok(()) } -} \ No newline at end of file +} diff --git a/src/hero_vault/README.md b/src/vault/README.md similarity index 100% rename from src/hero_vault/README.md rename to src/vault/README.md diff --git a/src/hero_vault/error.rs b/src/vault/error.rs similarity index 100% rename from src/hero_vault/error.rs rename to src/vault/error.rs diff --git a/src/hero_vault/ethereum/README.md b/src/vault/ethereum/README.md similarity index 100% rename from src/hero_vault/ethereum/README.md rename to src/vault/ethereum/README.md diff --git a/src/hero_vault/ethereum/contract.rs b/src/vault/ethereum/contract.rs similarity index 99% rename from src/hero_vault/ethereum/contract.rs rename to src/vault/ethereum/contract.rs index f3bb482..1cb3a97 100644 --- a/src/hero_vault/ethereum/contract.rs +++ b/src/vault/ethereum/contract.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::str::FromStr; use serde::{Serialize, Deserialize}; -use crate::hero_vault::error::CryptoError; +use crate::vault::error::CryptoError; use super::wallet::EthereumWallet; use super::networks::NetworkConfig; diff --git a/src/hero_vault/ethereum/contract_utils.rs b/src/vault/ethereum/contract_utils.rs similarity index 100% rename from src/hero_vault/ethereum/contract_utils.rs rename to src/vault/ethereum/contract_utils.rs diff --git a/src/hero_vault/ethereum/mod.rs b/src/vault/ethereum/mod.rs similarity index 98% rename from src/hero_vault/ethereum/mod.rs rename to src/vault/ethereum/mod.rs index 1cfd99c..a90037f 100644 --- a/src/hero_vault/ethereum/mod.rs +++ b/src/vault/ethereum/mod.rs @@ -16,11 +16,8 @@ mod provider; mod transaction; mod storage; mod contract; -mod contract_utils; +pub mod contract_utils; pub mod networks; -#[cfg(test)] -pub mod tests; - // Re-export public types and functions pub use wallet::EthereumWallet; pub use networks::NetworkConfig; diff --git a/src/hero_vault/ethereum/networks.rs b/src/vault/ethereum/networks.rs similarity index 100% rename from src/hero_vault/ethereum/networks.rs rename to src/vault/ethereum/networks.rs diff --git a/src/hero_vault/ethereum/provider.rs b/src/vault/ethereum/provider.rs similarity index 100% rename from src/hero_vault/ethereum/provider.rs rename to src/vault/ethereum/provider.rs diff --git a/src/hero_vault/ethereum/storage.rs b/src/vault/ethereum/storage.rs similarity index 100% rename from src/hero_vault/ethereum/storage.rs rename to src/vault/ethereum/storage.rs diff --git a/src/hero_vault/ethereum/tests/contract_args_tests.rs b/src/vault/ethereum/tests/contract_args_tests.rs similarity index 97% rename from src/hero_vault/ethereum/tests/contract_args_tests.rs rename to src/vault/ethereum/tests/contract_args_tests.rs index a729b6f..b7cb76a 100644 --- a/src/hero_vault/ethereum/tests/contract_args_tests.rs +++ b/src/vault/ethereum/tests/contract_args_tests.rs @@ -3,7 +3,7 @@ use ethers::types::Address; use std::str::FromStr; -use crate::hero_vault::ethereum::*; +use crate::vault::ethereum::*; #[test] fn test_contract_creation() { diff --git a/src/hero_vault/ethereum/tests/contract_tests.rs b/src/vault/ethereum/tests/contract_tests.rs similarity index 98% rename from src/hero_vault/ethereum/tests/contract_tests.rs rename to src/vault/ethereum/tests/contract_tests.rs index 171e9e4..d37dde3 100644 --- a/src/hero_vault/ethereum/tests/contract_tests.rs +++ b/src/vault/ethereum/tests/contract_tests.rs @@ -3,7 +3,7 @@ use ethers::types::Address; use std::str::FromStr; -use crate::hero_vault::ethereum::*; +use crate::vault::ethereum::*; #[test] fn test_contract_creation() { diff --git a/src/hero_vault/ethereum/tests/mod.rs b/src/vault/ethereum/tests/mod.rs similarity index 100% rename from src/hero_vault/ethereum/tests/mod.rs rename to src/vault/ethereum/tests/mod.rs diff --git a/src/hero_vault/ethereum/tests/network_tests.rs b/src/vault/ethereum/tests/network_tests.rs similarity index 99% rename from src/hero_vault/ethereum/tests/network_tests.rs rename to src/vault/ethereum/tests/network_tests.rs index 8f1aa06..0af8a7a 100644 --- a/src/hero_vault/ethereum/tests/network_tests.rs +++ b/src/vault/ethereum/tests/network_tests.rs @@ -1,6 +1,6 @@ //! Tests for Ethereum network functionality. -use crate::hero_vault::ethereum::*; +use crate::vault::ethereum::*; #[test] fn test_network_config() { diff --git a/src/hero_vault/ethereum/tests/transaction_tests.rs b/src/vault/ethereum/tests/transaction_tests.rs similarity index 95% rename from src/hero_vault/ethereum/tests/transaction_tests.rs rename to src/vault/ethereum/tests/transaction_tests.rs index 1cab28f..a662331 100644 --- a/src/hero_vault/ethereum/tests/transaction_tests.rs +++ b/src/vault/ethereum/tests/transaction_tests.rs @@ -1,9 +1,9 @@ //! Tests for Ethereum transaction functionality. -use crate::hero_vault::ethereum::*; -use crate::hero_vault::keypair::KeyPair; +use crate::vault::ethereum::*; +use crate::vault::keypair::implementation::KeyPair; use ethers::types::U256; -use std::str::FromStr; +// use std::str::FromStr; #[test] fn test_format_balance() { diff --git a/src/hero_vault/ethereum/tests/wallet_tests.rs b/src/vault/ethereum/tests/wallet_tests.rs similarity index 96% rename from src/hero_vault/ethereum/tests/wallet_tests.rs rename to src/vault/ethereum/tests/wallet_tests.rs index 7b78cec..fbf82f3 100644 --- a/src/hero_vault/ethereum/tests/wallet_tests.rs +++ b/src/vault/ethereum/tests/wallet_tests.rs @@ -1,7 +1,7 @@ //! Tests for Ethereum wallet functionality. -use crate::hero_vault::ethereum::*; -use crate::hero_vault::keypair::KeyPair; +use crate::vault::ethereum::*; +use crate::vault::keypair::implementation::KeyPair; use ethers::utils::hex; use ethers::prelude::Signer; @@ -62,8 +62,8 @@ fn test_wallet_management() { clear_ethereum_wallets(); // Create a key space and keypair - crate::hero_vault::keypair::create_space("test_space").unwrap(); - crate::hero_vault::keypair::create_keypair("test_keypair3").unwrap(); + crate::vault::keypair::session_manager::create_space("test_space").unwrap(); + crate::vault::keypair::create_keypair("test_keypair3").unwrap(); // Create a wallet let wallet = create_ethereum_wallet().unwrap(); diff --git a/src/hero_vault/ethereum/transaction.rs b/src/vault/ethereum/transaction.rs similarity index 99% rename from src/hero_vault/ethereum/transaction.rs rename to src/vault/ethereum/transaction.rs index e12892e..d772c73 100644 --- a/src/hero_vault/ethereum/transaction.rs +++ b/src/vault/ethereum/transaction.rs @@ -3,7 +3,7 @@ use ethers::prelude::*; use ethers::types::transaction::eip2718::TypedTransaction; -use crate::hero_vault::error::CryptoError; +use crate::vault::error::CryptoError; use super::wallet::EthereumWallet; use super::networks::NetworkConfig; use super::provider; diff --git a/src/hero_vault/ethereum/wallet.rs b/src/vault/ethereum/wallet.rs similarity index 98% rename from src/hero_vault/ethereum/wallet.rs rename to src/vault/ethereum/wallet.rs index cda3f18..f698156 100644 --- a/src/hero_vault/ethereum/wallet.rs +++ b/src/vault/ethereum/wallet.rs @@ -7,8 +7,8 @@ use k256::ecdsa::SigningKey; use std::str::FromStr; use sha2::{Sha256, Digest}; -use crate::hero_vault::error::CryptoError; -use crate::hero_vault::keypair::KeyPair; +use crate::vault::error::CryptoError; +use crate::vault::keypair::KeyPair; use super::networks::NetworkConfig; /// An Ethereum wallet derived from a keypair. diff --git a/src/hero_vault/keypair/README.md b/src/vault/keypair/README.md similarity index 50% rename from src/hero_vault/keypair/README.md rename to src/vault/keypair/README.md index b89f9cf..4cfb15d 100644 --- a/src/hero_vault/keypair/README.md +++ b/src/vault/keypair/README.md @@ -6,8 +6,9 @@ The Keypair module provides functionality for creating, managing, and using ECDS The Keypair module is organized into: -- `implementation.rs` - Core implementation of the KeyPair and KeySpace types -- `mod.rs` - Module exports and public interface +- `keypair_types.rs` - Defines the KeyPair and related types. +- `session_manager.rs` - Implements the core logic for managing keypairs and key spaces. +- `mod.rs` - Module exports and public interface. ## Key Types @@ -113,26 +114,44 @@ let mut loaded_space = KeySpace::load("my_space", "secure_password")?; The module provides functionality for creating, selecting, and using keypairs: ```rust -// Create a new keypair in the key space -let keypair = space.create_keypair("my_keypair", "secure_password")?; +use crate::vault::keypair::{KeySpace, KeyPair}; +use crate::vault::error::CryptoError; // Assuming CryptoError is in vault::error -// Select a keypair for use -space.select_keypair("my_keypair")?; +fn demonstrate_keypair_management() -> Result<(), CryptoError> { + // Create a new key space + let mut space = KeySpace::new("my_space", "secure_password")?; -// Get the currently selected keypair -let current = space.current_keypair()?; + // Create a new keypair in the key space + let keypair = space.create_keypair("my_keypair", "secure_password")?; + println!("Created keypair: {}", keypair.public_key().iter().map(|b| format!("{:02x}", b)).collect::()); -// List all keypairs in the key space -let keypairs = space.list_keypairs()?; + // Select a keypair for use + space.select_keypair("my_keypair")?; + println!("Selected keypair: {}", space.current_keypair()?.public_key().iter().map(|b| format!("{:02x}", b)).collect::()); -// Get a keypair by name -let keypair = space.get_keypair("my_keypair")?; + // List all keypairs in the key space + let keypairs = space.list_keypairs()?; + println!("Keypairs in space: {:?}", keypairs); -// Remove a keypair from the key space -space.remove_keypair("my_keypair")?; + // Get a keypair by name + let retrieved_keypair = space.get_keypair("my_keypair")?; + println!("Retrieved keypair: {}", retrieved_keypair.public_key().iter().map(|b| format!("{:02x}", b)).collect::()); -// Rename a keypair -space.rename_keypair("my_keypair", "new_name")?; + // Rename a keypair + space.rename_keypair("my_keypair", "new_name")?; + println!("Renamed keypair to new_name"); + let keypairs_after_rename = space.list_keypairs()?; + println!("Keypairs in space after rename: {:?}", keypairs_after_rename); + + + // Remove a keypair from the key space + space.remove_keypair("new_name")?; + println!("Removed keypair new_name"); + let keypairs_after_remove = space.list_keypairs()?; + println!("Keypairs in space after removal: {:?}", keypairs_after_remove); + + Ok(()) +} ``` ### Digital Signatures @@ -140,12 +159,35 @@ space.rename_keypair("my_keypair", "new_name")?; The module provides functionality for signing and verifying messages using ECDSA: ```rust -// Sign a message using the selected keypair -let keypair = space.current_keypair()?; -let signature = keypair.sign("This is a message to sign".as_bytes())?; +use crate::vault::keypair::KeySpace; +use crate::vault::error::CryptoError; // Assuming CryptoError is in vault::error -// Verify a signature -let is_valid = keypair.verify("This is a message to sign".as_bytes(), &signature)?; +fn demonstrate_digital_signatures() -> Result<(), CryptoError> { + // Assuming a key space and selected keypair exist + // let mut space = KeySpace::load("my_space", "secure_password")?; // Load existing space + let mut space = KeySpace::new("temp_space_for_demo", "password")?; // Or create a new one for demo + space.create_keypair("my_signing_key", "key_password")?; + space.select_keypair("my_signing_key")?; + + + // Sign a message using the selected keypair + let keypair = space.current_keypair()?; + let message = "This is a message to sign".as_bytes(); + let signature = keypair.sign(message)?; + println!("Message signed. Signature: {:?}", signature); + + // Verify a signature + let is_valid = keypair.verify(message, &signature)?; + println!("Signature valid: {}", is_valid); + + // Example of invalid signature verification + let invalid_signature = vec![0u8; signature.len()]; // A dummy invalid signature + let is_valid_invalid = keypair.verify(message, &invalid_signature)?; + println!("Invalid signature valid: {}", is_valid_invalid); + + + Ok(()) +} ``` ### Ethereum Address Derivation @@ -153,11 +195,53 @@ let is_valid = keypair.verify("This is a message to sign".as_bytes(), &signature The module provides functionality for deriving Ethereum addresses from keypairs: ```rust -// Derive an Ethereum address from a keypair -let keypair = space.current_keypair()?; -let address = keypair.to_ethereum_address()?; +use crate::vault::keypair::KeySpace; +use crate::vault::error::CryptoError; // Assuming CryptoError is in vault::error + +fn demonstrate_ethereum_address_derivation() -> Result<(), CryptoError> { + // Assuming a key space and selected keypair exist + // let mut space = KeySpace::load("my_space", "secure_password")?; // Load existing space + let mut space = KeySpace::new("temp_space_for_eth_demo", "password")?; // Or create a new one for demo + space.create_keypair("my_eth_key", "key_password")?; + space.select_keypair("my_eth_key")?; + + // Derive an Ethereum address from a keypair + let keypair = space.current_keypair()?; + let address = keypair.to_ethereum_address()?; + println!("Derived Ethereum address: {}", address); + + Ok(()) +} ``` +## Including in Your Project + +To include the Hero Vault Keypair module in your Rust project, add the following to your `Cargo.toml` file: + +```toml +[dependencies] +hero_vault = "0.1.0" # Replace with the actual version +``` + +Then, you can import and use the module in your Rust code: + +```rust +use hero_vault::vault::keypair::{KeySpace, KeyPair}; +use hero_vault::vault::error::CryptoError; +``` + +## Testing + +Tests for the Keypair module are included within the source files, likely in `session_manager.rs` or `mod.rs` as inline tests. + +To run the tests, navigate to the root directory of the project in your terminal and execute the following command: + +```bash +cargo test --lib vault::keypair +``` + +This command will run all tests specifically within the `vault::keypair` module. + ## Security Considerations - Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password diff --git a/src/hero_vault/keypair/implementation.rs b/src/vault/keypair/keypair_types.rs similarity index 66% rename from src/hero_vault/keypair/implementation.rs rename to src/vault/keypair/keypair_types.rs index edd35c6..cdc5374 100644 --- a/src/hero_vault/keypair/implementation.rs +++ b/src/vault/keypair/keypair_types.rs @@ -1,14 +1,13 @@ -//! Implementation of keypair functionality. +/// Implementation of keypair functionality. use k256::ecdsa::{SigningKey, VerifyingKey, signature::{Signer, Verifier}, Signature}; use rand::rngs::OsRng; use serde::{Serialize, Deserialize}; use std::collections::HashMap; -use once_cell::sync::Lazy; -use std::sync::Mutex; use sha2::{Sha256, Digest}; -use crate::hero_vault::error::CryptoError; +use crate::vault::symmetric::implementation; +use crate::vault::error::CryptoError; /// A keypair for signing and verifying messages. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -226,7 +225,7 @@ impl KeyPair { }; // Encrypt the message using the derived key - let ciphertext = crate::hero_vault::symmetric::encrypt_with_key(&shared_secret, message) + let ciphertext = implementation::encrypt_with_key(&shared_secret, message) .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; // Format: ephemeral_public_key || ciphertext @@ -263,7 +262,7 @@ impl KeyPair { }; // Decrypt the message using the derived key - crate::hero_vault::symmetric::decrypt_with_key(&shared_secret, actual_ciphertext) + implementation::decrypt_with_key(&shared_secret, actual_ciphertext) .map_err(|e| CryptoError::DecryptionFailed(e.to_string())) } } @@ -306,162 +305,3 @@ impl KeySpace { } } -/// Session state for the current key space and selected keypair. -pub struct Session { - pub current_space: Option, - pub selected_keypair: Option, -} - -impl Default for Session { - fn default() -> Self { - Session { - current_space: None, - selected_keypair: None, - } - } -} - -/// Global session state. -static SESSION: Lazy> = Lazy::new(|| { - Mutex::new(Session::default()) -}); - -/// Creates a new key space with the given name. -pub fn create_space(name: &str) -> Result<(), CryptoError> { - let mut session = SESSION.lock().unwrap(); - - // Create a new space - let space = KeySpace::new(name); - - // Set as current space - session.current_space = Some(space); - session.selected_keypair = None; - - Ok(()) -} - -/// Sets the current key space. -pub fn set_current_space(space: KeySpace) -> Result<(), CryptoError> { - let mut session = SESSION.lock().unwrap(); - session.current_space = Some(space); - session.selected_keypair = None; - Ok(()) -} - -/// Gets the current key space. -pub fn get_current_space() -> Result { - let session = SESSION.lock().unwrap(); - session.current_space.clone().ok_or(CryptoError::NoActiveSpace) -} - -/// Clears the current session (logout). -pub fn clear_session() { - let mut session = SESSION.lock().unwrap(); - session.current_space = None; - session.selected_keypair = None; -} - -/// Creates a new keypair in the current space. -pub fn create_keypair(name: &str) -> Result<(), CryptoError> { - let mut session = SESSION.lock().unwrap(); - - if let Some(ref mut space) = session.current_space { - if space.keypairs.contains_key(name) { - return Err(CryptoError::KeypairAlreadyExists(name.to_string())); - } - - let keypair = KeyPair::new(name); - space.keypairs.insert(name.to_string(), keypair); - - // Automatically select the new keypair - session.selected_keypair = Some(name.to_string()); - - Ok(()) - } else { - Err(CryptoError::NoActiveSpace) - } -} - -/// Selects a keypair for use. -pub fn select_keypair(name: &str) -> Result<(), CryptoError> { - let mut session = SESSION.lock().unwrap(); - - if let Some(ref space) = session.current_space { - if !space.keypairs.contains_key(name) { - return Err(CryptoError::KeypairNotFound(name.to_string())); - } - - session.selected_keypair = Some(name.to_string()); - Ok(()) - } else { - Err(CryptoError::NoActiveSpace) - } -} - -/// Gets the currently selected keypair. -pub fn get_selected_keypair() -> Result { - let session = SESSION.lock().unwrap(); - - if let Some(ref space) = session.current_space { - if let Some(ref keypair_name) = session.selected_keypair { - if let Some(keypair) = space.keypairs.get(keypair_name) { - return Ok(keypair.clone()); - } - return Err(CryptoError::KeypairNotFound(keypair_name.clone())); - } - return Err(CryptoError::NoKeypairSelected); - } - - Err(CryptoError::NoActiveSpace) -} - -/// Lists all keypair names in the current space. -pub fn list_keypairs() -> Result, CryptoError> { - let session = SESSION.lock().unwrap(); - - if let Some(ref space) = session.current_space { - Ok(space.keypairs.keys().cloned().collect()) - } else { - Err(CryptoError::NoActiveSpace) - } -} - -/// Gets the public key of the selected keypair. -pub fn keypair_pub_key() -> Result, CryptoError> { - let keypair = get_selected_keypair()?; - Ok(keypair.pub_key()) -} - -/// Derives a public key from a private key. -pub fn derive_public_key(private_key: &[u8]) -> Result, CryptoError> { - KeyPair::pub_key_from_private(private_key) -} - -/// Signs a message with the selected keypair. -pub fn keypair_sign(message: &[u8]) -> Result, CryptoError> { - let keypair = get_selected_keypair()?; - Ok(keypair.sign(message)) -} - -/// Verifies a message signature with the selected keypair. -pub fn keypair_verify(message: &[u8], signature_bytes: &[u8]) -> Result { - let keypair = get_selected_keypair()?; - keypair.verify(message, signature_bytes) -} - -/// Verifies a message signature with a public key. -pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result { - KeyPair::verify_with_public_key(public_key, message, signature_bytes) -} - -/// Encrypts a message for a recipient using their public key. -pub fn encrypt_asymmetric(recipient_public_key: &[u8], message: &[u8]) -> Result, CryptoError> { - let keypair = get_selected_keypair()?; - keypair.encrypt_asymmetric(recipient_public_key, message) -} - -/// Decrypts a message that was encrypted with the current keypair's public key. -pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result, CryptoError> { - let keypair = get_selected_keypair()?; - keypair.decrypt_asymmetric(ciphertext) -} diff --git a/src/hero_vault/keypair/mod.rs b/src/vault/keypair/mod.rs similarity index 75% rename from src/hero_vault/keypair/mod.rs rename to src/vault/keypair/mod.rs index 016b7f2..d9ea317 100644 --- a/src/hero_vault/keypair/mod.rs +++ b/src/vault/keypair/mod.rs @@ -2,13 +2,17 @@ //! //! This module provides functionality for creating and managing ECDSA key pairs. -mod implementation; +pub mod keypair_types; +pub mod session_manager; // Re-export public types and functions -pub use implementation::{ - KeyPair, KeySpace, +pub use keypair_types::{KeyPair, KeySpace}; +pub use session_manager::{ create_space, set_current_space, get_current_space, clear_session, create_keypair, select_keypair, get_selected_keypair, list_keypairs, keypair_pub_key, derive_public_key, keypair_sign, keypair_verify, verify_with_public_key, encrypt_asymmetric, decrypt_asymmetric }; + +#[cfg(test)] +mod tests; diff --git a/src/vault/keypair/session_manager.rs b/src/vault/keypair/session_manager.rs new file mode 100644 index 0000000..74094b0 --- /dev/null +++ b/src/vault/keypair/session_manager.rs @@ -0,0 +1,174 @@ +use once_cell::sync::Lazy; +use std::sync::Mutex; + +use crate::vault::error::CryptoError; +use crate::vault::keypair::keypair_types::{KeyPair, KeySpace}; // Assuming KeyPair and KeySpace will be in keypair_types.rs + +/// Session state for the current key space and selected keypair. +pub struct Session { + pub current_space: Option, + pub selected_keypair: Option, +} + +impl Default for Session { + fn default() -> Self { + Session { + current_space: None, + selected_keypair: None, + } + } +} + +/// Global session state. +pub static SESSION: Lazy> = Lazy::new(|| Mutex::new(Session::default())); + +// Session management and selected keypair operation functions will be added here +/// Creates a new key space with the given name. +pub fn create_space(name: &str) -> Result<(), CryptoError> { + let mut session = SESSION.lock().unwrap(); + + // Create a new space + let space = KeySpace::new(name); + + // Set as current space + session.current_space = Some(space); + session.selected_keypair = None; + + Ok(()) +} + +/// Sets the current key space. +pub fn set_current_space(space: KeySpace) -> Result<(), CryptoError> { + let mut session = SESSION.lock().unwrap(); + session.current_space = Some(space); + session.selected_keypair = None; + Ok(()) +} + +/// Gets the current key space. +pub fn get_current_space() -> Result { + let session = SESSION.lock().unwrap(); + session + .current_space + .clone() + .ok_or(CryptoError::NoActiveSpace) +} + +/// Clears the current session (logout). +pub fn clear_session() { + let mut session = SESSION.lock().unwrap(); + session.current_space = None; + session.selected_keypair = None; +} + +/// Creates a new keypair in the current space. +pub fn create_keypair(name: &str) -> Result<(), CryptoError> { + let mut session = SESSION.lock().unwrap(); + + if let Some(ref mut space) = session.current_space { + if space.keypairs.contains_key(name) { + return Err(CryptoError::KeypairAlreadyExists(name.to_string())); + } + + let keypair = KeyPair::new(name); + space.keypairs.insert(name.to_string(), keypair); + + // Automatically select the new keypair + session.selected_keypair = Some(name.to_string()); + + Ok(()) + } else { + Err(CryptoError::NoActiveSpace) + } +} + +/// Selects a keypair for use. +pub fn select_keypair(name: &str) -> Result<(), CryptoError> { + let mut session = SESSION.lock().unwrap(); + + if let Some(ref space) = session.current_space { + if !space.keypairs.contains_key(name) { + return Err(CryptoError::KeypairNotFound(name.to_string())); + } + + session.selected_keypair = Some(name.to_string()); + Ok(()) + } else { + Err(CryptoError::NoActiveSpace) + } +} + +/// Gets the currently selected keypair. +pub fn get_selected_keypair() -> Result { + let session = SESSION.lock().unwrap(); + + if let Some(ref space) = session.current_space { + if let Some(ref keypair_name) = session.selected_keypair { + if let Some(keypair) = space.keypairs.get(keypair_name) { + return Ok(keypair.clone()); + } + return Err(CryptoError::KeypairNotFound(keypair_name.clone())); + } + return Err(CryptoError::NoKeypairSelected); + } + + Err(CryptoError::NoActiveSpace) +} + +/// Lists all keypair names in the current space. +pub fn list_keypairs() -> Result, CryptoError> { + let session = SESSION.lock().unwrap(); + + if let Some(ref space) = session.current_space { + Ok(space.keypairs.keys().cloned().collect()) + } else { + Err(CryptoError::NoActiveSpace) + } +} + +/// Gets the public key of the selected keypair. +pub fn keypair_pub_key() -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + Ok(keypair.pub_key()) +} + +/// Derives a public key from a private key. +pub fn derive_public_key(private_key: &[u8]) -> Result, CryptoError> { + KeyPair::pub_key_from_private(private_key) +} + +/// Signs a message with the selected keypair. +pub fn keypair_sign(message: &[u8]) -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + Ok(keypair.sign(message)) +} + +/// Verifies a message signature with the selected keypair. +pub fn keypair_verify(message: &[u8], signature_bytes: &[u8]) -> Result { + let keypair = get_selected_keypair()?; + keypair.verify(message, signature_bytes) +} + +/// Verifies a message signature with a public key. +pub fn verify_with_public_key( + public_key: &[u8], + message: &[u8], + signature_bytes: &[u8], +) -> Result { + KeyPair::verify_with_public_key(public_key, message, signature_bytes) +} + +/// Encrypts a message for a recipient using their public key. +pub fn encrypt_asymmetric( + recipient_public_key: &[u8], + message: &[u8], +) -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + keypair.encrypt_asymmetric(recipient_public_key, message) +} + +/// Decrypts a message that was encrypted with the current keypair's public key. +pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + keypair.decrypt_asymmetric(ciphertext) +} diff --git a/src/vault/keypair/tests/implementation_tests.rs b/src/vault/keypair/tests/implementation_tests.rs new file mode 100644 index 0000000..b62bb10 --- /dev/null +++ b/src/vault/keypair/tests/implementation_tests.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} \ No newline at end of file diff --git a/src/vault/keypair/tests/keypair_types_tests.rs b/src/vault/keypair/tests/keypair_types_tests.rs new file mode 100644 index 0000000..fe45775 --- /dev/null +++ b/src/vault/keypair/tests/keypair_types_tests.rs @@ -0,0 +1,86 @@ + +use crate::vault::keypair::keypair_types::{KeyPair, KeySpace}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keypair_creation() { + let keypair = KeyPair::new("test_keypair"); + assert_eq!(keypair.name, "test_keypair"); + // Basic check that keys are generated (they should have non-zero length) + assert!(!keypair.pub_key().is_empty()); + } + + #[test] + fn test_keypair_sign_and_verify() { + let keypair = KeyPair::new("test_keypair"); + let message = b"This is a test message"; + let signature = keypair.sign(message); + assert!(!signature.is_empty()); + + let is_valid = keypair.verify(message, &signature).expect("Verification failed"); + assert!(is_valid); + + // Test with a wrong message + let wrong_message = b"This is a different message"; + let is_valid_wrong = keypair.verify(wrong_message, &signature).expect("Verification failed with wrong message"); + assert!(!is_valid_wrong); + } + + #[test] + fn test_verify_with_public_key() { + let keypair = KeyPair::new("test_keypair"); + let message = b"Another test message"; + let signature = keypair.sign(message); + let public_key = keypair.pub_key(); + + let is_valid = KeyPair::verify_with_public_key(&public_key, message, &signature).expect("Verification with public key failed"); + assert!(is_valid); + + // Test with a wrong public key + let wrong_keypair = KeyPair::new("wrong_keypair"); + let wrong_public_key = wrong_keypair.pub_key(); + let is_valid_wrong_key = KeyPair::verify_with_public_key(&wrong_public_key, message, &signature).expect("Verification with wrong public key failed"); + assert!(!is_valid_wrong_key); + } + + #[test] + fn test_asymmetric_encryption_decryption() { + // Sender's keypair + let sender_keypair = KeyPair::new("sender"); + let sender_public_key = sender_keypair.pub_key(); + + // Recipient's keypair + let recipient_keypair = KeyPair::new("recipient"); + let recipient_public_key = recipient_keypair.pub_key(); + + let message = b"This is a secret message"; + + // Sender encrypts for recipient + let ciphertext = sender_keypair.encrypt_asymmetric(&recipient_public_key, message).expect("Encryption failed"); + assert!(!ciphertext.is_empty()); + + // Recipient decrypts + let decrypted_message = recipient_keypair.decrypt_asymmetric(&ciphertext).expect("Decryption failed"); + assert_eq!(decrypted_message, message); + + // Test decryption with wrong keypair + let wrong_keypair = KeyPair::new("wrong_recipient"); + let result = wrong_keypair.decrypt_asymmetric(&ciphertext); + assert!(result.is_err()); + } + + #[test] + fn test_keyspace_add_keypair() { + let mut space = KeySpace::new("test_space"); + space.add_keypair("keypair1").expect("Failed to add keypair1"); + assert_eq!(space.keypairs.len(), 1); + assert!(space.keypairs.contains_key("keypair1")); + + // Test adding a duplicate keypair + let result = space.add_keypair("keypair1"); + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/src/vault/keypair/tests/mod.rs b/src/vault/keypair/tests/mod.rs new file mode 100644 index 0000000..d24426c --- /dev/null +++ b/src/vault/keypair/tests/mod.rs @@ -0,0 +1,3 @@ +mod implementation_tests; +mod keypair_types_tests; +mod session_manager_tests; \ No newline at end of file diff --git a/src/vault/keypair/tests/session_manager_tests.rs b/src/vault/keypair/tests/session_manager_tests.rs new file mode 100644 index 0000000..416c671 --- /dev/null +++ b/src/vault/keypair/tests/session_manager_tests.rs @@ -0,0 +1,111 @@ +use crate::vault::keypair::session_manager::{ + clear_session, create_keypair, create_space, get_current_space, get_selected_keypair, + list_keypairs, select_keypair, set_current_space, SESSION, +}; +use crate::vault::keypair::keypair_types::KeySpace; + +// Helper function to clear the session before each test +fn setup_test() { + clear_session(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_and_get_space() { + setup_test(); + create_space("test_space").expect("Failed to create space"); + let space = get_current_space().expect("Failed to get current space"); + assert_eq!(space.name, "test_space"); + } + + #[test] + fn test_set_current_space() { + setup_test(); + let space = KeySpace::new("another_space"); + set_current_space(space.clone()).expect("Failed to set current space"); + let current_space = get_current_space().expect("Failed to get current space"); + assert_eq!(current_space.name, "another_space"); + } + + #[test] + fn test_clear_session() { + setup_test(); + create_space("test_space").expect("Failed to create space"); + clear_session(); + let result = get_current_space(); + assert!(result.is_err()); + } + + #[test] + fn test_create_and_select_keypair() { + setup_test(); + create_space("test_space").expect("Failed to create space"); + create_keypair("test_keypair").expect("Failed to create keypair"); + let keypair = get_selected_keypair().expect("Failed to get selected keypair"); + assert_eq!(keypair.name, "test_keypair"); + + select_keypair("test_keypair").expect("Failed to select keypair"); + let selected_keypair = get_selected_keypair().expect("Failed to get selected keypair after select"); + assert_eq!(selected_keypair.name, "test_keypair"); + } + + #[test] + fn test_list_keypairs() { + setup_test(); + create_space("test_space").expect("Failed to create space"); + create_keypair("keypair1").expect("Failed to create keypair1"); + create_keypair("keypair2").expect("Failed to create keypair2"); + + let keypairs = list_keypairs().expect("Failed to list keypairs"); + assert_eq!(keypairs.len(), 2); + assert!(keypairs.contains(&"keypair1".to_string())); + assert!(keypairs.contains(&"keypair2".to_string())); + } + + #[test] + fn test_create_keypair_no_active_space() { + setup_test(); + let result = create_keypair("test_keypair"); + assert!(result.is_err()); + } + + #[test] + fn test_select_keypair_no_active_space() { + setup_test(); + let result = select_keypair("test_keypair"); + assert!(result.is_err()); + } + + #[test] + fn test_select_nonexistent_keypair() { + setup_test(); + create_space("test_space").expect("Failed to create space"); + let result = select_keypair("nonexistent_keypair"); + assert!(result.is_err()); + } + + #[test] + fn test_get_selected_keypair_no_active_space() { + setup_test(); + let result = get_selected_keypair(); + assert!(result.is_err()); + } + + #[test] + fn test_get_selected_keypair_no_keypair_selected() { + setup_test(); + create_space("test_space").expect("Failed to create space"); + let result = get_selected_keypair(); + assert!(result.is_err()); + } + + #[test] + fn test_list_keypairs_no_active_space() { + setup_test(); + let result = list_keypairs(); + assert!(result.is_err()); + } +} diff --git a/src/hero_vault/kvs/README.md b/src/vault/kvs/README.md similarity index 98% rename from src/hero_vault/kvs/README.md rename to src/vault/kvs/README.md index 89d6d2e..431a24c 100644 --- a/src/hero_vault/kvs/README.md +++ b/src/vault/kvs/README.md @@ -165,3 +165,9 @@ let loaded_store = KvStore::load("my_store", "secure_password")?; let api_key = loaded_store.get("api_key")?; println!("API Key: {}", api_key.unwrap_or_default()); ``` + +## to test + +```bash +cargo test --lib vault::keypair +``` \ No newline at end of file diff --git a/src/hero_vault/kvs/error.rs b/src/vault/kvs/error.rs similarity index 66% rename from src/hero_vault/kvs/error.rs rename to src/vault/kvs/error.rs index 039644e..bbd6eaf 100644 --- a/src/hero_vault/kvs/error.rs +++ b/src/vault/kvs/error.rs @@ -1,6 +1,5 @@ //! Error types for the key-value store. -use std::fmt; use thiserror::Error; /// Errors that can occur when using the key-value store. @@ -9,31 +8,31 @@ pub enum KvsError { /// I/O error #[error("I/O error: {0}")] Io(#[from] std::io::Error), - + /// Key not found #[error("Key not found: {0}")] KeyNotFound(String), - + /// Store not found #[error("Store not found: {0}")] StoreNotFound(String), - + /// Serialization error #[error("Serialization error: {0}")] Serialization(String), - + /// Deserialization error #[error("Deserialization error: {0}")] Deserialization(String), - + /// Encryption error #[error("Encryption error: {0}")] Encryption(String), - + /// Decryption error #[error("Decryption error: {0}")] Decryption(String), - + /// Other error #[error("Error: {0}")] Other(String), @@ -45,18 +44,20 @@ impl From for KvsError { } } -impl From for crate::hero_vault::error::CryptoError { +impl From for crate::vault::error::CryptoError { fn from(err: KvsError) -> Self { - crate::hero_vault::error::CryptoError::SerializationError(err.to_string()) + crate::vault::error::CryptoError::SerializationError(err.to_string()) } } -impl From for KvsError { - fn from(err: crate::hero_vault::error::CryptoError) -> Self { +impl From for KvsError { + fn from(err: crate::vault::error::CryptoError) -> Self { match err { - crate::hero_vault::error::CryptoError::EncryptionFailed(msg) => KvsError::Encryption(msg), - crate::hero_vault::error::CryptoError::DecryptionFailed(msg) => KvsError::Decryption(msg), - crate::hero_vault::error::CryptoError::SerializationError(msg) => KvsError::Serialization(msg), + crate::vault::error::CryptoError::EncryptionFailed(msg) => KvsError::Encryption(msg), + crate::vault::error::CryptoError::DecryptionFailed(msg) => KvsError::Decryption(msg), + crate::vault::error::CryptoError::SerializationError(msg) => { + KvsError::Serialization(msg) + } _ => KvsError::Other(err.to_string()), } } diff --git a/src/hero_vault/kvs/indexed_db_store.rs b/src/vault/kvs/indexed_db_store.rs similarity index 100% rename from src/hero_vault/kvs/indexed_db_store.rs rename to src/vault/kvs/indexed_db_store.rs diff --git a/src/hero_vault/kvs/key_space.rs b/src/vault/kvs/key_space.rs similarity index 100% rename from src/hero_vault/kvs/key_space.rs rename to src/vault/kvs/key_space.rs diff --git a/src/hero_vault/kvs/mod.rs b/src/vault/kvs/mod.rs similarity index 100% rename from src/hero_vault/kvs/mod.rs rename to src/vault/kvs/mod.rs diff --git a/src/hero_vault/kvs/slate_store.rs b/src/vault/kvs/slate_store.rs similarity index 100% rename from src/hero_vault/kvs/slate_store.rs rename to src/vault/kvs/slate_store.rs diff --git a/src/hero_vault/kvs/store.rs b/src/vault/kvs/store.rs similarity index 90% rename from src/hero_vault/kvs/store.rs rename to src/vault/kvs/store.rs index 9b8c554..ed1df9f 100644 --- a/src/hero_vault/kvs/store.rs +++ b/src/vault/kvs/store.rs @@ -1,11 +1,13 @@ //! Implementation of a simple key-value store using the filesystem. -use crate::hero_vault::kvs::error::{KvsError, Result}; -use crate::hero_vault::symmetric; +use crate::vault::kvs::error::{KvsError, Result}; +use crate::vault::symmetric::implementation::{ + decrypt_symmetric, derive_key_from_password, encrypt_symmetric, +}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::collections::HashMap; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; /// A key-value pair. @@ -92,7 +94,9 @@ pub fn get_store_path() -> PathBuf { pub fn create_store(name: &str, encrypted: bool, password: Option<&str>) -> Result { // Check if password is provided when encryption is enabled if encrypted && password.is_none() { - return Err(KvsError::Other("Password required for encrypted store".to_string())); + return Err(KvsError::Other( + "Password required for encrypted store".to_string(), + )); } // Create the store directory if it doesn't exist @@ -147,7 +151,9 @@ pub fn open_store(name: &str, password: Option<&str>) -> Result { // If encrypted, we need a password if is_encrypted && password.is_none() { - return Err(KvsError::Other("Password required for encrypted store".to_string())); + return Err(KvsError::Other( + "Password required for encrypted store".to_string(), + )); } // Parse the store data @@ -155,8 +161,8 @@ pub fn open_store(name: &str, password: Option<&str>) -> Result { // Decrypt the file content let password = password.unwrap(); let encrypted_data: Vec = serde_json::from_str(&file_content)?; - let key = symmetric::derive_key_from_password(password); - let decrypted_data = symmetric::decrypt_symmetric(&key, &encrypted_data)?; + let key = derive_key_from_password(password); + let decrypted_data = decrypt_symmetric(&key, &encrypted_data)?; let decrypted_str = String::from_utf8(decrypted_data) .map_err(|e| KvsError::Deserialization(e.to_string()))?; serde_json::from_str(&decrypted_str)? @@ -243,12 +249,14 @@ impl KvStore { if self.encrypted { if let Some(password) = &self.password { // Encrypt the data - let key = symmetric::derive_key_from_password(password); - let encrypted_data = symmetric::encrypt_symmetric(&key, serialized.as_bytes())?; + let key = derive_key_from_password(password); + let encrypted_data = encrypt_symmetric(&key, serialized.as_bytes())?; let encrypted_json = serde_json::to_string(&encrypted_data)?; fs::write(&self.path, encrypted_json)?; } else { - return Err(KvsError::Other("Password required for encrypted store".to_string())); + return Err(KvsError::Other( + "Password required for encrypted store".to_string(), + )); } } else { fs::write(&self.path, serialized)?; @@ -268,16 +276,16 @@ impl KVStore for KvStore { { let key_str = key.to_string(); let serialized = serde_json::to_string(value)?; - + // Update in-memory data { let mut data = self.data.lock().unwrap(); data.insert(key_str, serialized); } - + // Save to disk self.save()?; - + Ok(()) } @@ -289,12 +297,12 @@ impl KVStore for KvStore { { let key_str = key.to_string(); let data = self.data.lock().unwrap(); - + match data.get(&key_str) { Some(serialized) => { - let value = serde_json::from_str(serialized)?; + let value: V = serde_json::from_str(serialized)?; Ok(value) - }, + } None => Err(KvsError::KeyNotFound(key_str)), } } @@ -305,7 +313,7 @@ impl KVStore for KvStore { K: ToString, { let key_str = key.to_string(); - + // Update in-memory data { let mut data = self.data.lock().unwrap(); @@ -313,10 +321,10 @@ impl KVStore for KvStore { return Err(KvsError::KeyNotFound(key_str)); } } - + // Save to disk self.save()?; - + Ok(()) } @@ -327,14 +335,14 @@ impl KVStore for KvStore { { let key_str = key.to_string(); let data = self.data.lock().unwrap(); - + Ok(data.contains_key(&key_str)) } /// Lists all keys in the store. fn keys(&self) -> Result> { let data = self.data.lock().unwrap(); - + Ok(data.keys().cloned().collect()) } @@ -345,11 +353,11 @@ impl KVStore for KvStore { let mut data = self.data.lock().unwrap(); data.clear(); } - + // Save to disk self.save()?; - - Ok(()) + + Ok(()) } /// Gets the name of the store. diff --git a/src/vault/kvs/tests/mod.rs b/src/vault/kvs/tests/mod.rs new file mode 100644 index 0000000..668dbed --- /dev/null +++ b/src/vault/kvs/tests/mod.rs @@ -0,0 +1 @@ +mod store_tests; \ No newline at end of file diff --git a/src/vault/kvs/tests/store_tests.rs b/src/vault/kvs/tests/store_tests.rs new file mode 100644 index 0000000..5a972bf --- /dev/null +++ b/src/vault/kvs/tests/store_tests.rs @@ -0,0 +1,105 @@ +use crate::vault::kvs::store::{create_store, delete_store, open_store, KvStore}; +use std::path::PathBuf; + +// Helper function to generate a unique store name for each test +fn generate_test_store_name() -> String { + use rand::Rng; + let random_string: String = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(10) + .map(char::from) + .collect(); + format!("test_store_{}", random_string) +} + +// Helper function to clean up test stores +fn cleanup_test_store(name: &str) { + let _ = delete_store(name); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_and_open_store() { + let store_name = generate_test_store_name(); + let store = create_store(&store_name, false, None).expect("Failed to create store"); + assert_eq!(store.name(), store_name); + assert!(!store.is_encrypted()); + + let opened_store = open_store(&store_name, None).expect("Failed to open store"); + assert_eq!(opened_store.name(), store_name); + assert!(!opened_store.is_encrypted()); + + cleanup_test_store(&store_name); + } + + #[test] + fn test_set_and_get_value() { + let store_name = generate_test_store_name(); + let store = create_store(&store_name, false, None).expect("Failed to create store"); + + store.set("key1", &"value1").expect("Failed to set value"); + let value: String = store.get("key1").expect("Failed to get value"); + assert_eq!(value, "value1"); + + cleanup_test_store(&store_name); + } + + #[test] + fn test_delete_value() { + let store_name = generate_test_store_name(); + let store = create_store(&store_name, false, None).expect("Failed to create store"); + + store.set("key1", &"value1").expect("Failed to set value"); + store.delete("key1").expect("Failed to delete value"); + let result: Result = store.get("key1"); + assert!(result.is_err()); + + cleanup_test_store(&store_name); + } + + #[test] + fn test_contains_key() { + let store_name = generate_test_store_name(); + let store = create_store(&store_name, false, None).expect("Failed to create store"); + + store.set("key1", &"value1").expect("Failed to set value"); + assert!(store.contains("key1").expect("Failed to check contains")); + assert!(!store.contains("key2").expect("Failed to check contains")); + + cleanup_test_store(&store_name); + } + + #[test] + fn test_list_keys() { + let store_name = generate_test_store_name(); + let store = create_store(&store_name, false, None).expect("Failed to create store"); + + store.set("key1", &"value1").expect("Failed to set value"); + store.set("key2", &"value2").expect("Failed to set value"); + + let keys = store.keys().expect("Failed to list keys"); + assert_eq!(keys.len(), 2); + assert!(keys.contains(&"key1".to_string())); + assert!(keys.contains(&"key2".to_string())); + + cleanup_test_store(&store_name); + } + + #[test] + fn test_clear_store() { + let store_name = generate_test_store_name(); + let store = create_store(&store_name, false, None).expect("Failed to create store"); + + store.set("key1", &"value1").expect("Failed to set value"); + store.set("key2", &"value2").expect("Failed to set value"); + + store.clear().expect("Failed to clear store"); + let keys = store.keys().expect("Failed to list keys after clear"); + assert!(keys.is_empty()); + + cleanup_test_store(&store_name); + } +} \ No newline at end of file diff --git a/src/hero_vault/mod.rs b/src/vault/mod.rs similarity index 96% rename from src/hero_vault/mod.rs rename to src/vault/mod.rs index 0a301ca..130333c 100644 --- a/src/hero_vault/mod.rs +++ b/src/vault/mod.rs @@ -14,6 +14,7 @@ pub mod symmetric; pub mod ethereum; pub mod kvs; +// Re-export modules // Re-export common types for convenience pub use error::CryptoError; pub use keypair::{KeyPair, KeySpace}; diff --git a/src/hero_vault/symmetric/README.md b/src/vault/symmetric/README.md similarity index 100% rename from src/hero_vault/symmetric/README.md rename to src/vault/symmetric/README.md diff --git a/src/hero_vault/symmetric/implementation.rs b/src/vault/symmetric/implementation.rs similarity index 98% rename from src/hero_vault/symmetric/implementation.rs rename to src/vault/symmetric/implementation.rs index 476470e..3b201d7 100644 --- a/src/hero_vault/symmetric/implementation.rs +++ b/src/vault/symmetric/implementation.rs @@ -6,8 +6,8 @@ use rand::{rngs::OsRng, RngCore}; use serde::{Serialize, Deserialize}; use sha2::{Sha256, Digest}; -use crate::hero_vault::error::CryptoError; -use crate::hero_vault::keypair::KeySpace; +use crate::vault::error::CryptoError; +use crate::vault::keypair::KeySpace; /// The size of the nonce in bytes. const NONCE_SIZE: usize = 12; diff --git a/src/hero_vault/symmetric/mod.rs b/src/vault/symmetric/mod.rs similarity index 95% rename from src/hero_vault/symmetric/mod.rs rename to src/vault/symmetric/mod.rs index 5b2f24a..1d63e3e 100644 --- a/src/hero_vault/symmetric/mod.rs +++ b/src/vault/symmetric/mod.rs @@ -2,7 +2,7 @@ //! //! This module provides functionality for symmetric encryption using ChaCha20Poly1305. -mod implementation; +pub mod implementation; // Re-export public types and functions pub use implementation::{ diff --git a/src/zinit_client/mod.rs b/src/zinit_client/mod.rs new file mode 100644 index 0000000..deb32dd --- /dev/null +++ b/src/zinit_client/mod.rs @@ -0,0 +1,203 @@ +use std::sync::{Arc, Mutex, Once}; +use std::sync::atomic::{AtomicBool, Ordering}; +use lazy_static::lazy_static; +use zinit_client::{Client as ZinitClient, ClientError, Status}; +use std::collections::HashMap; +use serde_json::{Map, Value}; + +// Global Zinit client instance using lazy_static +lazy_static! { + static ref ZINIT_CLIENT: Mutex>> = Mutex::new(None); + static ref INIT: Once = Once::new(); +} + +// Wrapper for Zinit client to handle connection +pub struct ZinitClientWrapper { + client: ZinitClient, + initialized: AtomicBool, +} + +impl ZinitClientWrapper { + // Create a new Zinit client wrapper + fn new(client: ZinitClient) -> Self { + ZinitClientWrapper { + client, + initialized: AtomicBool::new(false), + } + } + + // Initialize the client + async fn initialize(&self) -> Result<(), ClientError> { + if self.initialized.load(Ordering::Relaxed) { + return Ok(()); + } + + // Try to list services to check if the connection works + let _ = self.client.list().await.map_err(|e| { + eprintln!("Failed to initialize Zinit client: {}", e); + e + })?; + + self.initialized.store(true, Ordering::Relaxed); + Ok(()) + } + + // List all services + pub async fn list(&self) -> Result, ClientError> { + self.client.list().await + } + + // Get status of a service + pub async fn status(&self, name: &str) -> Result { + self.client.status(name).await + } + + // Start a service + pub async fn start(&self, name: &str) -> Result<(), ClientError> { + self.client.start(name).await + } + + // Stop a service + pub async fn stop(&self, name: &str) -> Result<(), ClientError> { + self.client.stop(name).await + } + + // Restart a service + pub async fn restart(&self, name: &str) -> Result<(), ClientError> { + self.client.restart(name).await + } + + // Monitor a service + pub async fn monitor(&self, name: &str) -> Result<(), ClientError> { + self.client.monitor(name).await + } + + // Forget a service + pub async fn forget(&self, name: &str) -> Result<(), ClientError> { + self.client.forget(name).await + } + + // Send a signal to a service + pub async fn kill(&self, name: &str, signal: &str) -> Result<(), ClientError> { + self.client.kill(name, signal).await + } + + // Create a new service + pub async fn create_service(&self, name: &str, content: Map) -> Result { + self.client.create_service(name, content).await + } + + // Delete a service + pub async fn delete_service(&self, name: &str) -> Result { + self.client.delete_service(name).await + } + + // Get a service configuration + pub async fn get_service(&self, name: &str) -> Result { + self.client.get_service(name).await + } + + // Shutdown the system + pub async fn shutdown(&self) -> Result<(), ClientError> { + self.client.shutdown().await + } + + // Reboot the system + pub async fn reboot(&self) -> Result<(), ClientError> { + self.client.reboot().await + } + + // Start HTTP server + pub async fn start_http_server(&self, address: &str) -> Result { + self.client.start_http_server(address).await + } + + // Stop HTTP server + pub async fn stop_http_server(&self) -> Result<(), ClientError> { + self.client.stop_http_server().await + } + + // Get logs + pub async fn logs(&self, filter: Option) -> Result, ClientError> { + self.client.logs(filter).await + } +} + +// Get the Zinit client instance +pub async fn get_zinit_client(socket_path: &str) -> Result, ClientError> { + // Check if we already have a client + { + let guard = ZINIT_CLIENT.lock().unwrap(); + if let Some(ref client) = &*guard { + return Ok(Arc::clone(client)); + } + } + + // Create a new client + let client = create_zinit_client(socket_path).await?; + + // Store the client globally + { + let mut guard = ZINIT_CLIENT.lock().unwrap(); + *guard = Some(Arc::clone(&client)); + } + + Ok(client) +} + +// Create a new Zinit client +async fn create_zinit_client(socket_path: &str) -> Result, ClientError> { + // Connect via Unix socket + let client = ZinitClient::unix_socket(socket_path).await?; + let wrapper = Arc::new(ZinitClientWrapper::new(client)); + + // Initialize the client + wrapper.initialize().await?; + + Ok(wrapper) +} + +// Reset the Zinit client +pub async fn reset(socket_path: &str) -> Result<(), ClientError> { + // Clear the existing client + { + let mut client_guard = ZINIT_CLIENT.lock().unwrap(); + *client_guard = None; + } + + // Create a new client, only return error if it fails + get_zinit_client(socket_path).await?; + Ok(()) +} + +// Convenience functions for common operations + +// List all services +pub async fn list(socket_path: &str) -> Result, ClientError> { + let client = get_zinit_client(socket_path).await?; + client.list().await +} + +// Get status of a service +pub async fn status(socket_path: &str, name: &str) -> Result { + let client = get_zinit_client(socket_path).await?; + client.status(name).await +} + +// Start a service +pub async fn start(socket_path: &str, name: &str) -> Result<(), ClientError> { + let client = get_zinit_client(socket_path).await?; + client.start(name).await +} + +// Stop a service +pub async fn stop(socket_path: &str, name: &str) -> Result<(), ClientError> { + let client = get_zinit_client(socket_path).await?; + client.stop(name).await +} + +// Restart a service +pub async fn restart(socket_path: &str, name: &str) -> Result<(), ClientError> { + let client = get_zinit_client(socket_path).await?; + client.restart(name).await +} \ No newline at end of file