Merge branch 'main' into development_hero_vault

* main:
  restore
  tests & fixes in kvs & keypair
  feat: Add comprehensive test suite for Keypair module
  refactor: Improve Rhai test runner and vault module code
  ...
  ...
  ...
  feat: Enhance documentation and add .gitignore entries
  working example to showcase zinit usage in Rhai scripts
  implemented zinit-client for integration with Rhai-scripts
This commit is contained in:
despiegk 2025-05-13 07:00:25 +03:00
commit 9f263b6ec4
131 changed files with 3307 additions and 830 deletions

39
.gitignore vendored
View File

@ -23,3 +23,42 @@ Cargo.lock
/rhai_test_download /rhai_test_download
/rhai_test_fs /rhai_test_fs
run_rhai_tests.log 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

View File

@ -11,24 +11,36 @@ categories = ["os", "filesystem", "api-bindings"]
readme = "README.md" readme = "README.md"
[dependencies] [dependencies]
tera = "1.19.0" # Template engine for text rendering anyhow = "1.0.98"
# Cross-platform functionality base64 = "0.21.0" # Base64 encoding/decoding
libc = "0.2"
cfg-if = "1.0" cfg-if = "1.0"
thiserror = "1.0" # For error handling chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher
redis = "0.22.0" # Redis client clap = "2.33" # Command-line argument parsing
postgres = "0.19.4" # PostgreSQL client dirs = "5.0.1" # Directory paths
tokio-postgres = "0.7.8" # Async PostgreSQL client env_logger = "0.10.0" # Logger implementation
postgres-types = "0.2.5" # PostgreSQL type conversions 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 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 regex = "1.8.1" # For regex pattern matching
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
serde = { version = "1.0", features = [ serde = { version = "1.0", features = [
"derive", "derive",
] } # For serialization/deserialization ] } # For serialization/deserialization
serde_json = "1.0" # For JSON handling 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 tempfile = "3.5" # For temporary file operations
log = "0.4" # Logging facade log = "0.4" # Logging facade
env_logger = "0.10.0" # Logger implementation 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 clap = "2.33" # Command-line argument parsing
r2d2 = "0.8.10" r2d2 = "0.8.10"
r2d2_postgres = "0.18.2" 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 # Crypto dependencies
base64 = "0.21.0" # Base64 encoding/decoding base64 = "0.21.0" # Base64 encoding/decoding
@ -53,26 +62,6 @@ tokio = { version = "1.28", features = ["full"] }
uuid = { version = "1.16.0", features = ["v4"] } uuid = { version = "1.16.0", features = ["v4"] }
tokio-test = "0.4.4" 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 # Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
nix = "0.26" # Unix-specific functionality nix = "0.26" # Unix-specific functionality
@ -85,9 +74,9 @@ windows = { version = "0.48", features = [
] } ] }
[dev-dependencies] [dev-dependencies]
mockall = "0.11.4" # For mocking in tests
tempfile = "3.5" # For tests that need temporary files/directories tempfile = "3.5" # For tests that need temporary files/directories
tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing
mockall = "0.11.4" # For mocking in tests
[[bin]] [[bin]]
name = "herodo" name = "herodo"

View File

@ -2,7 +2,7 @@
// Demonstrates file system operations using SAL // Demonstrates file system operations using SAL
// Create a test directory // Create a test directory
let test_dir = "rhai_test_dir"; let test_dir = "/tmp/rhai_test_dir";
println(`Creating directory: ${test_dir}`); println(`Creating directory: ${test_dir}`);
let mkdir_result = mkdir(test_dir); let mkdir_result = mkdir(test_dir);
println(`Directory creation result: ${mkdir_result}`); println(`Directory creation result: ${mkdir_result}`);

View File

@ -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}`);
}

View File

@ -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!");

View File

@ -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!");

View File

@ -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!");

View File

@ -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!");

View File

@ -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!");

View File

@ -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;

View File

@ -24,7 +24,7 @@ log "${BLUE} Running All Rhai Tests ${NC}"
log "${BLUE}=======================================${NC}" log "${BLUE}=======================================${NC}"
# Find all test runner scripts # 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 # Initialize counters
TOTAL_MODULES=0 TOTAL_MODULES=0

34
src/docs/.gitignore vendored
View File

@ -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

View File

@ -46,7 +46,8 @@ pub mod redisclient;
pub mod rhai; pub mod rhai;
pub mod text; pub mod text;
pub mod virt; pub mod virt;
pub mod hero_vault; pub mod vault;
pub mod zinit_client;
// Version information // Version information
/// Returns the version of the SAL library /// Returns the version of the SAL library

View File

@ -1,9 +1,9 @@
use std::process::Command;
use std::path::Path;
use std::fs;
use std::fmt;
use std::error::Error; use std::error::Error;
use std::fmt;
use std::fs;
use std::io; use std::io;
use std::path::Path;
use std::process::Command;
// Define a custom error type for download operations // Define a custom error type for download operations
#[derive(Debug)] #[derive(Debug)]
@ -26,11 +26,17 @@ pub enum DownloadError {
impl fmt::Display for DownloadError { impl fmt::Display for DownloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { 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::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e),
DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url), DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url),
DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e), 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::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e),
DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e), DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e),
DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e), DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e),
@ -74,12 +80,18 @@ impl Error for DownloadError {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::download;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download a file with no minimum size requirement * // Download a file with no minimum size requirement
* let path = download("https://example.com/file.txt", "/tmp/", 0)?; * let path = download("https://example.com/file.txt", "/tmp/", 0)?;
* *
* // Download a file with minimum size requirement of 100KB * // Download a file with minimum size requirement of 100KB
* let path = download("https://example.com/file.zip", "/tmp/", 100)?; * let path = download("https://example.com/file.zip", "/tmp/", 100)?;
*
* Ok(())
* }
* ``` * ```
* *
* # Notes * # Notes
@ -95,7 +107,11 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
// Extract filename from URL // Extract filename from URL
let filename = match url.split('/').last() { let filename = match url.split('/').last() {
Some(name) => name, Some(name) => 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 // Create a full path for the downloaded file
@ -107,7 +123,14 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
// Use curl to download the file with progress bar // Use curl to download the file with progress bar
println!("Downloading {} to {}", url, file_path); println!("Downloading {} to {}", url, file_path);
let output = Command::new("curl") let output = Command::new("curl")
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) .args(&[
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.status() .status()
.map_err(DownloadError::CurlExecutionFailed)?; .map_err(DownloadError::CurlExecutionFailed)?;
@ -122,11 +145,17 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
let size_kb = size_bytes / 1024; let size_kb = size_bytes / 1024;
let size_mb = size_kb / 1024; let size_mb = size_kb / 1024;
if size_mb > 1 { if size_mb > 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 { } 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!"), Err(_) => println!("Download complete!"),
} }
@ -142,10 +171,10 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
// Check if it's a compressed file that needs extraction // Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase(); let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz") || let is_archive = lower_url.ends_with(".tar.gz")
lower_url.ends_with(".tgz") || || lower_url.ends_with(".tgz")
lower_url.ends_with(".tar") || || lower_url.ends_with(".tar")
lower_url.ends_with(".zip"); || lower_url.ends_with(".zip");
if is_archive { if is_archive {
// Extract the file using the appropriate command with progress indication // Extract the file using the appropriate command with progress indication
@ -167,9 +196,11 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
match output { match output {
Ok(status) => { Ok(status) => {
if !status.success() { 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)), Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
} }
@ -178,7 +209,7 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
Ok(entries) => { Ok(entries) => {
let count = entries.count(); let count = entries.count();
println!("Extraction complete! Extracted {} files/directories", count); println!("Extraction complete! Extracted {} files/directories", count);
}, }
Err(_) => println!("Extraction complete!"), Err(_) => println!("Extraction complete!"),
} }
@ -210,12 +241,18 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::download_file;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download a file with no minimum size requirement * // Download a file with no minimum size requirement
* let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?; * let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
* *
* // Download a file with minimum size requirement of 100KB * // Download a file with minimum size requirement of 100KB
* let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?; * 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<String, DownloadError> { pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
@ -231,7 +268,14 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
// Use curl to download the file with progress bar // Use curl to download the file with progress bar
println!("Downloading {} to {}", url, dest); println!("Downloading {} to {}", url, dest);
let output = Command::new("curl") let output = Command::new("curl")
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) .args(&[
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.status() .status()
.map_err(DownloadError::CurlExecutionFailed)?; .map_err(DownloadError::CurlExecutionFailed)?;
@ -246,11 +290,17 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
let size_kb = size_bytes / 1024; let size_kb = size_bytes / 1024;
let size_mb = size_kb / 1024; let size_mb = size_kb / 1024;
if size_mb > 1 { if size_mb > 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 { } 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!"), Err(_) => println!("Download complete!"),
} }
@ -284,9 +334,14 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::chmod_exec;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Make a file executable * // Make a file executable
* chmod_exec("/path/to/file")?; * chmod_exec("/path/to/file")?;
* Ok(())
* }
* ``` * ```
*/ */
pub fn chmod_exec(path: &str) -> Result<String, DownloadError> { pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
@ -294,11 +349,17 @@ pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
// Check if the path exists and is a file // Check if the path exists and is a file
if !path_obj.exists() { 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() { 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 // Get current permissions
@ -319,16 +380,19 @@ pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
{ {
// On non-Unix platforms, we can't set executable bit directly // On non-Unix platforms, we can't set executable bit directly
// Just return success with a warning // 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 // 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( DownloadError::CommandExecutionFailed(io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
format!("Failed to set executable permissions: {}", e) format!("Failed to set executable permissions: {}", e),
)) ))
)?; })?;
Ok(format!("Made {} executable", path)) Ok(format!("Made {} executable", path))
} }
@ -348,9 +412,14 @@ pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::download_install;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download and install a .deb package * // Download and install a .deb package
* let result = download_install("https://example.com/package.deb", 100)?; * let result = download_install("https://example.com/package.deb", 100)?;
* Ok(())
* }
* ``` * ```
* *
* # Notes * # Notes
@ -362,7 +431,11 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
// Extract filename from URL // Extract filename from URL
let filename = match url.split('/').last() { let filename = match url.split('/').last() {
Some(name) => name, Some(name) => 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 // Create a proper destination path
@ -370,10 +443,10 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
// Check if it's a compressed file that needs extraction // Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase(); let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz") || let is_archive = lower_url.ends_with(".tar.gz")
lower_url.ends_with(".tgz") || || lower_url.ends_with(".tgz")
lower_url.ends_with(".tar") || || lower_url.ends_with(".tar")
lower_url.ends_with(".zip"); || lower_url.ends_with(".zip");
let download_result = if is_archive { let download_result = if is_archive {
// For archives, use the directory-based download function // For archives, use the directory-based download function
@ -401,13 +474,15 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
Ok(status) => { Ok(status) => {
if !status.success() { if !status.success() {
return Err(DownloadError::PlatformNotSupported( 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( Err(_) => {
"Failed to check system compatibility for .deb installation".to_string() return Err(DownloadError::PlatformNotSupported(
)), "Failed to check system compatibility for .deb installation".to_string(),
))
}
} }
// Install the .deb package non-interactively // Install the .deb package non-interactively
@ -428,17 +503,17 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
if let Ok(fix_status) = fix_deps { if let Ok(fix_status) = fix_deps {
if !fix_status.success() { if !fix_status.success() {
return Err(DownloadError::InstallationFailed( return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string() "Failed to resolve package dependencies".to_string(),
)); ));
} }
} else { } else {
return Err(DownloadError::InstallationFailed( return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string() "Failed to resolve package dependencies".to_string(),
)); ));
} }
} }
println!("Package installation completed successfully"); println!("Package installation completed successfully");
}, }
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
} }
} }

View File

@ -1,9 +1,9 @@
use std::error::Error;
use std::fmt;
use std::fs; use std::fs;
use std::io;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::fmt;
use std::error::Error;
use std::io;
// Define a custom error type for file system operations // Define a custom error type for file system operations
#[derive(Debug)] #[derive(Debug)]
@ -33,14 +33,18 @@ impl fmt::Display for FsError {
match self { match self {
FsError::DirectoryNotFound(dir) => write!(f, "Directory '{}' does not exist", dir), FsError::DirectoryNotFound(dir) => write!(f, "Directory '{}' does not exist", dir),
FsError::FileNotFound(pattern) => write!(f, "No files found matching '{}'", pattern), 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::CopyFailed(e) => write!(f, "Failed to copy file: {}", e),
FsError::DeleteFailed(e) => write!(f, "Failed to delete: {}", e), FsError::DeleteFailed(e) => write!(f, "Failed to delete: {}", e),
FsError::CommandFailed(e) => write!(f, "{}", e), FsError::CommandFailed(e) => write!(f, "{}", e),
FsError::CommandNotFound(e) => write!(f, "Command not found: {}", e), FsError::CommandNotFound(e) => write!(f, "Command not found: {}", e),
FsError::CommandExecutionError(e) => write!(f, "Failed to execute command: {}", e), FsError::CommandExecutionError(e) => write!(f, "Failed to execute command: {}", e),
FsError::InvalidGlobPattern(e) => write!(f, "Invalid glob pattern: {}", 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::NotAFile(path) => write!(f, "Path '{}' is not a regular file", path),
FsError::UnknownFileType(path) => write!(f, "Unknown file type at '{}'", path), FsError::UnknownFileType(path) => write!(f, "Unknown file type at '{}'", path),
FsError::MetadataError(e) => write!(f, "Failed to get file metadata: {}", e), FsError::MetadataError(e) => write!(f, "Failed to get file metadata: {}", e),
@ -86,7 +90,10 @@ impl Error for FsError {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::copy;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Copy a single file * // Copy a single file
* let result = copy("file.txt", "backup/file.txt")?; * let result = copy("file.txt", "backup/file.txt")?;
* *
@ -95,6 +102,9 @@ impl Error for FsError {
* *
* // Copy a directory recursively * // Copy a directory recursively
* let result = copy("src_dir", "dest_dir")?; * let result = copy("src_dir", "dest_dir")?;
*
* Ok(())
* }
* ``` * ```
*/ */
pub fn copy(src: &str, dest: &str) -> Result<String, FsError> { pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
@ -110,9 +120,7 @@ pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
// Use glob to expand wildcards // Use glob to expand wildcards
let entries = glob::glob(src).map_err(FsError::InvalidGlobPattern)?; let entries = glob::glob(src).map_err(FsError::InvalidGlobPattern)?;
let paths: Vec<_> = entries let paths: Vec<_> = entries.filter_map(Result::ok).collect();
.filter_map(Result::ok)
.collect();
if paths.is_empty() { if paths.is_empty() {
return Err(FsError::FileNotFound(src.to_string())); return Err(FsError::FileNotFound(src.to_string()));
@ -150,16 +158,23 @@ pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
// For directories, use platform-specific command // For directories, use platform-specific command
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let output = Command::new("xcopy") let output = Command::new("xcopy")
.args(&["/E", "/I", "/H", "/Y", .args(&[
"/E",
"/I",
"/H",
"/Y",
&path.to_string_lossy(), &path.to_string_lossy(),
&target_path.to_string_lossy()]) &target_path.to_string_lossy(),
])
.status(); .status();
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let output = Command::new("cp") let output = Command::new("cp")
.args(&["-R", .args(&[
"-R",
&path.to_string_lossy(), &path.to_string_lossy(),
&target_path.to_string_lossy()]) &target_path.to_string_lossy(),
])
.status(); .status();
match output { match output {
@ -167,17 +182,26 @@ pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
if status.success() { if status.success() {
success_count += 1; 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 { if success_count > 0 {
Ok(format!("Successfully copied {} items from '{}' to '{}'", Ok(format!(
success_count, src, dest)) "Successfully copied {} items from '{}' to '{}'",
success_count, src, dest
))
} else { } 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 { } else {
// Handle non-wildcard paths normally // Handle non-wildcard paths normally
@ -200,7 +224,12 @@ pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
let file_name = src_path.file_name().unwrap_or_default(); let file_name = src_path.file_name().unwrap_or_default();
let new_dest_path = dest_path.join(file_name); let new_dest_path = dest_path.join(file_name);
fs::copy(src_path, new_dest_path).map_err(FsError::CopyFailed)?; 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 { } else {
// Otherwise copy file to the specified destination // Otherwise copy file to the specified destination
fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?; fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?;
@ -214,19 +243,23 @@ pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
.output(); .output();
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let output = Command::new("cp") let output = Command::new("cp").args(&["-R", src, dest]).output();
.args(&["-R", src, dest])
.output();
match output { match output {
Ok(out) => { Ok(out) => {
if out.status.success() { if out.status.success() {
Ok(format!("Successfully copied directory '{}' to '{}'", src, dest)) Ok(format!(
"Successfully copied directory '{}' to '{}'",
src, dest
))
} else { } else {
let error = String::from_utf8_lossy(&out.stderr); 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)), Err(e) => Err(FsError::CommandExecutionError(e)),
} }
} else { } else {
@ -249,6 +282,8 @@ pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
* # Examples * # Examples
* *
* ``` * ```
* use sal::os::exist;
*
* if exist("file.txt") { * if exist("file.txt") {
* println!("File exists"); * println!("File exists");
* } * }
@ -273,9 +308,14 @@ pub fn exist(path: &str) -> bool {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::find_file;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let file_path = find_file("/path/to/dir", "*.txt")?; * let file_path = find_file("/path/to/dir", "*.txt")?;
* println!("Found file: {}", file_path); * println!("Found file: {}", file_path);
* Ok(())
* }
* ``` * ```
*/ */
pub fn find_file(dir: &str, filename: &str) -> Result<String, FsError> { pub fn find_file(dir: &str, filename: &str) -> Result<String, FsError> {
@ -301,7 +341,10 @@ pub fn find_file(dir: &str, filename: &str) -> Result<String, FsError> {
_ => { _ => {
// If multiple matches, just return the first one instead of erroring // If multiple matches, just return the first one instead of erroring
// This makes wildcard searches more practical // 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()) Ok(files[0].to_string_lossy().to_string())
} }
} }
@ -322,11 +365,16 @@ pub fn find_file(dir: &str, filename: &str) -> Result<String, FsError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::find_files;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let files = find_files("/path/to/dir", "*.txt")?; * let files = find_files("/path/to/dir", "*.txt")?;
* for file in files { * for file in files {
* println!("Found file: {}", file); * println!("Found file: {}", file);
* } * }
* Ok(())
* }
* ``` * ```
*/ */
pub fn find_files(dir: &str, filename: &str) -> Result<Vec<String>, FsError> { pub fn find_files(dir: &str, filename: &str) -> Result<Vec<String>, FsError> {
@ -365,9 +413,14 @@ pub fn find_files(dir: &str, filename: &str) -> Result<Vec<String>, FsError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::find_dir;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let dir_path = find_dir("/path/to/parent", "sub*")?; * let dir_path = find_dir("/path/to/parent", "sub*")?;
* println!("Found directory: {}", dir_path); * println!("Found directory: {}", dir_path);
* Ok(())
* }
* ``` * ```
*/ */
pub fn find_dir(dir: &str, dirname: &str) -> Result<String, FsError> { pub fn find_dir(dir: &str, dirname: &str) -> Result<String, FsError> {
@ -390,7 +443,10 @@ pub fn find_dir(dir: &str, dirname: &str) -> Result<String, FsError> {
match dirs.len() { match dirs.len() {
0 => Err(FsError::DirectoryNotFound(dirname.to_string())), 0 => Err(FsError::DirectoryNotFound(dirname.to_string())),
1 => Ok(dirs[0].to_string_lossy().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
))),
} }
} }
@ -409,11 +465,16 @@ pub fn find_dir(dir: &str, dirname: &str) -> Result<String, FsError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::find_dirs;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let dirs = find_dirs("/path/to/parent", "sub*")?; * let dirs = find_dirs("/path/to/parent", "sub*")?;
* for dir in dirs { * for dir in dirs {
* println!("Found directory: {}", dir); * println!("Found directory: {}", dir);
* } * }
* Ok(())
* }
* ``` * ```
*/ */
pub fn find_dirs(dir: &str, dirname: &str) -> Result<Vec<String>, FsError> { pub fn find_dirs(dir: &str, dirname: &str) -> Result<Vec<String>, FsError> {
@ -452,11 +513,17 @@ pub fn find_dirs(dir: &str, dirname: &str) -> Result<Vec<String>, FsError> {
* # Examples * # Examples
* *
* ``` * ```
* use sal::os::delete;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Delete a file * // Delete a file
* let result = delete("file.txt")?; * let result = delete("file.txt")?;
* *
* // Delete a directory and all its contents * // Delete a directory and all its contents
* let result = delete("directory/")?; * let result = delete("directory/")?;
*
* Ok(())
* }
* ``` * ```
*/ */
pub fn delete(path: &str) -> Result<String, FsError> { pub fn delete(path: &str) -> Result<String, FsError> {
@ -494,8 +561,13 @@ pub fn delete(path: &str) -> Result<String, FsError> {
* # Examples * # Examples
* *
* ``` * ```
* use sal::os::mkdir;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = mkdir("path/to/new/directory")?; * let result = mkdir("path/to/new/directory")?;
* println!("{}", result); * println!("{}", result);
* Ok(())
* }
* ``` * ```
*/ */
pub fn mkdir(path: &str) -> Result<String, FsError> { pub fn mkdir(path: &str) -> Result<String, FsError> {
@ -529,9 +601,14 @@ pub fn mkdir(path: &str) -> Result<String, FsError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::file_size;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let size = file_size("file.txt")?; * let size = file_size("file.txt")?;
* println!("File size: {} bytes", size); * println!("File size: {} bytes", size);
* Ok(())
* }
* ``` * ```
*/ */
pub fn file_size(path: &str) -> Result<i64, FsError> { pub fn file_size(path: &str) -> Result<i64, FsError> {
@ -567,9 +644,14 @@ pub fn file_size(path: &str) -> Result<i64, FsError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::rsync;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = rsync("source_dir/", "backup_dir/")?; * let result = rsync("source_dir/", "backup_dir/")?;
* println!("{}", result); * println!("{}", result);
* Ok(())
* }
* ``` * ```
*/ */
pub fn rsync(src: &str, dest: &str) -> Result<String, FsError> { pub fn rsync(src: &str, dest: &str) -> Result<String, FsError> {
@ -599,13 +681,17 @@ pub fn rsync(src: &str, dest: &str) -> Result<String, FsError> {
match output { match output {
Ok(out) => { 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)) Ok(format!("Successfully synced '{}' to '{}'", src, dest))
} else { } else {
let error = String::from_utf8_lossy(&out.stderr); 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)), Err(e) => Err(FsError::CommandExecutionError(e)),
} }
} }
@ -624,9 +710,14 @@ pub fn rsync(src: &str, dest: &str) -> Result<String, FsError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::chdir;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = chdir("/path/to/directory")?; * let result = chdir("/path/to/directory")?;
* println!("{}", result); * println!("{}", result);
* Ok(())
* }
* ``` * ```
*/ */
pub fn chdir(path: &str) -> Result<String, FsError> { pub fn chdir(path: &str) -> Result<String, FsError> {
@ -662,9 +753,14 @@ pub fn chdir(path: &str) -> Result<String, FsError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::file_read;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let content = file_read("file.txt")?; * let content = file_read("file.txt")?;
* println!("File content: {}", content); * println!("File content: {}", content);
* Ok(())
* }
* ``` * ```
*/ */
pub fn file_read(path: &str) -> Result<String, FsError> { pub fn file_read(path: &str) -> Result<String, FsError> {
@ -700,8 +796,13 @@ pub fn file_read(path: &str) -> Result<String, FsError> {
* # Examples * # Examples
* *
* ``` * ```
* use sal::os::file_write;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = file_write("file.txt", "Hello, world!")?; * let result = file_write("file.txt", "Hello, world!")?;
* println!("{}", result); * println!("{}", result);
* Ok(())
* }
* ``` * ```
*/ */
pub fn file_write(path: &str, content: &str) -> Result<String, FsError> { pub fn file_write(path: &str, content: &str) -> Result<String, FsError> {
@ -734,8 +835,13 @@ pub fn file_write(path: &str, content: &str) -> Result<String, FsError> {
* # Examples * # Examples
* *
* ``` * ```
* use sal::os::file_write_append;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = file_write_append("log.txt", "New log entry\n")?; * let result = file_write_append("log.txt", "New log entry\n")?;
* println!("{}", result); * println!("{}", result);
* Ok(())
* }
* ``` * ```
*/ */
pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> { pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> {
@ -755,7 +861,8 @@ pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> {
// Append content to file // Append content to file
use std::io::Write; 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)) Ok(format!("Successfully appended to file '{}'", path))
} }
@ -775,7 +882,10 @@ pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::os::mv;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Move a file * // Move a file
* let result = mv("file.txt", "new_location/file.txt")?; * let result = mv("file.txt", "new_location/file.txt")?;
* *
@ -784,6 +894,9 @@ pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> {
* *
* // Rename a file * // Rename a file
* let result = mv("old_name.txt", "new_name.txt")?; * let result = mv("old_name.txt", "new_name.txt")?;
*
* Ok(())
* }
* ``` * ```
*/ */
pub fn mv(src: &str, dest: &str) -> Result<String, FsError> { pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
@ -826,7 +939,7 @@ pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
return FsError::DeleteFailed(del_err); return FsError::DeleteFailed(del_err);
} }
return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message
}, }
Err(copy_err) => return FsError::CopyFailed(copy_err), Err(copy_err) => return FsError::CopyFailed(copy_err),
} }
} else if src_path.is_dir() { } else if src_path.is_dir() {
@ -837,9 +950,7 @@ pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
.status(); .status();
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let output = Command::new("cp") let output = Command::new("cp").args(&["-R", src, dest]).status();
.args(&["-R", src, dest])
.status();
match output { match output {
Ok(status) => { Ok(status) => {
@ -850,9 +961,11 @@ pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
} }
return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message
} else { } 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), Err(cmd_err) => return FsError::CommandExecutionError(cmd_err),
} }
} }
@ -864,7 +977,10 @@ pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
if src_path.is_file() { if src_path.is_file() {
Ok(format!("Successfully moved file '{}' to '{}'", src, dest)) Ok(format!("Successfully moved file '{}' to '{}'", src, dest))
} else { } 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<String, FsError> {
* # Examples * # Examples
* *
* ``` * ```
* use sal::os::which;
*
* let cmd_path = which("ls"); * let cmd_path = which("ls");
* if cmd_path != "" { * if cmd_path != "" {
* println!("ls is available at: {}", cmd_path); * println!("ls is available at: {}", cmd_path);
@ -891,14 +1009,10 @@ pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
pub fn which(command: &str) -> String { pub fn which(command: &str) -> String {
// Use the appropriate command based on the platform // Use the appropriate command based on the platform
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let output = Command::new("where") let output = Command::new("where").arg(command).output();
.arg(command)
.output();
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let output = Command::new("which") let output = Command::new("which").arg(command).output();
.arg(command)
.output();
match output { match output {
Ok(out) => { Ok(out) => {
@ -908,7 +1022,7 @@ pub fn which(command: &str) -> String {
} else { } else {
String::new() String::new()
} }
}, }
Err(_) => String::new(), Err(_) => String::new(),
} }
} }
@ -929,22 +1043,31 @@ pub fn which(command: &str) -> String {
* # Examples * # Examples
* *
* ``` * ```
* use sal::os::cmd_ensure_exists;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Check if a single command exists * // Check if a single command exists
* let result = cmd_ensure_exists("nerdctl")?; * let result = cmd_ensure_exists("nerdctl")?;
* *
* // Check if multiple commands exist * // Check if multiple commands exist
* let result = cmd_ensure_exists("nerdctl,docker,containerd")?; * let result = cmd_ensure_exists("nerdctl,docker,containerd")?;
*
* Ok(())
* }
* ``` * ```
*/ */
pub fn cmd_ensure_exists(commands: &str) -> Result<String, FsError> { pub fn cmd_ensure_exists(commands: &str) -> Result<String, FsError> {
// Split the input by commas to handle multiple commands // 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()) .map(|s| s.trim())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.collect(); .collect();
if command_list.is_empty() { 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(); let mut missing_commands = Vec::new();

View File

@ -794,7 +794,7 @@ pub fn query_opt_with_pool_params(
/// This function sends a notification on the specified channel with the specified payload. /// This function sends a notification on the specified channel with the specified payload.
/// ///
/// Example: /// Example:
/// ``` /// ```no_run
/// use sal::postgresclient::notify; /// use sal::postgresclient::notify;
/// ///
/// notify("my_channel", "Hello, world!").expect("Failed to send notification"); /// 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. /// This function sends a notification on the specified channel with the specified payload using the connection pool.
/// ///
/// Example: /// Example:
/// ``` /// ```no_run
/// use sal::postgresclient::notify_with_pool; /// use sal::postgresclient::notify_with_pool;
/// ///
/// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification"); /// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification");

View File

@ -1,7 +1,7 @@
use std::process::Command;
use std::fmt;
use std::error::Error; use std::error::Error;
use std::fmt;
use std::io; use std::io;
use std::process::Command;
/// Error type for process management operations /// Error type for process management operations
/// ///
@ -23,11 +23,18 @@ pub enum ProcessError {
impl fmt::Display for ProcessError { impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { 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::CommandFailed(e) => write!(f, "{}", e),
ProcessError::NoProcessFound(pattern) => write!(f, "No processes found matching '{}'", pattern), ProcessError::NoProcessFound(pattern) => {
ProcessError::MultipleProcessesFound(pattern, count) => write!(f, "No processes found matching '{}'", pattern)
write!(f, "Multiple processes ({}) found matching '{}'", count, pattern), }
ProcessError::MultipleProcessesFound(pattern, count) => write!(
f,
"Multiple processes ({}) found matching '{}'",
count, pattern
),
} }
} }
} }
@ -65,6 +72,8 @@ pub struct ProcessInfo {
* # Examples * # Examples
* *
* ``` * ```
* use sal::process::which;
*
* match which("git") { * match which("git") {
* Some(path) => println!("Git is installed at: {}", path), * Some(path) => println!("Git is installed at: {}", path),
* None => println!("Git is not installed"), * None => println!("Git is not installed"),
@ -78,9 +87,7 @@ pub fn which(cmd: &str) -> Option<String> {
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
let which_cmd = "which"; let which_cmd = "which";
let output = Command::new(which_cmd) let output = Command::new(which_cmd).arg(cmd).output();
.arg(cmd)
.output();
match output { match output {
Ok(out) => { Ok(out) => {
@ -90,8 +97,8 @@ pub fn which(cmd: &str) -> Option<String> {
} else { } else {
None None
} }
}, }
Err(_) => None Err(_) => None,
} }
} }
@ -111,8 +118,13 @@ pub fn which(cmd: &str) -> Option<String> {
* *
* ``` * ```
* // Kill all processes with "server" in their name * // Kill all processes with "server" in their name
* use sal::process::kill;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = kill("server")?; * let result = kill("server")?;
* println!("{}", result); * println!("{}", result);
* Ok(())
* }
* ``` * ```
*/ */
pub fn kill(pattern: &str) -> Result<String, ProcessError> { pub fn kill(pattern: &str) -> Result<String, ProcessError> {
@ -144,10 +156,16 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
if stdout.contains("No tasks") { if stdout.contains("No tasks") {
Ok("No matching processes found".to_string()) Ok("No matching processes found".to_string())
} else { } else {
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", stdout))) Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
stdout
)))
} }
} else { } else {
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error))) Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
error
)))
} }
} }
} }
@ -168,7 +186,10 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
Ok("No matching processes found".to_string()) Ok("No matching processes found".to_string())
} else { } else {
let error = String::from_utf8_lossy(&output.stderr); 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
)))
} }
} }
} }
@ -189,6 +210,9 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
* *
* ``` * ```
* // List all processes * // List all processes
* use sal::process::process_list;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let processes = process_list("")?; * let processes = process_list("")?;
* *
* // List processes with "server" in their name * // List processes with "server" in their name
@ -196,6 +220,8 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
* for proc in processes { * for proc in processes {
* println!("PID: {}, Name: {}", proc.pid, proc.name); * println!("PID: {}, Name: {}", proc.pid, proc.name);
* } * }
* Ok(())
* }
* ``` * ```
*/ */
pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> { pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
@ -214,7 +240,8 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: Handle Name Priority) // 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(); let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0); let pid = parts[0].parse::<i64>().unwrap_or(0);
@ -235,7 +262,10 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
} }
} else { } else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 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
)));
} }
} }
@ -251,7 +281,8 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: PID COMMAND) // 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(); let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0); let pid = parts[0].parse::<i64>().unwrap_or(0);
@ -272,7 +303,10 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
} }
} else { } else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 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
)));
} }
} }
@ -293,9 +327,14 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
* *
* # Examples * # Examples
* *
* ``` * ```no_run
* use sal::process::process_get;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let process = process_get("unique-server-name")?; * let process = process_get("unique-server-name")?;
* println!("Found process: {} (PID: {})", process.name, process.pid); * println!("Found process: {} (PID: {})", process.name, process.pid);
* Ok(())
* }
* ``` * ```
*/ */
pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> { pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
@ -304,6 +343,9 @@ pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
match processes.len() { match processes.len() {
0 => Err(ProcessError::NoProcessFound(pattern.to_string())), 0 => Err(ProcessError::NoProcessFound(pattern.to_string())),
1 => Ok(processes[0].clone()), 1 => Ok(processes[0].clone()),
_ => Err(ProcessError::MultipleProcessesFound(pattern.to_string(), processes.len())), _ => Err(ProcessError::MultipleProcessesFound(
pattern.to_string(),
processes.len(),
)),
} }
} }

View File

@ -12,8 +12,9 @@ mod postgresclient;
mod process; mod process;
mod redisclient; mod redisclient;
mod rfs; mod rfs;
mod hero_vault; // This module now uses hero_vault internally mod vault;
mod text; mod text;
mod zinit;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -91,6 +92,9 @@ pub use rfs::register as register_rfs_module;
pub use crate::git::{GitRepo, GitTree}; pub use crate::git::{GitRepo, GitTree};
pub use git::register_git_module; pub use git::register_git_module;
// Re-export zinit module
pub use zinit::register_zinit_module;
// Re-export text module // Re-export text module
pub use text::register_text_module; pub use text::register_text_module;
// Re-export text functions directly from text module // Re-export text functions directly from text module
@ -107,7 +111,7 @@ pub use crate::text::{
pub use text::*; pub use text::*;
// Re-export crypto module // Re-export crypto module
pub use hero_vault::register_crypto_module; pub use vault::register_crypto_module;
// Rename copy functions to avoid conflicts // Rename copy functions to avoid conflicts
pub use os::copy as os_copy; pub use os::copy as os_copy;
@ -120,7 +124,7 @@ pub use os::copy as os_copy;
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```ignore
/// use rhai::Engine; /// use rhai::Engine;
/// use sal::rhai; /// use sal::rhai;
/// ///
@ -128,7 +132,8 @@ pub use os::copy as os_copy;
/// rhai::register(&mut engine); /// rhai::register(&mut engine);
/// ///
/// // Now you can use SAL functions in Rhai scripts /// // Now you can use SAL functions in Rhai scripts
/// let result = engine.eval::<bool>("exist('some_file.txt')").unwrap(); /// // You can evaluate Rhai scripts with SAL functions
/// let result = engine.eval::<i64>("exist('some_file.txt')").unwrap();
/// ``` /// ```
pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> { pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
// Register OS module functions // Register OS module functions
@ -146,6 +151,10 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
// Register Git module functions // Register Git module functions
git::register_git_module(engine)?; git::register_git_module(engine)?;
// Register Zinit module functions
zinit::register_zinit_module(engine)?;
// Register Text module functions // Register Text module functions
text::register_text_module(engine)?; text::register_text_module(engine)?;
@ -153,7 +162,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
rfs::register(engine)?; rfs::register(engine)?;
// Register Crypto module functions // Register Crypto module functions
hero_vault::register_crypto_module(engine)?; vault::register_crypto_module(engine)?;
// Register Redis client module functions // Register Redis client module functions

345
src/rhai/zinit.rs Normal file
View File

@ -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<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_zinit_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// 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<T> ToRhaiError<T> for Result<T, zinit_client::ClientError> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
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<Runtime, Box<EvalAltResult>> {
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<Map, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::list(socket_path).await
});
let services = result.to_rhai_error()?;
// Convert HashMap<String, String> 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<Map, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<String, Box<EvalAltResult>> {
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<String, Box<EvalAltResult>> {
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<Dynamic, Box<EvalAltResult>> {
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<Array, Box<EvalAltResult>> {
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<String> 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<Array, Box<EvalAltResult>> {
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<String> 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)
}
}
}

View File

@ -18,6 +18,8 @@
* # Examples * # Examples
* *
* ``` * ```
* use sal::text::dedent;
*
* let indented = " line 1\n line 2\n line 3"; * let indented = " line 1\n line 2\n line 3";
* let dedented = dedent(indented); * let dedented = dedent(indented);
* assert_eq!(dedented, "line 1\nline 2\n line 3"); * assert_eq!(dedented, "line 1\nline 2\n line 3");
@ -32,7 +34,8 @@ pub fn dedent(text: &str) -> String {
let lines: Vec<&str> = text.lines().collect(); let lines: Vec<&str> = text.lines().collect();
// Find the minimum indentation level (ignore empty lines) // Find the minimum indentation level (ignore empty lines)
let min_indent = lines.iter() let min_indent = lines
.iter()
.filter(|line| !line.trim().is_empty()) .filter(|line| !line.trim().is_empty())
.map(|line| { .map(|line| {
let mut spaces = 0; let mut spaces = 0;
@ -51,7 +54,8 @@ pub fn dedent(text: &str) -> String {
.unwrap_or(0); .unwrap_or(0);
// Remove that many spaces from the beginning of each line // Remove that many spaces from the beginning of each line
lines.iter() lines
.iter()
.map(|line| { .map(|line| {
if line.trim().is_empty() { if line.trim().is_empty() {
return String::new(); return String::new();
@ -66,11 +70,11 @@ pub fn dedent(text: &str) -> String {
Some(' ') => { Some(' ') => {
chars.next(); chars.next();
count += 1; count += 1;
}, }
Some('\t') => { Some('\t') => {
chars.next(); chars.next();
count += 4; count += 4;
}, }
_ => break, _ => break,
} }
} }
@ -82,7 +86,6 @@ pub fn dedent(text: &str) -> String {
.join("\n") .join("\n")
} }
/** /**
* Prefix a multiline string with a specified prefix. * Prefix a multiline string with a specified prefix.
* *
@ -100,6 +103,8 @@ pub fn dedent(text: &str) -> String {
* # Examples * # Examples
* *
* ``` * ```
* use sal::text::prefix;
*
* let text = "line 1\nline 2\nline 3"; * let text = "line 1\nline 2\nline 3";
* let prefixed = prefix(text, " "); * let prefixed = prefix(text, " ");
* assert_eq!(prefixed, " line 1\n line 2\n line 3"); * assert_eq!(prefixed, " line 1\n line 2\n line 3");

View File

@ -61,12 +61,15 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// ///
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let builder = TemplateBuilder::open("templates/example.html")? /// let builder = TemplateBuilder::open("templates/example.html")?
/// .add_var("title", "Hello World") /// .add_var("title", "Hello World")
/// .add_var("username", "John Doe"); /// .add_var("username", "John Doe");
/// Ok(())
/// }
/// ``` /// ```
pub fn add_var<S, V>(mut self, name: S, value: V) -> Self pub fn add_var<S, V>(mut self, name: S, value: V) -> Self
where where
@ -89,16 +92,19 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// use std::collections::HashMap; /// use std::collections::HashMap;
/// ///
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut vars = HashMap::new(); /// let mut vars = HashMap::new();
/// vars.insert("title", "Hello World"); /// vars.insert("title", "Hello World");
/// vars.insert("username", "John Doe"); /// vars.insert("username", "John Doe");
/// ///
/// let builder = TemplateBuilder::open("templates/example.html")? /// let builder = TemplateBuilder::open("templates/example.html")?
/// .add_vars(vars); /// .add_vars(vars);
/// Ok(())
/// }
/// ``` /// ```
pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self
where where
@ -148,15 +154,18 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// ///
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let result = TemplateBuilder::open("templates/example.html")? /// let result = TemplateBuilder::open("templates/example.html")?
/// .add_var("title", "Hello World") /// .add_var("title", "Hello World")
/// .add_var("username", "John Doe") /// .add_var("username", "John Doe")
/// .render()?; /// .render()?;
/// ///
/// println!("Rendered template: {}", result); /// println!("Rendered template: {}", result);
/// Ok(())
/// }
/// ``` /// ```
pub fn render(&mut self) -> Result<String, tera::Error> { pub fn render(&mut self) -> Result<String, tera::Error> {
// Initialize Tera if not already done // Initialize Tera if not already done
@ -185,17 +194,23 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// ///
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// TemplateBuilder::open("templates/example.html")? /// TemplateBuilder::open("templates/example.html")?
/// .add_var("title", "Hello World") /// .add_var("title", "Hello World")
/// .add_var("username", "John Doe") /// .add_var("username", "John Doe")
/// .render_to_file("output.html")?; /// .render_to_file("output.html")?;
/// Ok(())
/// }
/// ``` /// ```
pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> { pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> {
let rendered = self.render().map_err(|e| { 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) fs::write(output_path, rendered)
@ -217,9 +232,7 @@ mod tests {
// Create a template builder and add variables // Create a template builder and add variables
let mut builder = TemplateBuilder::open(temp_file.path())?; let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder builder = builder.add_var("name", "John").add_var("place", "Rust");
.add_var("name", "John")
.add_var("place", "Rust");
// Render the template // Render the template
let result = builder.render()?; let result = builder.render()?;
@ -280,7 +293,6 @@ mod tests {
let template_content = "{{ message }}\n"; let template_content = "{{ message }}\n";
fs::write(temp_file.path(), template_content)?; fs::write(temp_file.path(), template_content)?;
// Create an output file // Create an output file
let output_file = NamedTempFile::new()?; let output_file = NamedTempFile::new()?;

View File

@ -8,7 +8,7 @@ use std::sync::Arc;
use std::str::FromStr; use std::str::FromStr;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::hero_vault::error::CryptoError; use crate::vault::error::CryptoError;
use super::wallet::EthereumWallet; use super::wallet::EthereumWallet;
use super::networks::NetworkConfig; use super::networks::NetworkConfig;

View File

@ -16,11 +16,8 @@ mod provider;
mod transaction; mod transaction;
mod storage; mod storage;
mod contract; mod contract;
mod contract_utils; pub mod contract_utils;
pub mod networks; pub mod networks;
#[cfg(test)]
pub mod tests;
// Re-export public types and functions // Re-export public types and functions
pub use wallet::EthereumWallet; pub use wallet::EthereumWallet;
pub use networks::NetworkConfig; pub use networks::NetworkConfig;

Some files were not shown because too many files have changed in this diff Show More