feat: Add support for new OS package
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run

- Add a new `sal-os` package containing OS interaction utilities.
- Update workspace members to include the new package.
- Add README and basic usage examples for the new package.
This commit is contained in:
Mahmoud-Emad 2025-06-21 15:45:43 +03:00
parent a35edc2030
commit c4cdb8126c
27 changed files with 1735 additions and 424 deletions

View File

@ -11,7 +11,7 @@ categories = ["os", "filesystem", "api-bindings"]
readme = "README.md" readme = "README.md"
[workspace] [workspace]
members = [".", "vault", "git", "redisclient", "mycelium", "text"] members = [".", "vault", "git", "redisclient", "mycelium", "text", "os"]
[dependencies] [dependencies]
hex = "0.4" hex = "0.4"
@ -64,6 +64,7 @@ sal-git = { path = "git" }
sal-redisclient = { path = "redisclient" } sal-redisclient = { path = "redisclient" }
sal-mycelium = { path = "mycelium" } sal-mycelium = { path = "mycelium" }
sal-text = { path = "text" } sal-text = { path = "text" }
sal-os = { path = "os" }
# Optional features for specific OS functionality # Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]

36
os/Cargo.toml Normal file
View File

@ -0,0 +1,36 @@
[package]
name = "sal-os"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL OS - Operating system interaction utilities with cross-platform abstraction"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
keywords = ["system", "os", "filesystem", "download", "package-management"]
categories = ["os", "filesystem", "api-bindings"]
[dependencies]
# Core dependencies for file system operations
dirs = "6.0.0"
glob = "0.3.1"
libc = "0.2"
# Error handling
thiserror = "2.0.12"
# Rhai scripting support
rhai = { version = "1.12.0", features = ["sync"] }
# Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies]
nix = "0.30.1"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61.1", features = [
"Win32_Foundation",
"Win32_System_Threading",
"Win32_Storage_FileSystem",
] }
[dev-dependencies]
tempfile = "3.5"

104
os/README.md Normal file
View File

@ -0,0 +1,104 @@
# SAL OS Package (`sal-os`)
The `sal-os` package provides a comprehensive suite of operating system interaction utilities. It offers a cross-platform abstraction layer for common OS-level tasks, simplifying system programming in Rust.
## Features
- **File System Operations**: Comprehensive file and directory manipulation
- **Download Utilities**: File downloading with automatic extraction support
- **Package Management**: System package manager integration
- **Platform Detection**: Cross-platform OS and architecture detection
- **Rhai Integration**: Full scripting support for all OS operations
## Modules
- `fs`: File system operations (create, copy, delete, find, etc.)
- `download`: File downloading and basic installation
- `package`: System package management
- `platform`: Platform and architecture detection
## Usage
Add this to your `Cargo.toml`:
```toml
[dependencies]
sal-os = "0.1.0"
```
### File System Operations
```rust
use sal_os::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create directory
fs::mkdir("my_dir")?;
// Write and read files
fs::file_write("my_dir/example.txt", "Hello from SAL!")?;
let content = fs::file_read("my_dir/example.txt")?;
// Find files
let files = fs::find_files(".", "*.txt")?;
Ok(())
}
```
### Download Operations
```rust
use sal_os::download;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Download and extract archive
let path = download::download("https://example.com/archive.tar.gz", "/tmp", 1024)?;
// Download specific file
download::download_file("https://example.com/script.sh", "/tmp/script.sh", 0)?;
download::chmod_exec("/tmp/script.sh")?;
Ok(())
}
```
### Platform Detection
```rust
use sal_os::platform;
fn main() {
if platform::is_linux() {
println!("Running on Linux");
}
if platform::is_arm() {
println!("ARM architecture detected");
}
}
```
## Rhai Integration
The package provides full Rhai scripting support:
```rhai
// File operations
mkdir("test_dir");
file_write("test_dir/hello.txt", "Hello World!");
let content = file_read("test_dir/hello.txt");
// Download operations
download("https://example.com/file.zip", "/tmp", 0);
chmod_exec("/tmp/script.sh");
// Platform detection
if is_linux() {
print("Running on Linux");
}
```
## License
Licensed under the Apache License, Version 2.0.

13
os/src/lib.rs Normal file
View File

@ -0,0 +1,13 @@
pub mod download;
pub mod fs;
pub mod package;
pub mod platform;
// Re-export all public functions and types
pub use download::*;
pub use fs::*;
pub use package::*;
pub use platform::*;
// Rhai integration module
pub mod rhai;

View File

@ -1,6 +1,14 @@
use crate::process::CommandResult;
use std::process::Command; use std::process::Command;
/// A structure to hold command execution results
#[derive(Debug, Clone)]
pub struct CommandResult {
pub stdout: String,
pub stderr: String,
pub success: bool,
pub code: i32,
}
/// Error type for package management operations /// Error type for package management operations
#[derive(Debug)] #[derive(Debug)]
pub enum PackageError { pub enum PackageError {

View File

@ -1,4 +1,16 @@
use crate::rhai::error::SalError; use thiserror::Error;
#[derive(Debug, Error)]
pub enum PlatformError {
#[error("{0}: {1}")]
Generic(String, String),
}
impl PlatformError {
pub fn new(kind: &str, message: &str) -> Self {
PlatformError::Generic(kind.to_string(), message.to_string())
}
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn is_osx() -> bool { pub fn is_osx() -> bool {
@ -40,24 +52,24 @@ pub fn is_x86() -> bool {
false false
} }
pub fn check_linux_x86() -> Result<(), SalError> { pub fn check_linux_x86() -> Result<(), PlatformError> {
if is_linux() && is_x86() { if is_linux() && is_x86() {
Ok(()) Ok(())
} else { } else {
Err(SalError::Generic( Err(PlatformError::new(
"Platform Check Error".to_string(), "Platform Check Error",
"This operation is only supported on Linux x86_64.".to_string(), "This operation is only supported on Linux x86_64.",
)) ))
} }
} }
pub fn check_macos_arm() -> Result<(), SalError> { pub fn check_macos_arm() -> Result<(), PlatformError> {
if is_osx() && is_arm() { if is_osx() && is_arm() {
Ok(()) Ok(())
} else { } else {
Err(SalError::Generic( Err(PlatformError::new(
"Platform Check Error".to_string(), "Platform Check Error",
"This operation is only supported on macOS ARM.".to_string(), "This operation is only supported on macOS ARM.",
)) ))
} }
} }

View File

@ -2,10 +2,25 @@
//! //!
//! This module provides Rhai wrappers for the functions in the OS module. //! This module provides Rhai wrappers for the functions in the OS module.
use rhai::{Engine, EvalAltResult, Array}; use crate::package::PackHero;
use crate::os; use crate::{download as dl, fs, package};
use crate::os::package::PackHero; use rhai::{Array, Engine, EvalAltResult, Position};
use super::error::{ToRhaiError, register_error_types};
/// A trait for converting a Result to a Rhai-compatible error
pub trait ToRhaiError<T> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>>;
}
impl<T, E: std::error::Error> ToRhaiError<T> for Result<T, E> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
self.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.to_string().into(),
Position::NONE,
))
})
}
}
/// Register OS module functions with the Rhai engine /// Register OS module functions with the Rhai engine
/// ///
@ -17,9 +32,6 @@ use super::error::{ToRhaiError, register_error_types};
/// ///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise /// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register error types
register_error_types(engine)?;
// Register file system functions // Register file system functions
engine.register_fn("copy", copy); engine.register_fn("copy", copy);
engine.register_fn("copy_bin", copy_bin); engine.register_fn("copy_bin", copy_bin);
@ -36,20 +48,20 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
engine.register_fn("file_read", file_read); engine.register_fn("file_read", file_read);
engine.register_fn("file_write", file_write); engine.register_fn("file_write", file_write);
engine.register_fn("file_write_append", file_write_append); engine.register_fn("file_write_append", file_write_append);
// Register command check functions // Register command check functions
engine.register_fn("which", which); engine.register_fn("which", which);
engine.register_fn("cmd_ensure_exists", cmd_ensure_exists); engine.register_fn("cmd_ensure_exists", cmd_ensure_exists);
// Register download functions // Register download functions
engine.register_fn("download", download); engine.register_fn("download", download);
engine.register_fn("download_file", download_file); engine.register_fn("download_file", download_file);
engine.register_fn("download_install", download_install); engine.register_fn("download_install", download_install);
engine.register_fn("chmod_exec", chmod_exec); engine.register_fn("chmod_exec", chmod_exec);
// Register move function // Register move function
engine.register_fn("mv", mv); engine.register_fn("mv", mv);
// Register package management functions // Register package management functions
engine.register_fn("package_install", package_install); engine.register_fn("package_install", package_install);
engine.register_fn("package_remove", package_remove); engine.register_fn("package_remove", package_remove);
@ -60,7 +72,15 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
engine.register_fn("package_is_installed", package_is_installed); engine.register_fn("package_is_installed", package_is_installed);
engine.register_fn("package_set_debug", package_set_debug); engine.register_fn("package_set_debug", package_set_debug);
engine.register_fn("package_platform", package_platform); engine.register_fn("package_platform", package_platform);
// Register platform detection functions
engine.register_fn("platform_is_osx", platform_is_osx);
engine.register_fn("platform_is_linux", platform_is_linux);
engine.register_fn("platform_is_arm", platform_is_arm);
engine.register_fn("platform_is_x86", platform_is_x86);
engine.register_fn("platform_check_linux_x86", platform_check_linux_x86);
engine.register_fn("platform_check_macos_arm", platform_check_macos_arm);
Ok(()) Ok(())
} }
@ -68,132 +88,132 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
// File System Function Wrappers // File System Function Wrappers
// //
/// Wrapper for os::copy /// Wrapper for fs::copy
/// ///
/// Recursively copy a file or directory from source to destination. /// Recursively copy a file or directory from source to destination.
pub fn copy(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> { pub fn copy(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
os::copy(src, dest).to_rhai_error() fs::copy(src, dest).to_rhai_error()
} }
/// Wrapper for os::copy_bin /// Wrapper for fs::copy_bin
/// ///
/// Copy a binary to the correct location based on OS and user privileges. /// Copy a binary to the correct location based on OS and user privileges.
pub fn copy_bin(src: &str) -> Result<String, Box<EvalAltResult>> { pub fn copy_bin(src: &str) -> Result<String, Box<EvalAltResult>> {
os::copy_bin(src).to_rhai_error() fs::copy_bin(src).to_rhai_error()
} }
/// Wrapper for os::exist /// Wrapper for fs::exist
/// ///
/// Check if a file or directory exists. /// Check if a file or directory exists.
pub fn exist(path: &str) -> bool { pub fn exist(path: &str) -> bool {
os::exist(path) fs::exist(path)
} }
/// Wrapper for os::find_file /// Wrapper for fs::find_file
/// ///
/// Find a file in a directory (with support for wildcards). /// Find a file in a directory (with support for wildcards).
pub fn find_file(dir: &str, filename: &str) -> Result<String, Box<EvalAltResult>> { pub fn find_file(dir: &str, filename: &str) -> Result<String, Box<EvalAltResult>> {
os::find_file(dir, filename).to_rhai_error() fs::find_file(dir, filename).to_rhai_error()
} }
/// Wrapper for os::find_files /// Wrapper for fs::find_files
/// ///
/// Find multiple files in a directory (recursive, with support for wildcards). /// Find multiple files in a directory (recursive, with support for wildcards).
pub fn find_files(dir: &str, filename: &str) -> Result<Array, Box<EvalAltResult>> { pub fn find_files(dir: &str, filename: &str) -> Result<Array, Box<EvalAltResult>> {
let files = os::find_files(dir, filename).to_rhai_error()?; let files = fs::find_files(dir, filename).to_rhai_error()?;
// Convert Vec<String> to Rhai Array // Convert Vec<String> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
for file in files { for file in files {
array.push(file.into()); array.push(file.into());
} }
Ok(array) Ok(array)
} }
/// Wrapper for os::find_dir /// Wrapper for fs::find_dir
/// ///
/// Find a directory in a parent directory (with support for wildcards). /// Find a directory in a parent directory (with support for wildcards).
pub fn find_dir(dir: &str, dirname: &str) -> Result<String, Box<EvalAltResult>> { pub fn find_dir(dir: &str, dirname: &str) -> Result<String, Box<EvalAltResult>> {
os::find_dir(dir, dirname).to_rhai_error() fs::find_dir(dir, dirname).to_rhai_error()
} }
/// Wrapper for os::find_dirs /// Wrapper for fs::find_dirs
/// ///
/// Find multiple directories in a parent directory (recursive, with support for wildcards). /// Find multiple directories in a parent directory (recursive, with support for wildcards).
pub fn find_dirs(dir: &str, dirname: &str) -> Result<Array, Box<EvalAltResult>> { pub fn find_dirs(dir: &str, dirname: &str) -> Result<Array, Box<EvalAltResult>> {
let dirs = os::find_dirs(dir, dirname).to_rhai_error()?; let dirs = fs::find_dirs(dir, dirname).to_rhai_error()?;
// Convert Vec<String> to Rhai Array // Convert Vec<String> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
for dir in dirs { for dir in dirs {
array.push(dir.into()); array.push(dir.into());
} }
Ok(array) Ok(array)
} }
/// Wrapper for os::delete /// Wrapper for fs::delete
/// ///
/// Delete a file or directory (defensive - doesn't error if file doesn't exist). /// Delete a file or directory (defensive - doesn't error if file doesn't exist).
pub fn delete(path: &str) -> Result<String, Box<EvalAltResult>> { pub fn delete(path: &str) -> Result<String, Box<EvalAltResult>> {
os::delete(path).to_rhai_error() fs::delete(path).to_rhai_error()
} }
/// Wrapper for os::mkdir /// Wrapper for fs::mkdir
/// ///
/// Create a directory and all parent directories (defensive - doesn't error if directory exists). /// Create a directory and all parent directories (defensive - doesn't error if directory exists).
pub fn mkdir(path: &str) -> Result<String, Box<EvalAltResult>> { pub fn mkdir(path: &str) -> Result<String, Box<EvalAltResult>> {
os::mkdir(path).to_rhai_error() fs::mkdir(path).to_rhai_error()
} }
/// Wrapper for os::file_size /// Wrapper for fs::file_size
/// ///
/// Get the size of a file in bytes. /// Get the size of a file in bytes.
pub fn file_size(path: &str) -> Result<i64, Box<EvalAltResult>> { pub fn file_size(path: &str) -> Result<i64, Box<EvalAltResult>> {
os::file_size(path).to_rhai_error() fs::file_size(path).to_rhai_error()
} }
/// Wrapper for os::rsync /// Wrapper for fs::rsync
/// ///
/// Sync directories using rsync (or platform equivalent). /// Sync directories using rsync (or platform equivalent).
pub fn rsync(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> { pub fn rsync(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
os::rsync(src, dest).to_rhai_error() fs::rsync(src, dest).to_rhai_error()
} }
/// Wrapper for os::chdir /// Wrapper for fs::chdir
/// ///
/// Change the current working directory. /// Change the current working directory.
pub fn chdir(path: &str) -> Result<String, Box<EvalAltResult>> { pub fn chdir(path: &str) -> Result<String, Box<EvalAltResult>> {
os::chdir(path).to_rhai_error() fs::chdir(path).to_rhai_error()
} }
/// Wrapper for os::file_read /// Wrapper for fs::file_read
/// ///
/// Read the contents of a file. /// Read the contents of a file.
pub fn file_read(path: &str) -> Result<String, Box<EvalAltResult>> { pub fn file_read(path: &str) -> Result<String, Box<EvalAltResult>> {
os::file_read(path).to_rhai_error() fs::file_read(path).to_rhai_error()
} }
/// Wrapper for os::file_write /// Wrapper for fs::file_write
/// ///
/// Write content to a file (creates the file if it doesn't exist, overwrites if it does). /// Write content to a file (creates the file if it doesn't exist, overwrites if it does).
pub fn file_write(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> { pub fn file_write(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> {
os::file_write(path, content).to_rhai_error() fs::file_write(path, content).to_rhai_error()
} }
/// Wrapper for os::file_write_append /// Wrapper for fs::file_write_append
/// ///
/// Append content to a file (creates the file if it doesn't exist). /// Append content to a file (creates the file if it doesn't exist).
pub fn file_write_append(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> { pub fn file_write_append(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> {
os::file_write_append(path, content).to_rhai_error() fs::file_write_append(path, content).to_rhai_error()
} }
/// Wrapper for os::mv /// Wrapper for fs::mv
/// ///
/// Move a file or directory from source to destination. /// Move a file or directory from source to destination.
pub fn mv(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> { pub fn mv(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
os::mv(src, dest).to_rhai_error() fs::mv(src, dest).to_rhai_error()
} }
// //
@ -204,35 +224,39 @@ pub fn mv(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
/// ///
/// Download a file from URL to destination using the curl command. /// Download a file from URL to destination using the curl command.
pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> { pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
os::download(url, dest, min_size_kb).to_rhai_error() dl::download(url, dest, min_size_kb).to_rhai_error()
} }
/// Wrapper for os::download_file /// Wrapper for os::download_file
/// ///
/// Download a file from URL to a specific file destination using the curl command. /// Download a file from URL to a specific file destination using the curl command.
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> { pub fn download_file(
os::download_file(url, dest, min_size_kb).to_rhai_error() url: &str,
dest: &str,
min_size_kb: i64,
) -> Result<String, Box<EvalAltResult>> {
dl::download_file(url, dest, min_size_kb).to_rhai_error()
} }
/// Wrapper for os::download_install /// Wrapper for os::download_install
/// ///
/// Download a file and install it if it's a supported package format. /// Download a file and install it if it's a supported package format.
pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> { pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
os::download_install(url, min_size_kb).to_rhai_error() dl::download_install(url, min_size_kb).to_rhai_error()
} }
/// Wrapper for os::chmod_exec /// Wrapper for os::chmod_exec
/// ///
/// Make a file executable (equivalent to chmod +x). /// Make a file executable (equivalent to chmod +x).
pub fn chmod_exec(path: &str) -> Result<String, Box<EvalAltResult>> { pub fn chmod_exec(path: &str) -> Result<String, Box<EvalAltResult>> {
os::chmod_exec(path).to_rhai_error() dl::chmod_exec(path).to_rhai_error()
} }
/// Wrapper for os::which /// Wrapper for os::which
/// ///
/// Check if a command exists in the system PATH. /// Check if a command exists in the system PATH.
pub fn which(command: &str) -> String { pub fn which(command: &str) -> String {
os::which(command) fs::which(command)
} }
/// Wrapper for os::cmd_ensure_exists /// Wrapper for os::cmd_ensure_exists
@ -240,7 +264,7 @@ pub fn which(command: &str) -> String {
/// Ensure that one or more commands exist in the system PATH. /// Ensure that one or more commands exist in the system PATH.
/// If any command doesn't exist, an error is thrown. /// If any command doesn't exist, an error is thrown.
pub fn cmd_ensure_exists(commands: &str) -> Result<String, Box<EvalAltResult>> { pub fn cmd_ensure_exists(commands: &str) -> Result<String, Box<EvalAltResult>> {
os::cmd_ensure_exists(commands).to_rhai_error() fs::cmd_ensure_exists(commands).to_rhai_error()
} }
// //
@ -293,13 +317,13 @@ pub fn package_upgrade() -> Result<String, Box<EvalAltResult>> {
pub fn package_list() -> Result<Array, Box<EvalAltResult>> { pub fn package_list() -> Result<Array, Box<EvalAltResult>> {
let hero = PackHero::new(); let hero = PackHero::new();
let packages = hero.list_installed().to_rhai_error()?; let packages = hero.list_installed().to_rhai_error()?;
// Convert Vec<String> to Rhai Array // Convert Vec<String> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
for package in packages { for package in packages {
array.push(package.into()); array.push(package.into());
} }
Ok(array) Ok(array)
} }
@ -309,13 +333,13 @@ pub fn package_list() -> Result<Array, Box<EvalAltResult>> {
pub fn package_search(query: &str) -> Result<Array, Box<EvalAltResult>> { pub fn package_search(query: &str) -> Result<Array, Box<EvalAltResult>> {
let hero = PackHero::new(); let hero = PackHero::new();
let packages = hero.search(query).to_rhai_error()?; let packages = hero.search(query).to_rhai_error()?;
// Convert Vec<String> to Rhai Array // Convert Vec<String> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
for package in packages { for package in packages {
array.push(package.into()); array.push(package.into());
} }
Ok(array) Ok(array)
} }
@ -336,12 +360,12 @@ thread_local! {
pub fn package_set_debug(debug: bool) -> bool { pub fn package_set_debug(debug: bool) -> bool {
let mut hero = PackHero::new(); let mut hero = PackHero::new();
hero.set_debug(debug); hero.set_debug(debug);
// Also set the thread-local debug flag // Also set the thread-local debug flag
PACKAGE_DEBUG.with(|cell| { PACKAGE_DEBUG.with(|cell| {
*cell.borrow_mut() = debug; *cell.borrow_mut() = debug;
}); });
debug debug
} }
@ -349,8 +373,52 @@ pub fn package_set_debug(debug: bool) -> bool {
pub fn package_platform() -> String { pub fn package_platform() -> String {
let hero = PackHero::new(); let hero = PackHero::new();
match hero.platform() { match hero.platform() {
os::package::Platform::Ubuntu => "Ubuntu".to_string(), package::Platform::Ubuntu => "Ubuntu".to_string(),
os::package::Platform::MacOS => "MacOS".to_string(), package::Platform::MacOS => "MacOS".to_string(),
os::package::Platform::Unknown => "Unknown".to_string(), package::Platform::Unknown => "Unknown".to_string(),
} }
} }
//
// Platform Detection Function Wrappers
//
/// Wrapper for platform::is_osx
pub fn platform_is_osx() -> bool {
crate::platform::is_osx()
}
/// Wrapper for platform::is_linux
pub fn platform_is_linux() -> bool {
crate::platform::is_linux()
}
/// Wrapper for platform::is_arm
pub fn platform_is_arm() -> bool {
crate::platform::is_arm()
}
/// Wrapper for platform::is_x86
pub fn platform_is_x86() -> bool {
crate::platform::is_x86()
}
/// Wrapper for platform::check_linux_x86
pub fn platform_check_linux_x86() -> Result<(), Box<EvalAltResult>> {
crate::platform::check_linux_x86().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Platform Check Error: {}", e).into(),
Position::NONE,
))
})
}
/// Wrapper for platform::check_macos_arm
pub fn platform_check_macos_arm() -> Result<(), Box<EvalAltResult>> {
crate::platform::check_macos_arm().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Platform Check Error: {}", e).into(),
Position::NONE,
))
})
}

208
os/tests/download_tests.rs Normal file
View File

@ -0,0 +1,208 @@
use sal_os::{download, DownloadError};
use std::fs;
use tempfile::TempDir;
#[test]
fn test_chmod_exec() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_script.sh");
// Create a test file
fs::write(&test_file, "#!/bin/bash\necho 'test'").unwrap();
// Make it executable
let result = download::chmod_exec(test_file.to_str().unwrap());
assert!(result.is_ok());
// Check if file is executable (Unix only)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let metadata = fs::metadata(&test_file).unwrap();
let permissions = metadata.permissions();
assert!(permissions.mode() & 0o111 != 0); // Check if any execute bit is set
}
}
#[test]
fn test_download_error_handling() {
let temp_dir = TempDir::new().unwrap();
// Test with invalid URL
let result = download::download("invalid-url", temp_dir.path().to_str().unwrap(), 0);
assert!(result.is_err());
// Test with non-existent domain
let result = download::download(
"https://nonexistentdomain12345.com/file.txt",
temp_dir.path().to_str().unwrap(),
0,
);
assert!(result.is_err());
}
#[test]
fn test_download_file_error_handling() {
let temp_dir = TempDir::new().unwrap();
let dest_file = temp_dir.path().join("downloaded_file.txt");
// Test with invalid URL
let result = download::download_file("invalid-url", dest_file.to_str().unwrap(), 0);
assert!(result.is_err());
// Test with non-existent domain
let result = download::download_file(
"https://nonexistentdomain12345.com/file.txt",
dest_file.to_str().unwrap(),
0,
);
assert!(result.is_err());
}
#[test]
fn test_download_install_error_handling() {
// Test with invalid URL
let result = download::download_install("invalid-url", 0);
assert!(result.is_err());
// Test with non-existent domain
let result = download::download_install("https://nonexistentdomain12345.com/package.deb", 0);
assert!(result.is_err());
}
#[test]
fn test_download_minimum_size_validation() {
let temp_dir = TempDir::new().unwrap();
// Test with a very high minimum size requirement that won't be met
// This should fail even if the URL exists
let result = download::download(
"https://httpbin.org/bytes/10", // This returns only 10 bytes
temp_dir.path().to_str().unwrap(),
1000, // Require 1000KB minimum
);
// This might succeed or fail depending on network, but we're testing the interface
// The important thing is that it doesn't panic
let _ = result;
}
#[test]
fn test_download_to_nonexistent_directory() {
// Test downloading to a directory that doesn't exist
// The download function should create parent directories
let temp_dir = TempDir::new().unwrap();
let nonexistent_dir = temp_dir.path().join("nonexistent").join("nested");
let _ = download::download(
"https://httpbin.org/status/404", // This will fail, but directory creation should work
nonexistent_dir.to_str().unwrap(),
0,
);
// The directory should be created even if download fails
assert!(nonexistent_dir.exists());
}
#[test]
fn test_chmod_exec_nonexistent_file() {
// Test chmod_exec on a file that doesn't exist
let result = download::chmod_exec("/nonexistent/path/file.sh");
assert!(result.is_err());
}
#[test]
fn test_download_file_path_validation() {
let _ = TempDir::new().unwrap();
// Test with invalid destination path
let result = download::download_file(
"https://httpbin.org/status/404",
"/invalid/path/that/does/not/exist/file.txt",
0,
);
assert!(result.is_err());
}
// Integration test that requires network access
// This test is marked with ignore so it doesn't run by default
#[test]
#[ignore]
fn test_download_real_file() {
let temp_dir = TempDir::new().unwrap();
// Download a small file from httpbin (a testing service)
let result = download::download(
"https://httpbin.org/bytes/100", // Returns 100 random bytes
temp_dir.path().to_str().unwrap(),
0,
);
if result.is_ok() {
// If download succeeded, verify the file exists
let downloaded_path = result.unwrap();
assert!(fs::metadata(&downloaded_path).is_ok());
// Verify file size is approximately correct
let metadata = fs::metadata(&downloaded_path).unwrap();
assert!(metadata.len() >= 90 && metadata.len() <= 110); // Allow some variance
}
// If download failed (network issues), that's okay for this test
}
// Integration test for download_file
#[test]
#[ignore]
fn test_download_file_real() {
let temp_dir = TempDir::new().unwrap();
let dest_file = temp_dir.path().join("test_download.bin");
// Download a small file to specific location
let result = download::download_file(
"https://httpbin.org/bytes/50",
dest_file.to_str().unwrap(),
0,
);
if result.is_ok() {
// Verify the file was created at the specified location
assert!(dest_file.exists());
// Verify file size
let metadata = fs::metadata(&dest_file).unwrap();
assert!(metadata.len() >= 40 && metadata.len() <= 60); // Allow some variance
}
}
#[test]
fn test_download_error_types() {
// DownloadError is already imported at the top
// Test that our error types can be created and displayed
let error = DownloadError::InvalidUrl("test".to_string());
assert!(!error.to_string().is_empty());
let error = DownloadError::DownloadFailed("test".to_string());
assert!(!error.to_string().is_empty());
let error = DownloadError::FileTooSmall(50, 100);
assert!(!error.to_string().is_empty());
}
#[test]
fn test_download_url_parsing() {
let temp_dir = TempDir::new().unwrap();
// Test with URL that has no filename
let result = download::download("https://example.com/", temp_dir.path().to_str().unwrap(), 0);
// Should fail with invalid URL error
assert!(result.is_err());
// Test with URL that has query parameters
let result = download::download(
"https://httpbin.org/get?param=value",
temp_dir.path().to_str().unwrap(),
0,
);
// This might succeed or fail depending on network, but shouldn't panic
let _ = result;
}

212
os/tests/fs_tests.rs Normal file
View File

@ -0,0 +1,212 @@
use sal_os::fs;
use std::fs as std_fs;
use tempfile::TempDir;
#[test]
fn test_exist() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
// Test directory exists
assert!(fs::exist(temp_path.to_str().unwrap()));
// Test file doesn't exist
let non_existent = temp_path.join("non_existent.txt");
assert!(!fs::exist(non_existent.to_str().unwrap()));
// Create a file and test it exists
let test_file = temp_path.join("test.txt");
std_fs::write(&test_file, "test content").unwrap();
assert!(fs::exist(test_file.to_str().unwrap()));
}
#[test]
fn test_mkdir() {
let temp_dir = TempDir::new().unwrap();
let new_dir = temp_dir.path().join("new_directory");
// Directory shouldn't exist initially
assert!(!fs::exist(new_dir.to_str().unwrap()));
// Create directory
let result = fs::mkdir(new_dir.to_str().unwrap());
assert!(result.is_ok());
// Directory should now exist
assert!(fs::exist(new_dir.to_str().unwrap()));
// Creating existing directory should not error (defensive)
let result2 = fs::mkdir(new_dir.to_str().unwrap());
assert!(result2.is_ok());
}
#[test]
fn test_file_write_and_read() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_write.txt");
let content = "Hello, World!";
// Write file
let write_result = fs::file_write(test_file.to_str().unwrap(), content);
assert!(write_result.is_ok());
// File should exist
assert!(fs::exist(test_file.to_str().unwrap()));
// Read file
let read_result = fs::file_read(test_file.to_str().unwrap());
assert!(read_result.is_ok());
assert_eq!(read_result.unwrap(), content);
}
#[test]
fn test_file_write_append() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_append.txt");
// Write initial content
let initial_content = "Line 1\n";
let append_content = "Line 2\n";
let write_result = fs::file_write(test_file.to_str().unwrap(), initial_content);
assert!(write_result.is_ok());
// Append content
let append_result = fs::file_write_append(test_file.to_str().unwrap(), append_content);
assert!(append_result.is_ok());
// Read and verify
let read_result = fs::file_read(test_file.to_str().unwrap());
assert!(read_result.is_ok());
assert_eq!(read_result.unwrap(), format!("{}{}", initial_content, append_content));
}
#[test]
fn test_file_size() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_size.txt");
let content = "Hello, World!"; // 13 bytes
// Write file
fs::file_write(test_file.to_str().unwrap(), content).unwrap();
// Check size
let size_result = fs::file_size(test_file.to_str().unwrap());
assert!(size_result.is_ok());
assert_eq!(size_result.unwrap(), 13);
}
#[test]
fn test_delete() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_delete.txt");
// Create file
fs::file_write(test_file.to_str().unwrap(), "test").unwrap();
assert!(fs::exist(test_file.to_str().unwrap()));
// Delete file
let delete_result = fs::delete(test_file.to_str().unwrap());
assert!(delete_result.is_ok());
// File should no longer exist
assert!(!fs::exist(test_file.to_str().unwrap()));
// Deleting non-existent file should not error (defensive)
let delete_result2 = fs::delete(test_file.to_str().unwrap());
assert!(delete_result2.is_ok());
}
#[test]
fn test_copy() {
let temp_dir = TempDir::new().unwrap();
let source_file = temp_dir.path().join("source.txt");
let dest_file = temp_dir.path().join("dest.txt");
let content = "Copy test content";
// Create source file
fs::file_write(source_file.to_str().unwrap(), content).unwrap();
// Copy file
let copy_result = fs::copy(source_file.to_str().unwrap(), dest_file.to_str().unwrap());
assert!(copy_result.is_ok());
// Destination should exist and have same content
assert!(fs::exist(dest_file.to_str().unwrap()));
let dest_content = fs::file_read(dest_file.to_str().unwrap()).unwrap();
assert_eq!(dest_content, content);
}
#[test]
fn test_mv() {
let temp_dir = TempDir::new().unwrap();
let source_file = temp_dir.path().join("source_mv.txt");
let dest_file = temp_dir.path().join("dest_mv.txt");
let content = "Move test content";
// Create source file
fs::file_write(source_file.to_str().unwrap(), content).unwrap();
// Move file
let mv_result = fs::mv(source_file.to_str().unwrap(), dest_file.to_str().unwrap());
assert!(mv_result.is_ok());
// Source should no longer exist, destination should exist
assert!(!fs::exist(source_file.to_str().unwrap()));
assert!(fs::exist(dest_file.to_str().unwrap()));
// Destination should have same content
let dest_content = fs::file_read(dest_file.to_str().unwrap()).unwrap();
assert_eq!(dest_content, content);
}
#[test]
fn test_which() {
// Test with a command that should exist on most systems
let result = fs::which("ls");
assert!(!result.is_empty());
// Test with a command that shouldn't exist
let result = fs::which("nonexistentcommand12345");
assert!(result.is_empty());
}
#[test]
fn test_find_files() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
// Create test files
fs::file_write(&temp_path.join("test1.txt").to_string_lossy(), "content1").unwrap();
fs::file_write(&temp_path.join("test2.txt").to_string_lossy(), "content2").unwrap();
fs::file_write(&temp_path.join("other.log").to_string_lossy(), "log content").unwrap();
// Find .txt files
let txt_files = fs::find_files(temp_path.to_str().unwrap(), "*.txt");
assert!(txt_files.is_ok());
let files = txt_files.unwrap();
assert_eq!(files.len(), 2);
// Find all files
let all_files = fs::find_files(temp_path.to_str().unwrap(), "*");
assert!(all_files.is_ok());
let files = all_files.unwrap();
assert!(files.len() >= 3); // At least our 3 files
}
#[test]
fn test_find_dirs() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
// Create test directories
fs::mkdir(&temp_path.join("dir1").to_string_lossy()).unwrap();
fs::mkdir(&temp_path.join("dir2").to_string_lossy()).unwrap();
fs::mkdir(&temp_path.join("subdir").to_string_lossy()).unwrap();
// Find directories
let dirs = fs::find_dirs(temp_path.to_str().unwrap(), "dir*");
assert!(dirs.is_ok());
let found_dirs = dirs.unwrap();
assert!(found_dirs.len() >= 2); // At least dir1 and dir2
}

366
os/tests/package_tests.rs Normal file
View File

@ -0,0 +1,366 @@
use sal_os::package::{PackHero, Platform};
#[test]
fn test_pack_hero_creation() {
// Test that we can create a PackHero instance
let hero = PackHero::new();
// Test that platform detection works
let platform = hero.platform();
match platform {
Platform::Ubuntu | Platform::MacOS | Platform::Unknown => {
// All valid platforms
}
}
}
#[test]
fn test_platform_detection() {
let hero = PackHero::new();
let platform = hero.platform();
// Platform should be deterministic
let platform2 = hero.platform();
assert_eq!(format!("{:?}", platform), format!("{:?}", platform2));
// Test platform display
match platform {
Platform::Ubuntu => {
assert_eq!(format!("{:?}", platform), "Ubuntu");
}
Platform::MacOS => {
assert_eq!(format!("{:?}", platform), "MacOS");
}
Platform::Unknown => {
assert_eq!(format!("{:?}", platform), "Unknown");
}
}
}
#[test]
fn test_debug_mode() {
let mut hero = PackHero::new();
// Test setting debug mode
hero.set_debug(true);
hero.set_debug(false);
// Debug mode setting should not panic
}
#[test]
fn test_package_operations_error_handling() {
let hero = PackHero::new();
// Test with invalid package name
let result = hero.is_installed("nonexistent-package-12345-xyz");
// This should return a result (either Ok(false) or Err)
// Validate that we get a proper result type
match result {
Ok(is_installed) => {
// Should return false for non-existent package
assert!(
!is_installed,
"Non-existent package should not be reported as installed"
);
}
Err(_) => {
// Error is also acceptable (e.g., no package manager available)
// The important thing is it doesn't panic
}
}
// Test install with invalid package
let result = hero.install("nonexistent-package-12345-xyz");
// This should return an error
assert!(result.is_err());
// Test remove with invalid package
let result = hero.remove("nonexistent-package-12345-xyz");
// This might succeed (if package wasn't installed) or fail
// Validate that we get a proper result type
match result {
Ok(_) => {
// Success is acceptable (package wasn't installed)
}
Err(err) => {
// Error is also acceptable
// Verify error message is meaningful
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error message should not be empty");
}
}
}
#[test]
fn test_package_search_basic() {
let hero = PackHero::new();
// Test search with empty query
let result = hero.search("");
// Should handle empty query gracefully
// Validate that we get a proper result type
match result {
Ok(packages) => {
// Empty search might return all packages or empty list
// Verify the result is a valid vector
assert!(
packages.len() < 50000,
"Empty search returned unreasonably large result"
);
}
Err(err) => {
// Error is acceptable for empty query
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error message should not be empty");
}
}
// Test search with very specific query that likely won't match
let result = hero.search("nonexistent-package-xyz-12345");
if let Ok(packages) = result {
// If search succeeded, it should return a vector
// The vector should be valid (we can get its length)
let _count = packages.len();
// Search results should be reasonable (not absurdly large)
assert!(
packages.len() < 10000,
"Search returned unreasonably large result set"
);
}
// If search failed, that's also acceptable
}
#[test]
fn test_package_list_basic() {
let hero = PackHero::new();
// Test listing installed packages
let result = hero.list_installed();
if let Ok(packages) = result {
// If listing succeeded, it should return a vector
// On most systems, there should be at least some packages installed
println!("Found {} installed packages", packages.len());
}
// If listing failed (e.g., no package manager available), that's acceptable
}
#[test]
fn test_package_update_basic() {
let hero = PackHero::new();
// Test package list update
let result = hero.update();
// This might succeed or fail depending on permissions and network
// Validate that we get a proper result type
match result {
Ok(_) => {
// Success is good - package list was updated
}
Err(err) => {
// Error is acceptable (no permissions, no network, etc.)
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error message should not be empty");
// Common error patterns we expect
let error_lower = error_msg.to_lowercase();
assert!(
error_lower.contains("permission")
|| error_lower.contains("network")
|| error_lower.contains("command")
|| error_lower.contains("not found")
|| error_lower.contains("failed"),
"Error message should indicate a reasonable failure cause: {}",
error_msg
);
}
}
}
#[test]
#[ignore] // Skip by default as this can take a very long time and modify the system
fn test_package_upgrade_basic() {
let hero = PackHero::new();
// Test package upgrade (this is a real system operation)
let result = hero.upgrade();
// Validate that we get a proper result type
match result {
Ok(_) => {
// Success means packages were upgraded
println!("Package upgrade completed successfully");
}
Err(err) => {
// Error is acceptable (no permissions, no packages to upgrade, etc.)
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error message should not be empty");
println!("Package upgrade failed as expected: {}", error_msg);
}
}
}
#[test]
fn test_package_upgrade_interface() {
// Test that the upgrade interface works without actually upgrading
let hero = PackHero::new();
// Verify that PackHero has the upgrade method and it returns the right type
// This tests the interface without performing the actual upgrade
let _upgrade_fn = PackHero::upgrade;
// Test that we can call upgrade (it will likely fail due to permissions/network)
// but we're testing that the interface works correctly
let result = hero.upgrade();
// The result should be a proper Result type
match result {
Ok(_) => {
// Upgrade succeeded (unlikely in test environment)
}
Err(err) => {
// Expected in most test environments
// Verify error is meaningful
let error_msg = err.to_string();
assert!(!error_msg.is_empty(), "Error should have a message");
assert!(error_msg.len() > 5, "Error message should be descriptive");
}
}
}
// Platform-specific tests
#[cfg(target_os = "linux")]
#[test]
fn test_linux_platform_detection() {
let hero = PackHero::new();
let platform = hero.platform();
// On Linux, should detect Ubuntu or Unknown (if not Ubuntu-based)
match platform {
Platform::Ubuntu | Platform::Unknown => {
// Expected on Linux
}
Platform::MacOS => {
panic!("Should not detect macOS on Linux system");
}
}
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_platform_detection() {
let hero = PackHero::new();
let platform = hero.platform();
// On macOS, should detect MacOS
match platform {
Platform::MacOS => {
// Expected on macOS
}
Platform::Ubuntu | Platform::Unknown => {
panic!("Should detect macOS on macOS system, got {:?}", platform);
}
}
}
// Integration tests that require actual package managers
// These are marked with ignore so they don't run by default
#[test]
#[ignore]
fn test_real_package_check() {
let hero = PackHero::new();
// Test with a package that's commonly installed
#[cfg(target_os = "linux")]
let test_package = "bash";
#[cfg(target_os = "macos")]
let test_package = "bash";
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let test_package = "unknown";
let result = hero.is_installed(test_package);
if let Ok(is_installed) = result {
println!("Package '{}' is installed: {}", test_package, is_installed);
} else {
println!(
"Failed to check if '{}' is installed: {:?}",
test_package, result
);
}
}
#[test]
#[ignore]
fn test_real_package_search() {
let hero = PackHero::new();
// Search for a common package
let result = hero.search("git");
if let Ok(packages) = result {
println!("Found {} packages matching 'git'", packages.len());
if !packages.is_empty() {
println!(
"First few matches: {:?}",
&packages[..std::cmp::min(5, packages.len())]
);
}
} else {
println!("Package search failed: {:?}", result);
}
}
#[test]
#[ignore]
fn test_real_package_list() {
let hero = PackHero::new();
// List installed packages
let result = hero.list_installed();
if let Ok(packages) = result {
println!("Total installed packages: {}", packages.len());
if !packages.is_empty() {
println!(
"First few packages: {:?}",
&packages[..std::cmp::min(10, packages.len())]
);
}
} else {
println!("Package listing failed: {:?}", result);
}
}
#[test]
fn test_platform_enum_properties() {
// Test that Platform enum can be compared
assert_eq!(Platform::Ubuntu, Platform::Ubuntu);
assert_eq!(Platform::MacOS, Platform::MacOS);
assert_eq!(Platform::Unknown, Platform::Unknown);
assert_ne!(Platform::Ubuntu, Platform::MacOS);
assert_ne!(Platform::Ubuntu, Platform::Unknown);
assert_ne!(Platform::MacOS, Platform::Unknown);
}
#[test]
fn test_pack_hero_multiple_instances() {
// Test that multiple PackHero instances work correctly
let hero1 = PackHero::new();
let hero2 = PackHero::new();
// Both should detect the same platform
assert_eq!(
format!("{:?}", hero1.platform()),
format!("{:?}", hero2.platform())
);
// Both should handle debug mode independently
let mut hero1_mut = hero1;
let mut hero2_mut = hero2;
hero1_mut.set_debug(true);
hero2_mut.set_debug(false);
// No assertions here since debug mode doesn't have observable effects in tests
// But this ensures the API works correctly
}

199
os/tests/platform_tests.rs Normal file
View File

@ -0,0 +1,199 @@
use sal_os::platform;
#[test]
fn test_platform_detection_consistency() {
// Test that platform detection functions return consistent results
let is_osx = platform::is_osx();
let is_linux = platform::is_linux();
// On any given system, only one of these should be true
// (or both false if running on Windows or other OS)
if is_osx {
assert!(!is_linux, "Cannot be both macOS and Linux");
}
if is_linux {
assert!(!is_osx, "Cannot be both Linux and macOS");
}
}
#[test]
fn test_architecture_detection_consistency() {
// Test that architecture detection functions return consistent results
let is_arm = platform::is_arm();
let is_x86 = platform::is_x86();
// On any given system, only one of these should be true
// (or both false if running on other architectures)
if is_arm {
assert!(!is_x86, "Cannot be both ARM and x86");
}
if is_x86 {
assert!(!is_arm, "Cannot be both x86 and ARM");
}
}
#[test]
fn test_platform_functions_return_bool() {
// Test that all platform detection functions return boolean values
let _: bool = platform::is_osx();
let _: bool = platform::is_linux();
let _: bool = platform::is_arm();
let _: bool = platform::is_x86();
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_detection() {
// When compiled for macOS, is_osx should return true
assert!(platform::is_osx());
assert!(!platform::is_linux());
}
#[cfg(target_os = "linux")]
#[test]
fn test_linux_detection() {
// When compiled for Linux, is_linux should return true
assert!(platform::is_linux());
assert!(!platform::is_osx());
}
#[cfg(target_arch = "aarch64")]
#[test]
fn test_arm_detection() {
// When compiled for ARM64, is_arm should return true
assert!(platform::is_arm());
assert!(!platform::is_x86());
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_x86_detection() {
// When compiled for x86_64, is_x86 should return true
assert!(platform::is_x86());
assert!(!platform::is_arm());
}
#[test]
fn test_check_linux_x86() {
let result = platform::check_linux_x86();
// The result should depend on the current platform
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
assert!(result.is_ok(), "Should succeed on Linux x86_64");
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
assert!(result.is_err(), "Should fail on non-Linux x86_64 platforms");
// Check that the error message is meaningful
let error = result.unwrap_err();
let error_string = error.to_string();
assert!(error_string.contains("Linux x86_64"),
"Error message should mention Linux x86_64: {}", error_string);
}
}
#[test]
fn test_check_macos_arm() {
let result = platform::check_macos_arm();
// The result should depend on the current platform
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
assert!(result.is_ok(), "Should succeed on macOS ARM");
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
{
assert!(result.is_err(), "Should fail on non-macOS ARM platforms");
// Check that the error message is meaningful
let error = result.unwrap_err();
let error_string = error.to_string();
assert!(error_string.contains("macOS ARM"),
"Error message should mention macOS ARM: {}", error_string);
}
}
#[test]
fn test_platform_error_creation() {
use sal_os::platform::PlatformError;
// Test that we can create platform errors
let error = PlatformError::new("Test Error", "This is a test error message");
let error_string = error.to_string();
assert!(error_string.contains("Test Error"));
assert!(error_string.contains("This is a test error message"));
}
#[test]
fn test_platform_error_display() {
use sal_os::platform::PlatformError;
// Test error display formatting
let error = PlatformError::Generic("Category".to_string(), "Message".to_string());
let error_string = format!("{}", error);
assert!(error_string.contains("Category"));
assert!(error_string.contains("Message"));
}
#[test]
fn test_platform_error_debug() {
use sal_os::platform::PlatformError;
// Test error debug formatting
let error = PlatformError::Generic("Category".to_string(), "Message".to_string());
let debug_string = format!("{:?}", error);
assert!(debug_string.contains("Generic"));
assert!(debug_string.contains("Category"));
assert!(debug_string.contains("Message"));
}
#[test]
fn test_platform_functions_are_deterministic() {
// Platform detection should be deterministic - same result every time
let osx1 = platform::is_osx();
let osx2 = platform::is_osx();
assert_eq!(osx1, osx2);
let linux1 = platform::is_linux();
let linux2 = platform::is_linux();
assert_eq!(linux1, linux2);
let arm1 = platform::is_arm();
let arm2 = platform::is_arm();
assert_eq!(arm1, arm2);
let x86_1 = platform::is_x86();
let x86_2 = platform::is_x86();
assert_eq!(x86_1, x86_2);
}
#[test]
fn test_platform_check_functions_consistency() {
// The check functions should be consistent with the individual detection functions
let is_linux_x86 = platform::is_linux() && platform::is_x86();
let check_linux_x86_result = platform::check_linux_x86().is_ok();
assert_eq!(is_linux_x86, check_linux_x86_result);
let is_macos_arm = platform::is_osx() && platform::is_arm();
let check_macos_arm_result = platform::check_macos_arm().is_ok();
assert_eq!(is_macos_arm, check_macos_arm_result);
}
#[test]
fn test_current_platform_info() {
// Print current platform info for debugging (this will show in test output with --nocapture)
println!("Current platform detection:");
println!(" is_osx(): {}", platform::is_osx());
println!(" is_linux(): {}", platform::is_linux());
println!(" is_arm(): {}", platform::is_arm());
println!(" is_x86(): {}", platform::is_x86());
println!(" check_linux_x86(): {:?}", platform::check_linux_x86());
println!(" check_macos_arm(): {:?}", platform::check_macos_arm());
}

View File

@ -0,0 +1,364 @@
use rhai::Engine;
use sal_os::rhai::register_os_module;
use tempfile::TempDir;
fn create_test_engine() -> Engine {
let mut engine = Engine::new();
register_os_module(&mut engine).expect("Failed to register OS module");
engine
}
#[test]
fn test_rhai_module_registration() {
// Test that the OS module can be registered without errors
let _engine = create_test_engine();
// If we get here without panicking, the module was registered successfully
// We can't easily test function registration without calling the functions
}
#[test]
fn test_rhai_file_operations() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
// Test file operations through Rhai
let script = format!(
r#"
let test_dir = "{}/test_rhai";
let test_file = test_dir + "/test.txt";
let content = "Hello from Rhai!";
// Create directory
mkdir(test_dir);
// Check if directory exists
let dir_exists = exist(test_dir);
// Write file
file_write(test_file, content);
// Check if file exists
let file_exists = exist(test_file);
// Read file
let read_content = file_read(test_file);
// Return results
#{{"dir_exists": dir_exists, "file_exists": file_exists, "content_match": read_content == content}}
"#,
temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["dir_exists"].as_bool().unwrap(), true);
assert_eq!(result["file_exists"].as_bool().unwrap(), true);
assert_eq!(result["content_match"].as_bool().unwrap(), true);
}
#[test]
fn test_rhai_file_size() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_file = "{}/size_test.txt";
let content = "12345"; // 5 bytes
file_write(test_file, content);
let size = file_size(test_file);
size
"#,
temp_path
);
let result: i64 = engine.eval(&script).expect("Script execution failed");
assert_eq!(result, 5);
}
#[test]
fn test_rhai_file_append() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_file = "{}/append_test.txt";
file_write(test_file, "Line 1\n");
file_write_append(test_file, "Line 2\n");
let content = file_read(test_file);
content
"#,
temp_path
);
let result: String = engine.eval(&script).expect("Script execution failed");
assert_eq!(result, "Line 1\nLine 2\n");
}
#[test]
fn test_rhai_copy_and_move() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let source = "{}/source.txt";
let copy_dest = "{}/copy.txt";
let move_dest = "{}/moved.txt";
let content = "Test content";
// Create source file
file_write(source, content);
// Copy file
copy(source, copy_dest);
// Move the copy
mv(copy_dest, move_dest);
// Check results
let source_exists = exist(source);
let copy_exists = exist(copy_dest);
let move_exists = exist(move_dest);
let move_content = file_read(move_dest);
#{{"source_exists": source_exists, "copy_exists": copy_exists, "move_exists": move_exists, "content_match": move_content == content}}
"#,
temp_path, temp_path, temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["source_exists"].as_bool().unwrap(), true);
assert_eq!(result["copy_exists"].as_bool().unwrap(), false); // Should be moved
assert_eq!(result["move_exists"].as_bool().unwrap(), true);
assert_eq!(result["content_match"].as_bool().unwrap(), true);
}
#[test]
fn test_rhai_delete() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_file = "{}/delete_test.txt";
// Create file
file_write(test_file, "content");
let exists_before = exist(test_file);
// Delete file
delete(test_file);
let exists_after = exist(test_file);
#{{"before": exists_before, "after": exists_after}}
"#,
temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["before"].as_bool().unwrap(), true);
assert_eq!(result["after"].as_bool().unwrap(), false);
}
#[test]
fn test_rhai_find_files() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_dir = "{}/find_test";
mkdir(test_dir);
// Create test files
file_write(test_dir + "/file1.txt", "content1");
file_write(test_dir + "/file2.txt", "content2");
file_write(test_dir + "/other.log", "log content");
// Find .txt files
let txt_files = find_files(test_dir, "*.txt");
let all_files = find_files(test_dir, "*");
#{{"txt_count": txt_files.len(), "all_count": all_files.len()}}
"#,
temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["txt_count"].as_int().unwrap(), 2);
assert!(result["all_count"].as_int().unwrap() >= 3);
}
#[test]
fn test_rhai_which_command() {
let engine = create_test_engine();
let script = r#"
let ls_path = which("ls");
let nonexistent = which("nonexistentcommand12345");
#{"ls_found": ls_path.len() > 0, "nonexistent_found": nonexistent.len() > 0}
"#;
let result: rhai::Map = engine.eval(script).expect("Script execution failed");
assert_eq!(result["ls_found"].as_bool().unwrap(), true);
assert_eq!(result["nonexistent_found"].as_bool().unwrap(), false);
}
#[test]
fn test_rhai_error_handling() {
let engine = create_test_engine();
// Test that errors are properly propagated to Rhai
// Instead of try-catch, just test that the function call fails
let script = r#"file_read("/nonexistent/path/file.txt")"#;
let result = engine.eval::<String>(script);
assert!(
result.is_err(),
"Expected error when reading non-existent file"
);
}
#[test]
fn test_rhai_package_functions() {
let engine = create_test_engine();
// Test that package functions are registered by calling them
let script = r#"
let platform = package_platform();
let debug_result = package_set_debug(true);
#{"platform": platform, "debug": debug_result}
"#;
let result: rhai::Map = engine.eval(script).expect("Script execution failed");
// Platform should be a non-empty string
let platform: String = result["platform"].clone().try_cast().unwrap();
assert!(!platform.is_empty());
// Debug setting should return true
assert_eq!(result["debug"].as_bool().unwrap(), true);
}
#[test]
fn test_rhai_download_functions() {
let engine = create_test_engine();
// Test that download functions are registered by calling them
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_file = "{}/test_script.sh";
// Create a test script
file_write(test_file, "echo 'test'");
// Make it executable
try {{
let result = chmod_exec(test_file);
result.len() >= 0 // chmod_exec returns a string, so check if it's valid
}} catch {{
false
}}
"#,
temp_path
);
let result: bool = engine.eval(&script).expect("Script execution failed");
assert!(result);
}
#[test]
fn test_rhai_array_returns() {
let engine = create_test_engine();
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_str().unwrap();
let script = format!(
r#"
let test_dir = "{}/array_test";
mkdir(test_dir);
// Create some files
file_write(test_dir + "/file1.txt", "content");
file_write(test_dir + "/file2.txt", "content");
// Test that find_files returns an array
let files = find_files(test_dir, "*.txt");
// Test array operations
let count = files.len();
let first_file = if count > 0 {{ files[0] }} else {{ "" }};
#{{"count": count, "has_files": count > 0, "first_file_exists": first_file.len() > 0}}
"#,
temp_path
);
let result: rhai::Map = engine.eval(&script).expect("Script execution failed");
assert_eq!(result["count"].as_int().unwrap(), 2);
assert_eq!(result["has_files"].as_bool().unwrap(), true);
assert_eq!(result["first_file_exists"].as_bool().unwrap(), true);
}
#[test]
fn test_rhai_platform_functions() {
let engine = create_test_engine();
let script = r#"
let is_osx = platform_is_osx();
let is_linux = platform_is_linux();
let is_arm = platform_is_arm();
let is_x86 = platform_is_x86();
// Test that platform detection is consistent
let platform_consistent = !(is_osx && is_linux);
let arch_consistent = !(is_arm && is_x86);
#{"osx": is_osx, "linux": is_linux, "arm": is_arm, "x86": is_x86, "platform_consistent": platform_consistent, "arch_consistent": arch_consistent}
"#;
let result: rhai::Map = engine.eval(script).expect("Script execution failed");
// Verify platform detection consistency
assert_eq!(result["platform_consistent"].as_bool().unwrap(), true);
assert_eq!(result["arch_consistent"].as_bool().unwrap(), true);
// At least one platform should be detected
let osx = result["osx"].as_bool().unwrap();
let linux = result["linux"].as_bool().unwrap();
// At least one architecture should be detected
let arm = result["arm"].as_bool().unwrap();
let x86 = result["x86"].as_bool().unwrap();
// Print current platform for debugging
println!(
"Platform detection: OSX={}, Linux={}, ARM={}, x86={}",
osx, linux, arm, x86
);
}

View File

@ -40,7 +40,7 @@ pub type Result<T> = std::result::Result<T, Error>;
pub mod cmd; pub mod cmd;
pub use sal_mycelium as mycelium; pub use sal_mycelium as mycelium;
pub mod net; pub mod net;
pub mod os; pub use sal_os as os;
pub mod postgresclient; pub mod postgresclient;
pub mod process; pub mod process;
pub use sal_redisclient as redisclient; pub use sal_redisclient as redisclient;

View File

@ -1,245 +0,0 @@
# SAL OS Module (`sal::os`)
The `sal::os` module provides a comprehensive suite of operating system interaction utilities. It aims to offer a cross-platform abstraction layer for common OS-level tasks, simplifying system programming in Rust.
This module is composed of three main sub-modules:
- [`fs`](#fs): File system operations.
- [`download`](#download): File downloading and basic installation.
- [`package`](#package): System package management.
## Key Design Points
The `sal::os` module is engineered with several core principles to provide a robust and developer-friendly interface for OS interactions:
- **Cross-Platform Abstraction**: A primary goal is to offer a unified API for common OS tasks, smoothing over differences between operating systems (primarily Linux and macOS). While it strives for abstraction, it leverages platform-specific tools (e.g., `rsync` on Linux, `robocopy` on Windows for `fs::copy` or `fs::rsync`; `apt` on Debian-based systems, `brew` on macOS for `package` management) for optimal performance and behavior when necessary.
- **Modular Structure**: Functionality is organized into logical sub-modules:
- `fs`: For comprehensive file and directory manipulation.
- `download`: For retrieving files from URLs, with support for extraction and basic installation.
- `package`: For interacting with system package managers.
- **Granular Error Handling**: Each sub-module features custom error enums (`FsError`, `DownloadError`, `PackageError`) to provide specific and actionable feedback, aiding in debugging and robust error management.
- **Sensible Defaults and Defensive Operations**: Many functions are designed to be "defensive," e.g., `mkdir` creates parent directories if they don't exist and doesn't fail if the directory already exists. `delete` doesn't error if the target is already gone.
- **Facade for Simplicity**: The `package` sub-module uses a `PackHero` facade to provide a simple entry point for common package operations, automatically detecting the underlying OS and package manager.
- **Rhai Scriptability**: A significant portion of the `sal::os` module's functionality is exposed to Rhai scripts via `herodo`, enabling powerful automation of OS-level tasks.
## `fs` - File System Operations
The `fs` sub-module (`sal::os::fs`) offers a robust set of functions for interacting with the file system.
**Key Features:**
* **Error Handling**: A custom `FsError` enum for detailed error reporting on file system operations.
* **File Operations**:
* `copy(src, dest)`: Copies files and directories, with support for wildcards and recursive copying. Uses platform-specific commands (`cp -R`, `robocopy /MIR`).
* `exist(path)`: Checks if a file or directory exists.
* `find_file(dir, filename_pattern)`: Finds a single file in a directory, supporting wildcards.
* `find_files(dir, filename_pattern)`: Finds multiple files in a directory, supporting wildcards.
* `file_size(path)`: Returns the size of a file in bytes.
* `file_read(path)`: Reads the entire content of a file into a string.
* `file_write(path, content)`: Writes content to a file, overwriting if it exists, and creating parent directories if needed.
* `file_write_append(path, content)`: Appends content to a file, creating it and parent directories if needed.
* **Directory Operations**:
* `find_dir(parent_dir, dirname_pattern)`: Finds a single directory within a parent directory, supporting wildcards.
* `find_dirs(parent_dir, dirname_pattern)`: Finds multiple directories recursively within a parent directory, supporting wildcards.
* `delete(path)`: Deletes files or directories.
* `mkdir(path)`: Creates a directory, including parent directories if necessary.
* `rsync(src, dest)`: Synchronizes directories using platform-specific commands (`rsync -a --delete`, `robocopy /MIR`).
* `chdir(path)`: Changes the current working directory.
* **Path Operations**:
* `mv(src, dest)`: Moves or renames files and directories. Handles cross-device moves by falling back to copy-then-delete.
* **Command Utilities**:
* `which(command_name)`: Checks if a command exists in the system's PATH and returns its path.
**Usage Example (fs):**
```rust
use sal::os::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
if !fs::exist("my_dir") {
fs::mkdir("my_dir")?;
println!("Created directory 'my_dir'");
}
fs::file_write("my_dir/example.txt", "Hello from SAL!")?;
let content = fs::file_read("my_dir/example.txt")?;
println!("File content: {}", content);
Ok(())
}
```
## `download` - File Downloading and Installation
The `download` sub-module (`sal::os::download`) provides utilities for downloading files from URLs and performing basic installation tasks.
**Key Features:**
* **Error Handling**: A custom `DownloadError` enum for download-specific errors.
* **File Downloading**:
* `download(url, dest_dir, min_size_kb)`: Downloads a file to a specified directory.
* Uses `curl` with progress display.
* Supports minimum file size checks.
* Automatically extracts common archive formats (`.tar.gz`, `.tgz`, `.tar`, `.zip`) into `dest_dir`.
* `download_file(url, dest_file_path, min_size_kb)`: Downloads a file to a specific file path without automatic extraction.
* **File Permissions**:
* `chmod_exec(path)`: Makes a file executable (equivalent to `chmod +x` on Unix-like systems).
* **Download and Install**:
* `download_install(url, min_size_kb)`: Downloads a file (to `/tmp/`) and attempts to install it if it's a supported package format.
* Currently supports `.deb` packages on Debian-based systems.
* For `.deb` files, it uses `sudo dpkg --install` and attempts `sudo apt-get install -f -y` to fix dependencies if needed.
* Handles archives by extracting them to `/tmp/` first.
**Usage Example (download):**
```rust
use sal::os::download;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let archive_url = "https://example.com/my_archive.tar.gz";
let output_dir = "/tmp/my_app";
// Download and extract an archive
let extracted_path = download::download(archive_url, output_dir, 1024)?; // Min 1MB
println!("Archive extracted to: {}", extracted_path);
// Download a script and make it executable
let script_url = "https://example.com/my_script.sh";
let script_path = "/tmp/my_script.sh";
download::download_file(script_url, script_path, 0)?;
download::chmod_exec(script_path)?;
println!("Script downloaded and made executable at: {}", script_path);
Ok(())
}
```
## `package` - System Package Management
The `package` sub-module (`sal::os::package`) offers an abstraction layer for interacting with system package managers like APT (for Debian/Ubuntu) and Homebrew (for macOS).
**Key Features:**
* **Error Handling**: A custom `PackageError` enum.
* **Platform Detection**: Identifies the current OS (Ubuntu, macOS, or Unknown) to use the appropriate package manager.
* **`PackageManager` Trait**: Defines a common interface for package operations:
* `install(package_name)`
* `remove(package_name)`
* `update()` (updates package lists)
* `upgrade()` (upgrades all installed packages)
* `list_installed()`
* `search(query)`
* `is_installed(package_name)`
* **Implementations**:
* `AptPackageManager`: For Debian/Ubuntu systems (uses `apt-get`, `dpkg`).
* `BrewPackageManager`: For macOS systems (uses `brew`).
* **`PackHero` Facade**: A simple entry point to access package management functions in a platform-agnostic way.
* `PackHero::new().install("nginx")?`
**Usage Example (package):**
```rust
use sal::os::package::PackHero;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let pack_hero = PackHero::new();
// Check if a package is installed
if !pack_hero.is_installed("htop")? {
println!("htop is not installed. Attempting to install...");
pack_hero.install("htop")?;
println!("htop installed successfully.");
} else {
println!("htop is already installed.");
}
// Update package lists
println!("Updating package lists...");
pack_hero.update()?;
println!("Package lists updated.");
Ok(())
}
```
## Rhai Scripting with `herodo`
The `sal::os` module is extensively scriptable via `herodo`, allowing for automation of various operating system tasks directly from Rhai scripts. The `sal::rhai::os` module registers the necessary functions.
### File System (`fs`) Functions
- `copy(src: String, dest: String) -> String`: Copies files/directories (supports wildcards).
- `exist(path: String) -> bool`: Checks if a file or directory exists.
- `find_file(dir: String, filename_pattern: String) -> String`: Finds a single file in `dir` matching `filename_pattern`.
- `find_files(dir: String, filename_pattern: String) -> Array`: Finds multiple files in `dir` (recursive).
- `find_dir(parent_dir: String, dirname_pattern: String) -> String`: Finds a single directory in `parent_dir`.
- `find_dirs(parent_dir: String, dirname_pattern: String) -> Array`: Finds multiple directories in `parent_dir` (recursive).
- `delete(path: String) -> String`: Deletes a file or directory.
- `mkdir(path: String) -> String`: Creates a directory (and parents if needed).
- `file_size(path: String) -> Int`: Returns file size in bytes.
- `rsync(src: String, dest: String) -> String`: Synchronizes directories.
- `chdir(path: String) -> String`: Changes the current working directory.
- `file_read(path: String) -> String`: Reads entire file content.
- `file_write(path: String, content: String) -> String`: Writes content to a file (overwrites).
- `file_write_append(path: String, content: String) -> String`: Appends content to a file.
- `mv(src: String, dest: String) -> String`: Moves/renames a file or directory.
- `which(command_name: String) -> String`: Checks if a command exists in PATH and returns its path.
- `cmd_ensure_exists(commands: String) -> String`: Ensures one or more commands (comma-separated) exist in PATH; throws an error if any are missing.
### Download Functions
- `download(url: String, dest_dir: String, min_size_kb: Int) -> String`: Downloads from `url` to `dest_dir`, extracts common archives.
- `download_file(url: String, dest_file_path: String, min_size_kb: Int) -> String`: Downloads from `url` to `dest_file_path` (no extraction).
- `download_install(url: String, min_size_kb: Int) -> String`: Downloads and attempts to install (e.g., `.deb` packages).
- `chmod_exec(path: String) -> String`: Makes a file executable (`chmod +x`).
### Package Management Functions
- `package_install(package_name: String) -> String`: Installs a package.
- `package_remove(package_name: String) -> String`: Removes a package.
- `package_update() -> String`: Updates package lists.
- `package_upgrade() -> String`: Upgrades all installed packages.
- `package_list() -> Array`: Lists all installed packages.
- `package_search(query: String) -> Array`: Searches for packages.
- `package_is_installed(package_name: String) -> bool`: Checks if a package is installed.
- `package_set_debug(debug: bool) -> bool`: Enables/disables debug logging for package operations.
- `package_platform() -> String`: Returns the detected package platform (e.g., "Ubuntu", "MacOS").
### Rhai Example
```rhai
// File system operations
let test_dir = "/tmp/sal_os_rhai_test";
if exist(test_dir) {
delete(test_dir);
}
mkdir(test_dir);
print(`Created directory: ${test_dir}`);
file_write(`${test_dir}/message.txt`, "Hello from Rhai OS module!");
let content = file_read(`${test_dir}/message.txt`);
print(`File content: ${content}`);
// Download operation (example URL, may not be active)
// let script_url = "https://raw.githubusercontent.com/someuser/somescript/main/script.sh";
// let script_path = `${test_dir}/downloaded_script.sh`;
// try {
// download_file(script_url, script_path, 0);
// chmod_exec(script_path);
// print(`Downloaded and made executable: ${script_path}`);
// } catch (e) {
// print(`Download example failed (this is okay for a test): ${e}`);
// }
// Package management (illustrative, requires sudo for install/remove/update)
print(`Package platform: ${package_platform()}`);
if !package_is_installed("htop") {
print("htop is not installed.");
// package_install("htop"); // Would require sudo
} else {
print("htop is already installed.");
}
print("OS module Rhai script finished.");
```
This module provides a powerful and convenient way to handle common OS-level tasks within your Rust applications.

View File

@ -1,8 +0,0 @@
mod fs;
mod download;
pub mod package;
pub use fs::*;
pub use download::*;
pub use package::*;
pub mod platform;

View File

@ -2,9 +2,9 @@
//! //!
//! This module provides Rhai wrappers for functions that interact with the Rhai engine itself. //! This module provides Rhai wrappers for functions that interact with the Rhai engine itself.
use rhai::{Engine, EvalAltResult, NativeCallContext};
use crate::os;
use super::error::ToRhaiError; use super::error::ToRhaiError;
use rhai::{Engine, EvalAltResult, NativeCallContext};
use sal_os as os;
/// Register core module functions with the Rhai engine /// Register core module functions with the Rhai engine
/// ///
@ -37,7 +37,7 @@ pub fn exec(context: NativeCallContext, source: &str) -> Result<rhai::Dynamic, B
let file_name = source.split('/').last().unwrap_or("script.rhai"); let file_name = source.split('/').last().unwrap_or("script.rhai");
let dest_path = temp_dir.join(format!("{}-{}", uuid::Uuid::new_v4(), file_name)); let dest_path = temp_dir.join(format!("{}-{}", uuid::Uuid::new_v4(), file_name));
let dest_str = dest_path.to_str().unwrap(); let dest_str = dest_path.to_str().unwrap();
os::download_file(source, dest_str, 0).to_rhai_error()?; os::download_file(source, dest_str, 0).to_rhai_error()?;
os::file_read(dest_str).to_rhai_error()? os::file_read(dest_str).to_rhai_error()?
} else if os::exist(source) { } else if os::exist(source) {
@ -50,4 +50,4 @@ pub fn exec(context: NativeCallContext, source: &str) -> Result<rhai::Dynamic, B
// Execute the script content // Execute the script content
context.engine().eval(&content) context.engine().eval(&content)
} }

View File

@ -7,8 +7,8 @@ mod buildah;
mod core; mod core;
pub mod error; pub mod error;
mod nerdctl; mod nerdctl;
mod os; // OS module is now provided by sal-os package
mod platform; // Platform module is now provided by sal-os package
mod postgresclient; mod postgresclient;
mod process; mod process;
@ -26,8 +26,8 @@ pub use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
// Re-export error module // Re-export error module
pub use error::*; pub use error::*;
// Re-export specific functions from modules to avoid name conflicts // Re-export specific functions from sal-os package
pub use os::{ pub use sal_os::rhai::{
delete, delete,
// Download functions // Download functions
download, download,
@ -106,7 +106,7 @@ pub use sal_text::rhai::register_text_module;
pub use vault::register_crypto_module; pub use vault::register_crypto_module;
// Rename copy functions to avoid conflicts // Rename copy functions to avoid conflicts
pub use os::copy as os_copy; pub use sal_os::rhai::copy as os_copy;
/// Register all SAL modules with the Rhai engine /// Register all SAL modules with the Rhai engine
/// ///
@ -132,7 +132,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
core::register_core_module(engine)?; core::register_core_module(engine)?;
// Register OS module functions // Register OS module functions
os::register_os_module(engine)?; sal_os::rhai::register_os_module(engine)?;
// Register Process module functions // Register Process module functions
process::register_process_module(engine)?; process::register_process_module(engine)?;
@ -167,8 +167,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
// Register PostgreSQL client module functions // Register PostgreSQL client module functions
postgresclient::register_postgresclient_module(engine)?; postgresclient::register_postgresclient_module(engine)?;
// Register Platform module functions // Platform functions are now registered by sal-os package
platform::register(engine);
// Register Screen module functions // Register Screen module functions
screen::register(engine); screen::register(engine);

View File

@ -1,40 +0,0 @@
use crate::os::platform;
use rhai::{plugin::*, Engine};
#[export_module]
pub mod platform_functions {
#[rhai_fn(name = "platform_is_osx")]
pub fn is_osx() -> bool {
platform::is_osx()
}
#[rhai_fn(name = "platform_is_linux")]
pub fn is_linux() -> bool {
platform::is_linux()
}
#[rhai_fn(name = "platform_is_arm")]
pub fn is_arm() -> bool {
platform::is_arm()
}
#[rhai_fn(name = "platform_is_x86")]
pub fn is_x86() -> bool {
platform::is_x86()
}
#[rhai_fn(name = "platform_check_linux_x86")]
pub fn check_linux_x86() -> Result<(), crate::rhai::error::SalError> {
platform::check_linux_x86()
}
#[rhai_fn(name = "platform_check_macos_arm")]
pub fn check_macos_arm() -> Result<(), crate::rhai::error::SalError> {
platform::check_macos_arm()
}
}
pub fn register(engine: &mut Engine) {
let platform_module = exported_module!(platform_functions);
engine.register_global_module(platform_module.into());
}

View File

@ -1,9 +1,9 @@
// File: /root/code/git.threefold.info/herocode/sal/src/virt/nerdctl/container.rs // File: /root/code/git.threefold.info/herocode/sal/src/virt/nerdctl/container.rs
use std::collections::HashMap;
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
use crate::os;
use super::container_types::Container; use super::container_types::Container;
use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError};
use sal_os as os;
use std::collections::HashMap;
impl Container { impl Container {
/// Create a new container reference with the given name /// Create a new container reference with the given name
@ -18,18 +18,22 @@ impl Container {
pub fn new(name: &str) -> Result<Self, NerdctlError> { pub fn new(name: &str) -> Result<Self, NerdctlError> {
// Check if required commands exist // Check if required commands exist
match os::cmd_ensure_exists("nerdctl,runc,buildah") { match os::cmd_ensure_exists("nerdctl,runc,buildah") {
Err(e) => return Err(NerdctlError::CommandExecutionFailed( Err(e) => {
std::io::Error::new(std::io::ErrorKind::NotFound, return Err(NerdctlError::CommandExecutionFailed(std::io::Error::new(
format!("Required commands not found: {}", e)) std::io::ErrorKind::NotFound,
)), format!("Required commands not found: {}", e),
)))
}
_ => {} _ => {}
} }
// Check if container exists // Check if container exists
let result = execute_nerdctl_command(&["ps", "-a", "--format", "{{.Names}} {{.ID}}"])?; let result = execute_nerdctl_command(&["ps", "-a", "--format", "{{.Names}} {{.ID}}"])?;
// Look for the container name in the output // Look for the container name in the output
let container_id = result.stdout.lines() let container_id = result
.stdout
.lines()
.filter_map(|line| { .filter_map(|line| {
if line.starts_with(&format!("{} ", name)) { if line.starts_with(&format!("{} ", name)) {
Some(line.split_whitespace().nth(1)?.to_string()) Some(line.split_whitespace().nth(1)?.to_string())
@ -38,7 +42,7 @@ impl Container {
} }
}) })
.next(); .next();
Ok(Self { Ok(Self {
name: name.to_string(), name: name.to_string(),
container_id, container_id,
@ -59,7 +63,7 @@ impl Container {
snapshotter: None, snapshotter: None,
}) })
} }
/// Create a container from an image /// Create a container from an image
/// ///
/// # Arguments /// # Arguments

View File

@ -15,7 +15,7 @@ use tempfile::NamedTempFile;
#[test] #[test]
fn test_template_builder_basic_string_variable() { fn test_template_builder_basic_string_variable() {
// Create a temporary template file // Create a temporary template file
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Hello {{name}}!"; let template_content = "Hello {{name}}!";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
@ -30,7 +30,7 @@ fn test_template_builder_basic_string_variable() {
#[test] #[test]
fn test_template_builder_multiple_variables() { fn test_template_builder_multiple_variables() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{{greeting}} {{name}}, you have {{count}} messages."; let template_content = "{{greeting}} {{name}}, you have {{count}} messages.";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
@ -47,7 +47,7 @@ fn test_template_builder_multiple_variables() {
#[test] #[test]
fn test_template_builder_different_types() { fn test_template_builder_different_types() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "String: {{text}}, Int: {{number}}, Float: {{decimal}}, Bool: {{flag}}"; let template_content = "String: {{text}}, Int: {{number}}, Float: {{decimal}}, Bool: {{flag}}";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
@ -65,8 +65,9 @@ fn test_template_builder_different_types() {
#[test] #[test]
fn test_template_builder_array_variable() { fn test_template_builder_array_variable() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Items: {% for item in items %}{{item}}{% if not loop.last %}, {% endif %}{% endfor %}"; let template_content =
"Items: {% for item in items %}{{item}}{% if not loop.last %}, {% endif %}{% endfor %}";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
let items = vec!["apple", "banana", "cherry"]; let items = vec!["apple", "banana", "cherry"];
@ -81,7 +82,7 @@ fn test_template_builder_array_variable() {
#[test] #[test]
fn test_template_builder_add_vars_hashmap() { fn test_template_builder_add_vars_hashmap() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{{title}}: {{description}}"; let template_content = "{{title}}: {{description}}";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
@ -101,7 +102,7 @@ fn test_template_builder_add_vars_hashmap() {
#[test] #[test]
fn test_template_builder_render_to_file() { fn test_template_builder_render_to_file() {
// Create template file // Create template file
let mut template_file = NamedTempFile::new().expect("Failed to create template file"); let template_file = NamedTempFile::new().expect("Failed to create template file");
let template_content = "Hello {{name}}, today is {{day}}."; let template_content = "Hello {{name}}, today is {{day}}.";
fs::write(template_file.path(), template_content).expect("Failed to write template"); fs::write(template_file.path(), template_content).expect("Failed to write template");
@ -121,8 +122,9 @@ fn test_template_builder_render_to_file() {
#[test] #[test]
fn test_template_builder_conditional() { fn test_template_builder_conditional() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{% if show_message %}Message: {{message}}{% else %}No message{% endif %}"; let template_content =
"{% if show_message %}Message: {{message}}{% else %}No message{% endif %}";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
// Test with condition true // Test with condition true
@ -148,7 +150,7 @@ fn test_template_builder_conditional() {
#[test] #[test]
fn test_template_builder_loop_with_index() { fn test_template_builder_loop_with_index() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "{% for item in items %}{{loop.index}}: {{item}}\n{% endfor %}"; let template_content = "{% for item in items %}{{loop.index}}: {{item}}\n{% endfor %}";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
@ -164,7 +166,7 @@ fn test_template_builder_loop_with_index() {
#[test] #[test]
fn test_template_builder_nested_variables() { fn test_template_builder_nested_variables() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "User: {{user.name}} ({{user.email}})"; let template_content = "User: {{user.name}} ({{user.email}})";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
@ -183,7 +185,7 @@ fn test_template_builder_nested_variables() {
#[test] #[test]
fn test_template_builder_missing_variable_error() { fn test_template_builder_missing_variable_error() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Hello {{missing_var}}!"; let template_content = "Hello {{missing_var}}!";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
@ -196,7 +198,7 @@ fn test_template_builder_missing_variable_error() {
#[test] #[test]
fn test_template_builder_invalid_template_syntax() { fn test_template_builder_invalid_template_syntax() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "Hello {{unclosed_var!"; let template_content = "Hello {{unclosed_var!";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
@ -215,7 +217,7 @@ fn test_template_builder_nonexistent_file() {
#[test] #[test]
fn test_template_builder_empty_template() { fn test_template_builder_empty_template() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
fs::write(temp_file.path(), "").expect("Failed to write empty template"); fs::write(temp_file.path(), "").expect("Failed to write empty template");
let result = TemplateBuilder::open(temp_file.path()) let result = TemplateBuilder::open(temp_file.path())
@ -228,7 +230,7 @@ fn test_template_builder_empty_template() {
#[test] #[test]
fn test_template_builder_template_with_no_variables() { fn test_template_builder_template_with_no_variables() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = "This is a static template with no variables."; let template_content = "This is a static template with no variables.";
fs::write(temp_file.path(), template_content).expect("Failed to write template"); fs::write(temp_file.path(), template_content).expect("Failed to write template");
@ -242,7 +244,7 @@ fn test_template_builder_template_with_no_variables() {
#[test] #[test]
fn test_template_builder_complex_report() { fn test_template_builder_complex_report() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let template_content = r#" let template_content = r#"
# {{report_title}} # {{report_title}}

View File

@ -1,13 +1,13 @@
//! Unit tests for text replacement functionality //! Unit tests for text replacement functionality
//! //!
//! These tests validate the TextReplacer and TextReplacerBuilder including: //! These tests validate the TextReplacer including:
//! - Literal string replacement //! - Literal string replacement
//! - Regex pattern replacement //! - Regex pattern replacement
//! - Multiple chained replacements //! - Multiple chained replacements
//! - File operations (read, write, in-place) //! - File operations (read, write, in-place)
//! - Error handling and edge cases //! - Error handling and edge cases
use sal_text::{TextReplacer, TextReplacerBuilder}; use sal_text::TextReplacer;
use std::fs; use std::fs;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
@ -141,7 +141,7 @@ fn test_text_replacer_no_matches() {
#[test] #[test]
fn test_text_replacer_file_operations() { fn test_text_replacer_file_operations() {
// Create a temporary file with test content // Create a temporary file with test content
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let test_content = "Hello world, there are 123 items"; let test_content = "Hello world, there are 123 items";
fs::write(temp_file.path(), test_content).expect("Failed to write to temp file"); fs::write(temp_file.path(), test_content).expect("Failed to write to temp file");
@ -157,18 +157,21 @@ fn test_text_replacer_file_operations() {
.expect("Failed to build replacer"); .expect("Failed to build replacer");
// Test replace_file // Test replace_file
let result = replacer.replace_file(temp_file.path()).expect("Failed to replace file content"); let result = replacer
.replace_file(temp_file.path())
.expect("Failed to replace file content");
assert_eq!(result, "Hello universe, there are NUMBER items"); assert_eq!(result, "Hello universe, there are NUMBER items");
// Verify original file is unchanged // Verify original file is unchanged
let original_content = fs::read_to_string(temp_file.path()).expect("Failed to read original file"); let original_content =
fs::read_to_string(temp_file.path()).expect("Failed to read original file");
assert_eq!(original_content, test_content); assert_eq!(original_content, test_content);
} }
#[test] #[test]
fn test_text_replacer_file_in_place() { fn test_text_replacer_file_in_place() {
// Create a temporary file with test content // Create a temporary file with test content
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let test_content = "Hello world, there are 123 items"; let test_content = "Hello world, there are 123 items";
fs::write(temp_file.path(), test_content).expect("Failed to write to temp file"); fs::write(temp_file.path(), test_content).expect("Failed to write to temp file");
@ -180,7 +183,9 @@ fn test_text_replacer_file_in_place() {
.expect("Failed to build replacer"); .expect("Failed to build replacer");
// Test replace_file_in_place // Test replace_file_in_place
replacer.replace_file_in_place(temp_file.path()).expect("Failed to replace file in place"); replacer
.replace_file_in_place(temp_file.path())
.expect("Failed to replace file in place");
// Verify file content was changed // Verify file content was changed
let new_content = fs::read_to_string(temp_file.path()).expect("Failed to read modified file"); let new_content = fs::read_to_string(temp_file.path()).expect("Failed to read modified file");
@ -190,7 +195,7 @@ fn test_text_replacer_file_in_place() {
#[test] #[test]
fn test_text_replacer_file_to_file() { fn test_text_replacer_file_to_file() {
// Create source file // Create source file
let mut source_file = NamedTempFile::new().expect("Failed to create source file"); let source_file = NamedTempFile::new().expect("Failed to create source file");
let test_content = "Hello world, there are 123 items"; let test_content = "Hello world, there are 123 items";
fs::write(source_file.path(), test_content).expect("Failed to write to source file"); fs::write(source_file.path(), test_content).expect("Failed to write to source file");
@ -205,11 +210,13 @@ fn test_text_replacer_file_to_file() {
.expect("Failed to build replacer"); .expect("Failed to build replacer");
// Test replace_file_to // Test replace_file_to
replacer.replace_file_to(source_file.path(), dest_file.path()) replacer
.replace_file_to(source_file.path(), dest_file.path())
.expect("Failed to replace file to destination"); .expect("Failed to replace file to destination");
// Verify source file is unchanged // Verify source file is unchanged
let source_content = fs::read_to_string(source_file.path()).expect("Failed to read source file"); let source_content =
fs::read_to_string(source_file.path()).expect("Failed to read source file");
assert_eq!(source_content, test_content); assert_eq!(source_content, test_content);
// Verify destination file has replaced content // Verify destination file has replaced content
@ -263,9 +270,10 @@ fn test_text_replacer_multiline_text() {
.build() .build()
.expect("Failed to build replacer"); .expect("Failed to build replacer");
let input = "function test() {\n // This is a comment\n return true;\n // Another comment\n}"; let input =
"function test() {\n // This is a comment\n return true;\n // Another comment\n}";
let result = replacer.replace(input); let result = replacer.replace(input);
// Note: This test depends on how the regex engine handles multiline mode // Note: This test depends on how the regex engine handles multiline mode
// The actual behavior might need adjustment based on regex flags // The actual behavior might need adjustment based on regex flags
assert!(result.contains("function test()")); assert!(result.contains("function test()"));
@ -288,7 +296,7 @@ fn test_text_replacer_unicode_text() {
#[test] #[test]
fn test_text_replacer_large_text() { fn test_text_replacer_large_text() {
let large_text = "word ".repeat(10000); let large_text = "word ".repeat(10000);
let replacer = TextReplacer::builder() let replacer = TextReplacer::builder()
.pattern("word") .pattern("word")
.replacement("term") .replacement("term")