diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index 5a78ec33a..2160f974c 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -671,6 +671,12 @@ The **file.append** method appends the `content` to file at `path`. If no file e The **file.compress** method compresses a file using the gzip algorithm. If the destination file doesn't exist it will be created. If the source file doesn't exist an error will be thrown. If the source path is a directory the contents will be placed in a tar archive and then compressed. +### file.chmod + +`file.chmod(path: str, mode: int) -> None` + +The **file.chmod** method changes the permissions of a file. The `mode` should typically be specified in octal (e.g. `0o755`). On Windows, this method only considers the `0o200` (owner writable) bit to determine whether to toggle the read-only attribute of the file, similar to Golang's `os.Chmod`. + ### file.copy `file.copy(src: str, dst: str) -> None` diff --git a/implants/golem/tests/cli.rs b/implants/golem/tests/cli.rs index 4d0cb8ac4..4894d30d4 100644 --- a/implants/golem/tests/cli.rs +++ b/implants/golem/tests/cli.rs @@ -39,7 +39,7 @@ fn test_golem_main_basic_non_interactive() -> anyhow::Result<()> { cmd.assert() .success() .stdout(predicate::str::contains(r#"HELLO"#)) - .stdout(predicate::str::contains(r#""append", "compress""#)); + .stdout(predicate::str::contains(r#""append", "chmod", "compress""#)); Ok(()) } @@ -64,7 +64,7 @@ fn test_golem_main_loaded_files() -> anyhow::Result<()> { cmd.arg(GOLEM_CLI_TEST_DIR); cmd.assert() .success() - .stdout(predicate::str::contains(r#"["append", "compress""#)); + .stdout(predicate::str::contains(r#"["append", "chmod", "compress""#)); Ok(()) } diff --git a/implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs b/implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs index 7e6fa23ab..1c8ecbf7c 100644 --- a/implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs +++ b/implants/lib/eldritch/stdlib/eldritch-libfile/src/fake.rs @@ -105,6 +105,10 @@ impl FileLibrary for FileLibraryFake { Ok(()) } + fn chmod(&self, _path: String, _mode: i64) -> Result<(), String> { + Ok(()) + } + fn copy(&self, src: String, dst: String) -> Result<(), String> { let mut root = self.root.lock(); let src_parts = Self::normalize_path(&src); diff --git a/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs b/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs index a0fe1f88f..09dbceeef 100644 --- a/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs +++ b/implants/lib/eldritch/stdlib/eldritch-libfile/src/lib.rs @@ -68,6 +68,20 @@ pub trait FileLibrary { /// - Returns an error string if the source doesn't exist or copy fails. fn copy(&self, src: String, dst: String) -> Result<(), String>; + #[eldritch_method] + /// Changes the permissions of a file. + /// + /// **Parameters** + /// - `path` (`str`): The file path. + /// - `mode` (`u32`): The permission mode (e.g., 0o755). On Windows, only the 0o200 (owner writable) bit is considered. + /// + /// **Returns** + /// - `None` + /// + /// **Errors** + /// - Returns an error string if chmod fails. + fn chmod(&self, path: String, mode: i64) -> Result<(), String>; + #[eldritch_method] /// Decompresses a GZIP file. /// diff --git a/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/chmod_impl.rs b/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/chmod_impl.rs new file mode 100644 index 000000000..6ea9c4c5f --- /dev/null +++ b/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/chmod_impl.rs @@ -0,0 +1,76 @@ +use alloc::string::String; +use alloc::string::ToString; + +pub fn chmod(path: String, mode: i64) -> Result<(), String> { + chmod_impl(path, mode as u32) +} + +#[cfg(unix)] +fn chmod_impl(path: String, mode: u32) -> Result<(), String> { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + let mut perms = fs::metadata(&path) + .map_err(|e| e.to_string())? + .permissions(); + perms.set_mode(mode); + fs::set_permissions(&path, perms).map_err(|e| e.to_string()) +} + +#[cfg(windows)] +fn chmod_impl(path: String, mode: u32) -> Result<(), String> { + use std::fs; + + let mut perms = fs::metadata(&path) + .map_err(|e| e.to_string())? + .permissions(); + + // Windows logic: only check the 0o200 bit (owner writable) + let is_writable = (mode & 0o200) != 0; + perms.set_readonly(!is_writable); + + fs::set_permissions(&path, perms).map_err(|e| e.to_string()) +} + +#[cfg(not(any(unix, windows)))] +fn chmod_impl(_path: String, _mode: u32) -> Result<(), String> { + Err("chmod is not supported on this platform".to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + use tempfile::NamedTempFile; + + #[test] + #[cfg(unix)] + fn test_chmod_unix() { + let file = NamedTempFile::new().unwrap(); + let path = file.path().to_str().unwrap().to_string(); + + chmod(path.clone(), 0o755).unwrap(); + + let perms = fs::metadata(&path).unwrap().permissions(); + assert_eq!(perms.mode() & 0o777, 0o755); + } + + #[test] + #[cfg(windows)] + fn test_chmod_windows() { + let file = NamedTempFile::new().unwrap(); + let path = file.path().to_str().unwrap().to_string(); + + // Try setting writable (clear readonly) + chmod(path.clone(), 0o600).unwrap(); + let perms = fs::metadata(&path).unwrap().permissions(); + assert_eq!(perms.readonly(), false); + + // Try setting readonly + chmod(path.clone(), 0o400).unwrap(); + let perms = fs::metadata(&path).unwrap().permissions(); + assert_eq!(perms.readonly(), true); + } +} diff --git a/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs b/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs index 28d8554e6..c8a331f66 100644 --- a/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs +++ b/implants/lib/eldritch/stdlib/eldritch-libfile/src/std/mod.rs @@ -6,6 +6,7 @@ use eldritch_core::Value; use eldritch_macros::eldritch_library_impl; pub mod append_impl; +pub mod chmod_impl; pub mod compress_impl; pub mod copy_impl; pub mod decompress_impl; @@ -46,6 +47,10 @@ impl FileLibrary for StdFileLibrary { compress_impl::compress(src, dst) } + fn chmod(&self, path: String, mode: i64) -> Result<(), String> { + chmod_impl::chmod(path, mode) + } + fn copy(&self, src: String, dst: String) -> Result<(), String> { copy_impl::copy(src, dst) } diff --git a/tavern/internal/www/src/assets/eldritch-docs.json b/tavern/internal/www/src/assets/eldritch-docs.json index d14058bf2..513340ec3 100644 --- a/tavern/internal/www/src/assets/eldritch-docs.json +++ b/tavern/internal/www/src/assets/eldritch-docs.json @@ -299,6 +299,10 @@ "signature": "file.compress(src: str, dst: str) -> None", "description": "The **file.compress** method compresses a file using the gzip algorithm. If the destination file doesn't exist it will be created. If the source file doesn't exist an error will be thrown. If the source path is a directory the contents will be placed in a tar archive and then compressed." }, + "file.chmod": { + "signature": "file.chmod(path: str, mode: int) -> None", + "description": "The **file.chmod** method changes the permissions of a file. The `mode` should typically be specified in octal (e.g. `0o755`). On Windows, this method only considers the `0o200` (owner writable) bit to determine whether to toggle the read-only attribute of the file, similar to Golang's `os.Chmod`." + }, "file.copy": { "signature": "file.copy(src: str, dst: str) -> None", "description": "The **file.copy** method copies a file from `src` path to `dst` path. If `dst` file doesn't exist it will be created."