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:
commit
9f263b6ec4
41
.gitignore
vendored
41
.gitignore
vendored
@ -22,4 +22,43 @@ Cargo.lock
|
||||
/rhai_test_template
|
||||
/rhai_test_download
|
||||
/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
|
||||
|
55
Cargo.toml
55
Cargo.toml
@ -11,24 +11,36 @@ categories = ["os", "filesystem", "api-bindings"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
tera = "1.19.0" # Template engine for text rendering
|
||||
# Cross-platform functionality
|
||||
libc = "0.2"
|
||||
anyhow = "1.0.98"
|
||||
base64 = "0.21.0" # Base64 encoding/decoding
|
||||
cfg-if = "1.0"
|
||||
|
||||
|
||||
thiserror = "1.0" # For error handling
|
||||
redis = "0.22.0" # Redis client
|
||||
postgres = "0.19.4" # PostgreSQL client
|
||||
tokio-postgres = "0.7.8" # Async PostgreSQL client
|
||||
postgres-types = "0.2.5" # PostgreSQL type conversions
|
||||
chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher
|
||||
clap = "2.33" # Command-line argument parsing
|
||||
dirs = "5.0.1" # Directory paths
|
||||
env_logger = "0.10.0" # Logger implementation
|
||||
ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library
|
||||
glob = "0.3.1" # For file pattern matching
|
||||
jsonrpsee = "0.25.1"
|
||||
k256 = { version = "0.13.1", features = ["ecdsa", "ecdh"] } # Elliptic curve cryptography
|
||||
lazy_static = "1.4.0" # For lazy initialization of static variables
|
||||
libc = "0.2"
|
||||
log = "0.4" # Logging facade
|
||||
once_cell = "1.18.0" # Lazy static initialization
|
||||
postgres = "0.19.4" # PostgreSQL client
|
||||
postgres-types = "0.2.5" # PostgreSQL type conversions
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_postgres = "0.18.2"
|
||||
rand = "0.8.5" # Random number generation
|
||||
redis = "0.22.0" # Redis client
|
||||
regex = "1.8.1" # For regex pattern matching
|
||||
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
|
||||
serde = { version = "1.0", features = [
|
||||
"derive",
|
||||
] } # For serialization/deserialization
|
||||
serde_json = "1.0" # For JSON handling
|
||||
glob = "0.3.1" # For file pattern matching
|
||||
sha2 = "0.10.7" # SHA-2 hash functions
|
||||
tempfile = "3.5" # For temporary file operations
|
||||
log = "0.4" # Logging facade
|
||||
env_logger = "0.10.0" # Logger implementation
|
||||
@ -37,9 +49,6 @@ rand = "0.8.5" # Random number generation
|
||||
clap = "2.33" # Command-line argument parsing
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_postgres = "0.18.2"
|
||||
slatedb = "0.6.1" # Embedded key-value store
|
||||
object_store = "0.5.0" # Object store implementations used by SlateDB
|
||||
bytes = "1.4.0" # Used for byte operations
|
||||
|
||||
# Crypto dependencies
|
||||
base64 = "0.21.0" # Base64 encoding/decoding
|
||||
@ -53,26 +62,6 @@ tokio = { version = "1.28", features = ["full"] }
|
||||
uuid = { version = "1.16.0", features = ["v4"] }
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
# WebAssembly dependencies
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = "0.2.87"
|
||||
js-sys = "0.3.64"
|
||||
wasm-bindgen-futures = "0.4.39"
|
||||
web-sys = { version = "0.3.64", features = [
|
||||
"Window",
|
||||
"Storage",
|
||||
"IdbDatabase",
|
||||
"IdbOpenDbRequest",
|
||||
"IdbFactory",
|
||||
"IdbTransaction",
|
||||
"IdbObjectStore",
|
||||
"IdbRequest",
|
||||
"IdbKeyRange",
|
||||
"IdbCursorWithValue",
|
||||
"Event",
|
||||
"console"
|
||||
] }
|
||||
|
||||
# Optional features for specific OS functionality
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = "0.26" # Unix-specific functionality
|
||||
@ -85,9 +74,9 @@ windows = { version = "0.48", features = [
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
mockall = "0.11.4" # For mocking in tests
|
||||
tempfile = "3.5" # For tests that need temporary files/directories
|
||||
tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing
|
||||
mockall = "0.11.4" # For mocking in tests
|
||||
|
||||
[[bin]]
|
||||
name = "herodo"
|
||||
|
@ -2,7 +2,7 @@
|
||||
// Demonstrates file system operations using SAL
|
||||
|
||||
// Create a test directory
|
||||
let test_dir = "rhai_test_dir";
|
||||
let test_dir = "/tmp/rhai_test_dir";
|
||||
println(`Creating directory: ${test_dir}`);
|
||||
let mkdir_result = mkdir(test_dir);
|
||||
println(`Directory creation result: ${mkdir_result}`);
|
||||
@ -61,4 +61,4 @@ for file in files {
|
||||
// delete(test_dir);
|
||||
// println("Cleanup complete");
|
||||
|
||||
"File operations script completed successfully!"
|
||||
"File operations script completed successfully!"
|
||||
|
87
examples/zinit/zinit_basic.rhai
Normal file
87
examples/zinit/zinit_basic.rhai
Normal 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}`);
|
||||
}
|
108
rhai_tests/keypair/01_keypair_operations.rhai
Normal file
108
rhai_tests/keypair/01_keypair_operations.rhai
Normal 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!");
|
162
rhai_tests/keypair/02_keyspace_operations.rhai
Normal file
162
rhai_tests/keypair/02_keyspace_operations.rhai
Normal 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!");
|
167
rhai_tests/keypair/03_session_management.rhai
Normal file
167
rhai_tests/keypair/03_session_management.rhai
Normal 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!");
|
192
rhai_tests/keypair/04_encryption_decryption.rhai
Normal file
192
rhai_tests/keypair/04_encryption_decryption.rhai
Normal 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!");
|
231
rhai_tests/keypair/05_error_handling.rhai
Normal file
231
rhai_tests/keypair/05_error_handling.rhai
Normal 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!");
|
293
rhai_tests/keypair/run_all_tests.rhai
Normal file
293
rhai_tests/keypair/run_all_tests.rhai
Normal 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;
|
@ -24,7 +24,7 @@ log "${BLUE} Running All Rhai Tests ${NC}"
|
||||
log "${BLUE}=======================================${NC}"
|
||||
|
||||
# Find all test runner scripts
|
||||
RUNNERS=$(find src/rhai_tests -name "run_all_tests.rhai")
|
||||
RUNNERS=$(find rhai_tests -name "run_all_tests.rhai")
|
||||
|
||||
# Initialize counters
|
||||
TOTAL_MODULES=0
|
||||
|
34
src/docs/.gitignore
vendored
34
src/docs/.gitignore
vendored
@ -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
|
@ -46,7 +46,8 @@ pub mod redisclient;
|
||||
pub mod rhai;
|
||||
pub mod text;
|
||||
pub mod virt;
|
||||
pub mod hero_vault;
|
||||
pub mod vault;
|
||||
pub mod zinit_client;
|
||||
|
||||
// Version information
|
||||
/// Returns the version of the SAL library
|
||||
|
@ -1,9 +1,9 @@
|
||||
use std::process::Command;
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
// Define a custom error type for download operations
|
||||
#[derive(Debug)]
|
||||
@ -26,11 +26,17 @@ pub enum DownloadError {
|
||||
impl fmt::Display for DownloadError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DownloadError::CreateDirectoryFailed(e) => write!(f, "Error creating directories: {}", e),
|
||||
DownloadError::CreateDirectoryFailed(e) => {
|
||||
write!(f, "Error creating directories: {}", e)
|
||||
}
|
||||
DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e),
|
||||
DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url),
|
||||
DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e),
|
||||
DownloadError::FileTooSmall(size, min) => write!(f, "Error: Downloaded file is too small ({}KB < {}KB)", size, min),
|
||||
DownloadError::FileTooSmall(size, min) => write!(
|
||||
f,
|
||||
"Error: Downloaded file is too small ({}KB < {}KB)",
|
||||
size, min
|
||||
),
|
||||
DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e),
|
||||
DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e),
|
||||
DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e),
|
||||
@ -74,12 +80,18 @@ impl Error for DownloadError {
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Download a file with no minimum size requirement
|
||||
* let path = download("https://example.com/file.txt", "/tmp/", 0)?;
|
||||
* ```no_run
|
||||
* use sal::os::download;
|
||||
*
|
||||
* // Download a file with minimum size requirement of 100KB
|
||||
* let path = download("https://example.com/file.zip", "/tmp/", 100)?;
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* // Download a file with no minimum size requirement
|
||||
* let path = download("https://example.com/file.txt", "/tmp/", 0)?;
|
||||
*
|
||||
* // Download a file with minimum size requirement of 100KB
|
||||
* let path = download("https://example.com/file.zip", "/tmp/", 100)?;
|
||||
*
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* # Notes
|
||||
@ -91,30 +103,41 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
|
||||
// Create parent directories if they don't exist
|
||||
let dest_path = Path::new(dest);
|
||||
fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?;
|
||||
|
||||
|
||||
// Extract filename from URL
|
||||
let filename = match url.split('/').last() {
|
||||
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
|
||||
let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename);
|
||||
|
||||
|
||||
// Create a temporary path for downloading
|
||||
let temp_path = format!("{}.download", file_path);
|
||||
|
||||
|
||||
// Use curl to download the file with progress bar
|
||||
println!("Downloading {} to {}", url, file_path);
|
||||
let output = Command::new("curl")
|
||||
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url])
|
||||
.args(&[
|
||||
"--progress-bar",
|
||||
"--location",
|
||||
"--fail",
|
||||
"--output",
|
||||
&temp_path,
|
||||
url,
|
||||
])
|
||||
.status()
|
||||
.map_err(DownloadError::CurlExecutionFailed)?;
|
||||
|
||||
|
||||
if !output.success() {
|
||||
return Err(DownloadError::DownloadFailed(url.to_string()));
|
||||
}
|
||||
|
||||
|
||||
// Show file size after download
|
||||
match fs::metadata(&temp_path) {
|
||||
Ok(metadata) => {
|
||||
@ -122,14 +145,20 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
|
||||
let size_kb = size_bytes / 1024;
|
||||
let size_mb = size_kb / 1024;
|
||||
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 {
|
||||
println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0);
|
||||
println!(
|
||||
"Download complete! File size: {:.2} KB",
|
||||
size_bytes as f64 / 1024.0
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(_) => println!("Download complete!"),
|
||||
}
|
||||
|
||||
|
||||
// Check file size if minimum size is specified
|
||||
if min_size_kb > 0 {
|
||||
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
|
||||
@ -139,57 +168,59 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
|
||||
return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if it's a compressed file that needs extraction
|
||||
let lower_url = url.to_lowercase();
|
||||
let is_archive = lower_url.ends_with(".tar.gz") ||
|
||||
lower_url.ends_with(".tgz") ||
|
||||
lower_url.ends_with(".tar") ||
|
||||
lower_url.ends_with(".zip");
|
||||
|
||||
let is_archive = lower_url.ends_with(".tar.gz")
|
||||
|| lower_url.ends_with(".tgz")
|
||||
|| lower_url.ends_with(".tar")
|
||||
|| lower_url.ends_with(".zip");
|
||||
|
||||
if is_archive {
|
||||
// Extract the file using the appropriate command with progress indication
|
||||
println!("Extracting {} to {}", temp_path, dest);
|
||||
let output = if lower_url.ends_with(".zip") {
|
||||
Command::new("unzip")
|
||||
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
|
||||
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
|
||||
.status()
|
||||
} else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
|
||||
Command::new("tar")
|
||||
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||
.status()
|
||||
} else {
|
||||
Command::new("tar")
|
||||
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||
.status()
|
||||
};
|
||||
|
||||
|
||||
match output {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string()));
|
||||
return Err(DownloadError::ExtractionFailed(
|
||||
"Error extracting archive".to_string(),
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
|
||||
}
|
||||
|
||||
|
||||
// Show number of extracted files
|
||||
match fs::read_dir(dest) {
|
||||
Ok(entries) => {
|
||||
let count = entries.count();
|
||||
println!("Extraction complete! Extracted {} files/directories", count);
|
||||
},
|
||||
}
|
||||
Err(_) => println!("Extraction complete!"),
|
||||
}
|
||||
|
||||
|
||||
// Remove the temporary file
|
||||
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
|
||||
|
||||
|
||||
Ok(dest.to_string())
|
||||
} else {
|
||||
// Just rename the temporary file to the final destination
|
||||
fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
|
||||
|
||||
|
||||
Ok(file_path)
|
||||
}
|
||||
}
|
||||
@ -210,12 +241,18 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Download a file with no minimum size requirement
|
||||
* let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
|
||||
* ```no_run
|
||||
* use sal::os::download_file;
|
||||
*
|
||||
* // Download a file with minimum size requirement of 100KB
|
||||
* let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?;
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* // Download a file with no minimum size requirement
|
||||
* let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
|
||||
*
|
||||
* // Download a file with minimum size requirement of 100KB
|
||||
* let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?;
|
||||
*
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
|
||||
@ -224,21 +261,28 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?;
|
||||
}
|
||||
|
||||
|
||||
// Create a temporary path for downloading
|
||||
let temp_path = format!("{}.download", dest);
|
||||
|
||||
|
||||
// Use curl to download the file with progress bar
|
||||
println!("Downloading {} to {}", url, dest);
|
||||
let output = Command::new("curl")
|
||||
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url])
|
||||
.args(&[
|
||||
"--progress-bar",
|
||||
"--location",
|
||||
"--fail",
|
||||
"--output",
|
||||
&temp_path,
|
||||
url,
|
||||
])
|
||||
.status()
|
||||
.map_err(DownloadError::CurlExecutionFailed)?;
|
||||
|
||||
|
||||
if !output.success() {
|
||||
return Err(DownloadError::DownloadFailed(url.to_string()));
|
||||
}
|
||||
|
||||
|
||||
// Show file size after download
|
||||
match fs::metadata(&temp_path) {
|
||||
Ok(metadata) => {
|
||||
@ -246,14 +290,20 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
|
||||
let size_kb = size_bytes / 1024;
|
||||
let size_mb = size_kb / 1024;
|
||||
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 {
|
||||
println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0);
|
||||
println!(
|
||||
"Download complete! File size: {:.2} KB",
|
||||
size_bytes as f64 / 1024.0
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(_) => println!("Download complete!"),
|
||||
}
|
||||
|
||||
|
||||
// Check file size if minimum size is specified
|
||||
if min_size_kb > 0 {
|
||||
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
|
||||
@ -263,10 +313,10 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
|
||||
return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Rename the temporary file to the final destination
|
||||
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
|
||||
|
||||
|
||||
Ok(dest.to_string())
|
||||
}
|
||||
|
||||
@ -284,27 +334,38 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Make a file executable
|
||||
* chmod_exec("/path/to/file")?;
|
||||
* ```no_run
|
||||
* use sal::os::chmod_exec;
|
||||
*
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* // Make a file executable
|
||||
* chmod_exec("/path/to/file")?;
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
|
||||
let path_obj = Path::new(path);
|
||||
|
||||
|
||||
// Check if the path exists and is a file
|
||||
if !path_obj.exists() {
|
||||
return Err(DownloadError::NotAFile(format!("Path does not exist: {}", path)));
|
||||
return Err(DownloadError::NotAFile(format!(
|
||||
"Path does not exist: {}",
|
||||
path
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
if !path_obj.is_file() {
|
||||
return Err(DownloadError::NotAFile(format!("Path is not a file: {}", path)));
|
||||
return Err(DownloadError::NotAFile(format!(
|
||||
"Path is not a file: {}",
|
||||
path
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
// Get current permissions
|
||||
let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
|
||||
// Set executable bit for user, group, and others
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@ -314,47 +375,55 @@ pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
|
||||
let new_mode = mode | 0o111;
|
||||
permissions.set_mode(new_mode);
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On non-Unix platforms, we can't set executable bit directly
|
||||
// Just return success with a warning
|
||||
return Ok(format!("Made {} executable (note: non-Unix platform, may not be fully supported)", path));
|
||||
return Ok(format!(
|
||||
"Made {} executable (note: non-Unix platform, may not be fully supported)",
|
||||
path
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Apply the new permissions
|
||||
fs::set_permissions(path, permissions).map_err(|e|
|
||||
fs::set_permissions(path, permissions).map_err(|e| {
|
||||
DownloadError::CommandExecutionFailed(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Failed to set executable permissions: {}", e)
|
||||
format!("Failed to set executable permissions: {}", e),
|
||||
))
|
||||
)?;
|
||||
|
||||
})?;
|
||||
|
||||
Ok(format!("Made {} executable", path))
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file and install it if it's a supported package format.
|
||||
*
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
*
|
||||
* * `url` - The URL to download from
|
||||
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
|
||||
*
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
*
|
||||
* * `Ok(String)` - The path where the file was saved or extracted
|
||||
* * `Err(DownloadError)` - An error if the download or installation failed
|
||||
*
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
*
|
||||
* ```no_run
|
||||
* use sal::os::download_install;
|
||||
*
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* // Download and install a .deb package
|
||||
* let result = download_install("https://example.com/package.deb", 100)?;
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
* // Download and install a .deb package
|
||||
* let result = download_install("https://example.com/package.deb", 100)?;
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* # Notes
|
||||
*
|
||||
*
|
||||
* Currently only supports .deb packages on Debian-based systems.
|
||||
* For other file types, it behaves the same as the download function.
|
||||
*/
|
||||
@ -362,19 +431,23 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
|
||||
// Extract filename from URL
|
||||
let filename = match url.split('/').last() {
|
||||
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
|
||||
let dest_path = format!("/tmp/{}", filename);
|
||||
|
||||
// Check if it's a compressed file that needs extraction
|
||||
let lower_url = url.to_lowercase();
|
||||
let is_archive = lower_url.ends_with(".tar.gz") ||
|
||||
lower_url.ends_with(".tgz") ||
|
||||
lower_url.ends_with(".tar") ||
|
||||
lower_url.ends_with(".zip");
|
||||
|
||||
let is_archive = lower_url.ends_with(".tar.gz")
|
||||
|| lower_url.ends_with(".tgz")
|
||||
|| lower_url.ends_with(".tar")
|
||||
|| lower_url.ends_with(".zip");
|
||||
|
||||
let download_result = if is_archive {
|
||||
// For archives, use the directory-based download function
|
||||
download(url, "/tmp", min_size_kb)?
|
||||
@ -382,13 +455,13 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
|
||||
// For regular files, use the file-specific download function
|
||||
download_file(url, &dest_path, min_size_kb)?
|
||||
};
|
||||
|
||||
|
||||
// Check if the downloaded result is a file
|
||||
let path = Path::new(&dest_path);
|
||||
if !path.is_file() {
|
||||
return Ok(download_result); // Not a file, might be an extracted directory
|
||||
}
|
||||
|
||||
|
||||
// Check if it's a .deb package
|
||||
if dest_path.to_lowercase().ends_with(".deb") {
|
||||
// Check if we're on a Debian-based platform
|
||||
@ -396,26 +469,28 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
|
||||
.arg("-c")
|
||||
.arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version")
|
||||
.status();
|
||||
|
||||
|
||||
match platform_check {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
return Err(DownloadError::PlatformNotSupported(
|
||||
"Cannot install .deb package: not on a Debian-based system".to_string()
|
||||
"Cannot install .deb package: not on a Debian-based system".to_string(),
|
||||
));
|
||||
}
|
||||
},
|
||||
Err(_) => return Err(DownloadError::PlatformNotSupported(
|
||||
"Failed to check system compatibility for .deb installation".to_string()
|
||||
)),
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(DownloadError::PlatformNotSupported(
|
||||
"Failed to check system compatibility for .deb installation".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Install the .deb package non-interactively
|
||||
println!("Installing package: {}", dest_path);
|
||||
let install_result = Command::new("sudo")
|
||||
.args(&["dpkg", "--install", &dest_path])
|
||||
.status();
|
||||
|
||||
|
||||
match install_result {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
@ -424,24 +499,24 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
|
||||
let fix_deps = Command::new("sudo")
|
||||
.args(&["apt-get", "install", "-f", "-y"])
|
||||
.status();
|
||||
|
||||
|
||||
if let Ok(fix_status) = fix_deps {
|
||||
if !fix_status.success() {
|
||||
return Err(DownloadError::InstallationFailed(
|
||||
"Failed to resolve package dependencies".to_string()
|
||||
"Failed to resolve package dependencies".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(DownloadError::InstallationFailed(
|
||||
"Failed to resolve package dependencies".to_string()
|
||||
"Failed to resolve package dependencies".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
println!("Package installation completed successfully");
|
||||
},
|
||||
}
|
||||
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(download_result)
|
||||
}
|
||||
|
597
src/os/fs.rs
597
src/os/fs.rs
File diff suppressed because it is too large
Load Diff
@ -794,7 +794,7 @@ pub fn query_opt_with_pool_params(
|
||||
/// This function sends a notification on the specified channel with the specified payload.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use sal::postgresclient::notify;
|
||||
///
|
||||
/// notify("my_channel", "Hello, world!").expect("Failed to send notification");
|
||||
@ -810,7 +810,7 @@ pub fn notify(channel: &str, payload: &str) -> Result<(), PostgresError> {
|
||||
/// This function sends a notification on the specified channel with the specified payload using the connection pool.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use sal::postgresclient::notify_with_pool;
|
||||
///
|
||||
/// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification");
|
||||
|
@ -1,10 +1,10 @@
|
||||
use std::process::Command;
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::process::Command;
|
||||
|
||||
/// Error type for process management operations
|
||||
///
|
||||
///
|
||||
/// This enum represents various errors that can occur during process management
|
||||
/// operations such as listing, finding, or killing processes.
|
||||
#[derive(Debug)]
|
||||
@ -23,11 +23,18 @@ pub enum ProcessError {
|
||||
impl fmt::Display for ProcessError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ProcessError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
|
||||
ProcessError::CommandExecutionFailed(e) => {
|
||||
write!(f, "Failed to execute command: {}", e)
|
||||
}
|
||||
ProcessError::CommandFailed(e) => write!(f, "{}", e),
|
||||
ProcessError::NoProcessFound(pattern) => write!(f, "No processes found matching '{}'", pattern),
|
||||
ProcessError::MultipleProcessesFound(pattern, count) =>
|
||||
write!(f, "Multiple processes ({}) found matching '{}'", count, pattern),
|
||||
ProcessError::NoProcessFound(pattern) => {
|
||||
write!(f, "No processes found matching '{}'", pattern)
|
||||
}
|
||||
ProcessError::MultipleProcessesFound(pattern, count) => write!(
|
||||
f,
|
||||
"Multiple processes ({}) found matching '{}'",
|
||||
count, pattern
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -53,18 +60,20 @@ pub struct ProcessInfo {
|
||||
|
||||
/**
|
||||
* Check if a command exists in PATH.
|
||||
*
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
*
|
||||
* * `cmd` - The command to check
|
||||
*
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
*
|
||||
* * `Option<String>` - The full path to the command if found, None otherwise
|
||||
*
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
*
|
||||
* ```
|
||||
* use sal::process::which;
|
||||
*
|
||||
* match which("git") {
|
||||
* Some(path) => println!("Git is installed at: {}", path),
|
||||
* None => println!("Git is not installed"),
|
||||
@ -74,14 +83,12 @@ pub struct ProcessInfo {
|
||||
pub fn which(cmd: &str) -> Option<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let which_cmd = "where";
|
||||
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let which_cmd = "which";
|
||||
|
||||
let output = Command::new(which_cmd)
|
||||
.arg(cmd)
|
||||
.output();
|
||||
|
||||
|
||||
let output = Command::new(which_cmd).arg(cmd).output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
if out.status.success() {
|
||||
@ -90,29 +97,34 @@ pub fn which(cmd: &str) -> Option<String> {
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill processes matching a pattern.
|
||||
*
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
*
|
||||
* * `pattern` - The pattern to match against process names
|
||||
*
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating processes were killed or none were found
|
||||
* * `Err(ProcessError)` - An error if the kill operation failed
|
||||
*
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
*
|
||||
* ```
|
||||
* // Kill all processes with "server" in their name
|
||||
* let result = kill("server")?;
|
||||
* println!("{}", result);
|
||||
* use sal::process::kill;
|
||||
*
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* let result = kill("server")?;
|
||||
* println!("{}", result);
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn kill(pattern: &str) -> Result<String, ProcessError> {
|
||||
@ -121,7 +133,7 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
|
||||
{
|
||||
// On Windows, use taskkill with wildcard support
|
||||
let mut args = vec!["/F"]; // Force kill
|
||||
|
||||
|
||||
if pattern.contains('*') {
|
||||
// If it contains wildcards, use filter
|
||||
args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]);
|
||||
@ -129,12 +141,12 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
|
||||
// Otherwise use image name directly
|
||||
args.extend(&["/IM", pattern]);
|
||||
}
|
||||
|
||||
|
||||
let output = Command::new("taskkill")
|
||||
.args(&args)
|
||||
.output()
|
||||
.map_err(ProcessError::CommandExecutionFailed)?;
|
||||
|
||||
|
||||
if output.status.success() {
|
||||
Ok("Successfully killed processes".to_string())
|
||||
} else {
|
||||
@ -144,14 +156,20 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
|
||||
if stdout.contains("No tasks") {
|
||||
Ok("No matching processes found".to_string())
|
||||
} else {
|
||||
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", stdout)))
|
||||
Err(ProcessError::CommandFailed(format!(
|
||||
"Failed to kill processes: {}",
|
||||
stdout
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error)))
|
||||
Err(ProcessError::CommandFailed(format!(
|
||||
"Failed to kill processes: {}",
|
||||
error
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
// On Unix-like systems, use pkill which has built-in pattern matching
|
||||
@ -160,7 +178,7 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
|
||||
.arg(pattern)
|
||||
.output()
|
||||
.map_err(ProcessError::CommandExecutionFailed)?;
|
||||
|
||||
|
||||
// pkill returns 0 if processes were killed, 1 if none matched
|
||||
if output.status.success() {
|
||||
Ok("Successfully killed processes".to_string())
|
||||
@ -168,39 +186,47 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
|
||||
Ok("No matching processes found".to_string())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error)))
|
||||
Err(ProcessError::CommandFailed(format!(
|
||||
"Failed to kill processes: {}",
|
||||
error
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List processes matching a pattern (or all if pattern is empty).
|
||||
*
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
*
|
||||
* * `pattern` - The pattern to match against process names (empty string for all processes)
|
||||
*
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
*
|
||||
* * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes
|
||||
* * `Err(ProcessError)` - An error if the list operation failed
|
||||
*
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
*
|
||||
* ```
|
||||
* // List all processes
|
||||
* let processes = process_list("")?;
|
||||
*
|
||||
* // List processes with "server" in their name
|
||||
* let processes = process_list("server")?;
|
||||
* for proc in processes {
|
||||
* println!("PID: {}, Name: {}", proc.pid, proc.name);
|
||||
* use sal::process::process_list;
|
||||
*
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* let processes = process_list("")?;
|
||||
*
|
||||
* // List processes with "server" in their name
|
||||
* let processes = process_list("server")?;
|
||||
* for proc in processes {
|
||||
* println!("PID: {}, Name: {}", proc.pid, proc.name);
|
||||
* }
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
|
||||
let mut processes = Vec::new();
|
||||
|
||||
|
||||
// Platform specific implementations
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@ -209,22 +235,23 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
|
||||
.args(&["process", "list", "brief"])
|
||||
.output()
|
||||
.map_err(ProcessError::CommandExecutionFailed)?;
|
||||
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
|
||||
|
||||
// Parse output (assuming format: Handle Name Priority)
|
||||
for line in stdout.lines().skip(1) { // Skip header
|
||||
for line in stdout.lines().skip(1) {
|
||||
// Skip header
|
||||
let parts: Vec<&str> = line.trim().split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let pid = parts[0].parse::<i64>().unwrap_or(0);
|
||||
let name = parts[1].to_string();
|
||||
|
||||
|
||||
// Filter by pattern if provided
|
||||
if !pattern.is_empty() && !name.contains(pattern) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
processes.push(ProcessInfo {
|
||||
pid,
|
||||
name,
|
||||
@ -235,10 +262,13 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
|
||||
}
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr)));
|
||||
return Err(ProcessError::CommandFailed(format!(
|
||||
"Failed to list processes: {}",
|
||||
stderr
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
// Unix implementation using ps
|
||||
@ -246,22 +276,23 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
|
||||
.args(&["-eo", "pid,comm"])
|
||||
.output()
|
||||
.map_err(ProcessError::CommandExecutionFailed)?;
|
||||
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
|
||||
|
||||
// Parse output (assuming format: PID COMMAND)
|
||||
for line in stdout.lines().skip(1) { // Skip header
|
||||
for line in stdout.lines().skip(1) {
|
||||
// Skip header
|
||||
let parts: Vec<&str> = line.trim().split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let pid = parts[0].parse::<i64>().unwrap_or(0);
|
||||
let name = parts[1].to_string();
|
||||
|
||||
|
||||
// Filter by pattern if provided
|
||||
if !pattern.is_empty() && !name.contains(pattern) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
processes.push(ProcessInfo {
|
||||
pid,
|
||||
name,
|
||||
@ -272,38 +303,49 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
|
||||
}
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr)));
|
||||
return Err(ProcessError::CommandFailed(format!(
|
||||
"Failed to list processes: {}",
|
||||
stderr
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(processes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single process matching the pattern (error if 0 or more than 1 match).
|
||||
*
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
*
|
||||
* * `pattern` - The pattern to match against process names
|
||||
*
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
*
|
||||
* * `Ok(ProcessInfo)` - Information about the matching process
|
||||
* * `Err(ProcessError)` - An error if no process or multiple processes match
|
||||
*
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let process = process_get("unique-server-name")?;
|
||||
* println!("Found process: {} (PID: {})", process.name, process.pid);
|
||||
*
|
||||
* ```no_run
|
||||
* use sal::process::process_get;
|
||||
*
|
||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* let process = process_get("unique-server-name")?;
|
||||
* println!("Found process: {} (PID: {})", process.name, process.pid);
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
|
||||
let processes = process_list(pattern)?;
|
||||
|
||||
|
||||
match processes.len() {
|
||||
0 => Err(ProcessError::NoProcessFound(pattern.to_string())),
|
||||
1 => Ok(processes[0].clone()),
|
||||
_ => Err(ProcessError::MultipleProcessesFound(pattern.to_string(), processes.len())),
|
||||
_ => Err(ProcessError::MultipleProcessesFound(
|
||||
pattern.to_string(),
|
||||
processes.len(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,9 @@ mod postgresclient;
|
||||
mod process;
|
||||
mod redisclient;
|
||||
mod rfs;
|
||||
mod hero_vault; // This module now uses hero_vault internally
|
||||
mod vault;
|
||||
mod text;
|
||||
mod zinit;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@ -91,6 +92,9 @@ pub use rfs::register as register_rfs_module;
|
||||
pub use crate::git::{GitRepo, GitTree};
|
||||
pub use git::register_git_module;
|
||||
|
||||
// Re-export zinit module
|
||||
pub use zinit::register_zinit_module;
|
||||
|
||||
// Re-export text module
|
||||
pub use text::register_text_module;
|
||||
// Re-export text functions directly from text module
|
||||
@ -107,7 +111,7 @@ pub use crate::text::{
|
||||
pub use text::*;
|
||||
|
||||
// Re-export crypto module
|
||||
pub use hero_vault::register_crypto_module;
|
||||
pub use vault::register_crypto_module;
|
||||
|
||||
// Rename copy functions to avoid conflicts
|
||||
pub use os::copy as os_copy;
|
||||
@ -120,7 +124,7 @@ pub use os::copy as os_copy;
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// use rhai::Engine;
|
||||
/// use sal::rhai;
|
||||
///
|
||||
@ -128,7 +132,8 @@ pub use os::copy as os_copy;
|
||||
/// rhai::register(&mut engine);
|
||||
///
|
||||
/// // Now you can use SAL functions in Rhai scripts
|
||||
/// let result = engine.eval::<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>> {
|
||||
// Register OS module functions
|
||||
@ -146,6 +151,10 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
// Register Git module functions
|
||||
git::register_git_module(engine)?;
|
||||
|
||||
|
||||
// Register Zinit module functions
|
||||
zinit::register_zinit_module(engine)?;
|
||||
|
||||
// Register Text module functions
|
||||
text::register_text_module(engine)?;
|
||||
|
||||
@ -153,7 +162,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
rfs::register(engine)?;
|
||||
|
||||
// Register Crypto module functions
|
||||
hero_vault::register_crypto_module(engine)?;
|
||||
vault::register_crypto_module(engine)?;
|
||||
|
||||
|
||||
// Register Redis client module functions
|
||||
|
345
src/rhai/zinit.rs
Normal file
345
src/rhai/zinit.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +1,32 @@
|
||||
/**
|
||||
* Dedent a multiline string by removing common leading whitespace.
|
||||
*
|
||||
*
|
||||
* This function analyzes all non-empty lines in the input text to determine
|
||||
* the minimum indentation level, then removes that amount of whitespace
|
||||
* from the beginning of each line. This is useful for working with
|
||||
* multi-line strings in code that have been indented to match the
|
||||
* surrounding code structure.
|
||||
*
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
*
|
||||
* * `text` - The multiline string to dedent
|
||||
*
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
*
|
||||
* * `String` - The dedented string
|
||||
*
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
*
|
||||
* ```
|
||||
* use sal::text::dedent;
|
||||
*
|
||||
* let indented = " line 1\n line 2\n line 3";
|
||||
* let dedented = dedent(indented);
|
||||
* assert_eq!(dedented, "line 1\nline 2\n line 3");
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* # Notes
|
||||
*
|
||||
*
|
||||
* - Empty lines are preserved but have all leading whitespace removed
|
||||
* - Tabs are counted as 4 spaces for indentation purposes
|
||||
*/
|
||||
@ -32,7 +34,8 @@ pub fn dedent(text: &str) -> String {
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
|
||||
// Find the minimum indentation level (ignore empty lines)
|
||||
let min_indent = lines.iter()
|
||||
let min_indent = lines
|
||||
.iter()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| {
|
||||
let mut spaces = 0;
|
||||
@ -51,7 +54,8 @@ pub fn dedent(text: &str) -> String {
|
||||
.unwrap_or(0);
|
||||
|
||||
// Remove that many spaces from the beginning of each line
|
||||
lines.iter()
|
||||
lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
if line.trim().is_empty() {
|
||||
return String::new();
|
||||
@ -59,22 +63,22 @@ pub fn dedent(text: &str) -> String {
|
||||
|
||||
let mut count = 0;
|
||||
let mut chars = line.chars().peekable();
|
||||
|
||||
|
||||
// Skip initial spaces up to min_indent
|
||||
while count < min_indent && chars.peek().is_some() {
|
||||
match chars.peek() {
|
||||
Some(' ') => {
|
||||
chars.next();
|
||||
count += 1;
|
||||
},
|
||||
}
|
||||
Some('\t') => {
|
||||
chars.next();
|
||||
count += 4;
|
||||
},
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Return the remaining characters
|
||||
chars.collect::<String>()
|
||||
})
|
||||
@ -82,24 +86,25 @@ pub fn dedent(text: &str) -> String {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prefix a multiline string with a specified prefix.
|
||||
*
|
||||
*
|
||||
* This function adds the specified prefix to the beginning of each line in the input text.
|
||||
*
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
*
|
||||
* * `text` - The multiline string to prefix
|
||||
* * `prefix` - The prefix to add to each line
|
||||
*
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
*
|
||||
* * `String` - The prefixed string
|
||||
*
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
*
|
||||
* ```
|
||||
* use sal::text::prefix;
|
||||
*
|
||||
* let text = "line 1\nline 2\nline 3";
|
||||
* let prefixed = prefix(text, " ");
|
||||
* assert_eq!(prefixed, " line 1\n line 2\n line 3");
|
||||
|
@ -32,7 +32,7 @@ impl TemplateBuilder {
|
||||
/// ```
|
||||
pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> {
|
||||
let path_str = template_path.as_ref().to_string_lossy().to_string();
|
||||
|
||||
|
||||
// Verify the template file exists
|
||||
if !Path::new(&path_str).exists() {
|
||||
return Err(io::Error::new(
|
||||
@ -40,14 +40,14 @@ impl TemplateBuilder {
|
||||
format!("Template file not found: {}", path_str),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Ok(Self {
|
||||
template_path: path_str,
|
||||
context: Context::new(),
|
||||
tera: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Adds a variable to the template context.
|
||||
///
|
||||
/// # Arguments
|
||||
@ -61,12 +61,15 @@ impl TemplateBuilder {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use sal::text::TemplateBuilder;
|
||||
///
|
||||
/// let builder = TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_var("title", "Hello World")
|
||||
/// .add_var("username", "John Doe");
|
||||
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let builder = TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_var("title", "Hello World")
|
||||
/// .add_var("username", "John Doe");
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn add_var<S, V>(mut self, name: S, value: V) -> Self
|
||||
where
|
||||
@ -76,7 +79,7 @@ impl TemplateBuilder {
|
||||
self.context.insert(name.as_ref(), &value);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Adds multiple variables to the template context from a HashMap.
|
||||
///
|
||||
/// # Arguments
|
||||
@ -89,16 +92,19 @@ impl TemplateBuilder {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use sal::text::TemplateBuilder;
|
||||
/// use std::collections::HashMap;
|
||||
///
|
||||
/// let mut vars = HashMap::new();
|
||||
/// vars.insert("title", "Hello World");
|
||||
/// vars.insert("username", "John Doe");
|
||||
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut vars = HashMap::new();
|
||||
/// vars.insert("title", "Hello World");
|
||||
/// vars.insert("username", "John Doe");
|
||||
///
|
||||
/// let builder = TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_vars(vars);
|
||||
/// let builder = TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_vars(vars);
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self
|
||||
where
|
||||
@ -110,7 +116,7 @@ impl TemplateBuilder {
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Initializes the Tera template engine with the template file.
|
||||
///
|
||||
/// This method is called automatically by render() if not called explicitly.
|
||||
@ -122,24 +128,24 @@ impl TemplateBuilder {
|
||||
if self.tera.is_none() {
|
||||
// Create a new Tera instance with just this template
|
||||
let mut tera = Tera::default();
|
||||
|
||||
|
||||
// Read the template content
|
||||
let template_content = fs::read_to_string(&self.template_path)
|
||||
.map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?;
|
||||
|
||||
|
||||
// Add the template to Tera
|
||||
let template_name = Path::new(&self.template_path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("template");
|
||||
|
||||
|
||||
tera.add_raw_template(template_name, &template_content)?;
|
||||
self.tera = Some(tera);
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Renders the template with the current context.
|
||||
///
|
||||
/// # Returns
|
||||
@ -148,31 +154,34 @@ impl TemplateBuilder {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use sal::text::TemplateBuilder;
|
||||
///
|
||||
/// let result = TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_var("title", "Hello World")
|
||||
/// .add_var("username", "John Doe")
|
||||
/// .render()?;
|
||||
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let result = TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_var("title", "Hello World")
|
||||
/// .add_var("username", "John Doe")
|
||||
/// .render()?;
|
||||
///
|
||||
/// println!("Rendered template: {}", result);
|
||||
/// println!("Rendered template: {}", result);
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn render(&mut self) -> Result<String, tera::Error> {
|
||||
// Initialize Tera if not already done
|
||||
self.initialize_tera()?;
|
||||
|
||||
|
||||
// Get the template name
|
||||
let template_name = Path::new(&self.template_path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("template");
|
||||
|
||||
|
||||
// Render the template
|
||||
let tera = self.tera.as_ref().unwrap();
|
||||
tera.render(template_name, &self.context)
|
||||
}
|
||||
|
||||
|
||||
/// Renders the template and writes the result to a file.
|
||||
///
|
||||
/// # Arguments
|
||||
@ -185,19 +194,25 @@ impl TemplateBuilder {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use sal::text::TemplateBuilder;
|
||||
///
|
||||
/// TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_var("title", "Hello World")
|
||||
/// .add_var("username", "John Doe")
|
||||
/// .render_to_file("output.html")?;
|
||||
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_var("title", "Hello World")
|
||||
/// .add_var("username", "John Doe")
|
||||
/// .render_to_file("output.html")?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> {
|
||||
let rendered = self.render().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("Template rendering error: {}", e))
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Template rendering error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
|
||||
fs::write(output_path, rendered)
|
||||
}
|
||||
}
|
||||
@ -207,70 +222,68 @@ mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a temporary template file
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n";
|
||||
fs::write(temp_file.path(), template_content)?;
|
||||
|
||||
|
||||
// Create a template builder and add variables
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
builder = builder
|
||||
.add_var("name", "John")
|
||||
.add_var("place", "Rust");
|
||||
|
||||
builder = builder.add_var("name", "John").add_var("place", "Rust");
|
||||
|
||||
// Render the template
|
||||
let result = builder.render()?;
|
||||
assert_eq!(result, "Hello, John! Welcome to Rust.\n");
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a temporary template file
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
let template_content = "{% if show_greeting %}Hello, {{ name }}!{% endif %}\n{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}\n";
|
||||
fs::write(temp_file.path(), template_content)?;
|
||||
|
||||
|
||||
// Create a template builder and add variables
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
|
||||
|
||||
// Add variables including a boolean and a vector
|
||||
builder = builder
|
||||
.add_var("name", "Alice")
|
||||
.add_var("show_greeting", true)
|
||||
.add_var("items", vec!["apple", "banana", "cherry"]);
|
||||
|
||||
|
||||
// Render the template
|
||||
let result = builder.render()?;
|
||||
assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n");
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a temporary template file
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?;
|
||||
temp_file.flush()?;
|
||||
|
||||
|
||||
// Create a HashMap of variables
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("greeting", "Hi");
|
||||
vars.insert("name", "Bob");
|
||||
|
||||
|
||||
// Create a template builder and add variables from HashMap
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
builder = builder.add_vars(vars);
|
||||
|
||||
|
||||
// Render the template
|
||||
let result = builder.render()?;
|
||||
assert_eq!(result, "Hi, Bob!\n");
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
@ -279,20 +292,19 @@ mod tests {
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
let template_content = "{{ message }}\n";
|
||||
fs::write(temp_file.path(), template_content)?;
|
||||
|
||||
|
||||
|
||||
// Create an output file
|
||||
let output_file = NamedTempFile::new()?;
|
||||
|
||||
|
||||
// Create a template builder, add a variable, and render to file
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
builder = builder.add_var("message", "This is a test");
|
||||
builder.render_to_file(output_file.path())?;
|
||||
|
||||
|
||||
// Read the output file and verify its contents
|
||||
let content = fs::read_to_string(output_file.path())?;
|
||||
assert_eq!(content, "This is a test\n");
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||
use std::str::FromStr;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::hero_vault::error::CryptoError;
|
||||
use crate::vault::error::CryptoError;
|
||||
use super::wallet::EthereumWallet;
|
||||
use super::networks::NetworkConfig;
|
||||
|
@ -16,11 +16,8 @@ mod provider;
|
||||
mod transaction;
|
||||
mod storage;
|
||||
mod contract;
|
||||
mod contract_utils;
|
||||
pub mod contract_utils;
|
||||
pub mod networks;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
// Re-export public types and functions
|
||||
pub use wallet::EthereumWallet;
|
||||
pub use networks::NetworkConfig;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user