From 05b66070a3b62c880866b287bad796d3c2a354e3 Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Sat, 24 Jan 2026 22:17:23 +0800 Subject: [PATCH] feat: builtins.readType & builtins.readDir --- nix-js/runtime-ts/src/builtins/io.ts | 15 +++- nix-js/runtime-ts/src/types/global.d.ts | 2 + nix-js/src/runtime.rs | 60 ++++++++++++++ nix-js/tests/io_operations.rs | 100 ++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 4 deletions(-) diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index b1b3b31..89429d6 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -292,8 +292,14 @@ const autoDetectAndFetch = (attrs: NixAttrs): NixAttrs => { return { outPath: fetchurl(attrs) }; }; -export const readDir = (path: NixValue): never => { - throw new Error("Not implemented: readDir"); +export const readDir = (path: NixValue): NixAttrs => { + const pathStr = coerceToPath(path); + const entries: Record = Deno.core.ops.op_read_dir(pathStr); + const result: NixAttrs = {}; + for (const [name, type] of Object.entries(entries)) { + result[name] = type; + } + return result; }; export const readFile = (path: NixValue): string => { @@ -301,8 +307,9 @@ export const readFile = (path: NixValue): string => { return Deno.core.ops.op_read_file(pathStr); }; -export const readFileType = (path: NixValue): never => { - throw new Error("Not implemented: readFileType"); +export const readFileType = (path: NixValue): string => { + const pathStr = coerceToPath(path); + return Deno.core.ops.op_read_file_type(pathStr); }; export const pathExists = (path: NixValue): boolean => { diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index c979cb7..8fe747f 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -38,6 +38,8 @@ declare global { function op_resolve_path(currentDir: string, path: string): string; function op_import(path: string): string; function op_read_file(path: string): string; + function op_read_file_type(path: string): string; + function op_read_dir(path: string): Record; function op_path_exists(path: string): boolean; function op_sha256_hex(data: string): string; function op_make_store_path(ty: string, hash_hex: string, name: string): string; diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 6727ebb..5711e33 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -46,6 +46,8 @@ fn runtime_extension() -> Extension { let mut ops = vec![ op_import::(), op_read_file(), + op_read_file_type(), + op_read_dir(), op_path_exists(), op_resolve_path(), op_sha256_hex(), @@ -163,6 +165,64 @@ fn op_path_exists(#[string] path: String) -> bool { std::path::Path::new(&path).exists() } +#[deno_core::op2] +#[string] +fn op_read_file_type(#[string] path: String) -> std::result::Result { + let path = Path::new(&path); + let metadata = std::fs::symlink_metadata(path) + .map_err(|e| format!("Failed to read file type for {}: {}", path.display(), e))?; + + let file_type = metadata.file_type(); + let type_str = if file_type.is_dir() { + "directory" + } else if file_type.is_symlink() { + "symlink" + } else if file_type.is_file() { + "regular" + } else { + "unknown" + }; + + Ok(type_str.to_string()) +} + +#[deno_core::op2] +#[serde] +fn op_read_dir(#[string] path: String) -> std::result::Result, NixError> { + let path = Path::new(&path); + + if !path.is_dir() { + return Err(format!("{} is not a directory", path.display()).into()); + } + + let entries = std::fs::read_dir(path) + .map_err(|e| format!("Failed to read directory {}: {}", path.display(), e))?; + + let mut result = std::collections::HashMap::new(); + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; + let file_name = entry.file_name().to_string_lossy().to_string(); + + let file_type = entry.file_type() + .map_err(|e| format!("Failed to read file type for {}: {}", entry.path().display(), e))?; + + let type_str = if file_type.is_dir() { + "directory" + } else if file_type.is_symlink() { + "symlink" + } else if file_type.is_file() { + "regular" + } else { + "unknown" + }; + + result.insert(file_name, type_str.to_string()); + } + + Ok(result) +} + #[deno_core::op2] #[string] fn op_resolve_path( diff --git a/nix-js/tests/io_operations.rs b/nix-js/tests/io_operations.rs index d6f3341..7b90f38 100644 --- a/nix-js/tests/io_operations.rs +++ b/nix-js/tests/io_operations.rs @@ -260,3 +260,103 @@ fn path_deterministic() { // Same inputs should produce same store path assert_eq!(result1, result2); } + +#[test] +fn read_file_type_regular_file() { + let temp_dir = tempfile::tempdir().unwrap(); + let test_file = temp_dir.path().join("test.txt"); + std::fs::write(&test_file, "Test content").unwrap(); + + let expr = format!(r#"builtins.readFileType {}"#, test_file.display()); + assert_eq!(eval(&expr), Value::String("regular".to_string())); +} + +#[test] +fn read_file_type_directory() { + let temp_dir = tempfile::tempdir().unwrap(); + let test_dir = temp_dir.path().join("testdir"); + std::fs::create_dir(&test_dir).unwrap(); + + let expr = format!(r#"builtins.readFileType {}"#, test_dir.display()); + assert_eq!(eval(&expr), Value::String("directory".to_string())); +} + +#[test] +fn read_file_type_symlink() { + let temp_dir = tempfile::tempdir().unwrap(); + let target = temp_dir.path().join("target.txt"); + let symlink = temp_dir.path().join("link.txt"); + + std::fs::write(&target, "Target content").unwrap(); + + #[cfg(unix)] + std::os::unix::fs::symlink(&target, &symlink).unwrap(); + + #[cfg(unix)] + { + let expr = format!(r#"builtins.readFileType {}"#, symlink.display()); + assert_eq!(eval(&expr), Value::String("symlink".to_string())); + } +} + +#[test] +fn read_dir_basic() { + let temp_dir = tempfile::tempdir().unwrap(); + let test_dir = temp_dir.path().join("readdir_test"); + std::fs::create_dir(&test_dir).unwrap(); + + std::fs::write(test_dir.join("file1.txt"), "Content 1").unwrap(); + std::fs::write(test_dir.join("file2.txt"), "Content 2").unwrap(); + std::fs::create_dir(test_dir.join("subdir")).unwrap(); + + let expr = format!(r#"builtins.readDir {}"#, test_dir.display()); + let result = eval(&expr); + + if let Value::AttrSet(attrs) = result { + assert_eq!(attrs.get("file1.txt"), Some(&Value::String("regular".to_string()))); + assert_eq!(attrs.get("file2.txt"), Some(&Value::String("regular".to_string()))); + assert_eq!(attrs.get("subdir"), Some(&Value::String("directory".to_string()))); + assert_eq!(attrs.len(), 3); + } else { + panic!("Expected AttrSet, got {:?}", result); + } +} + +#[test] +fn read_dir_empty() { + let temp_dir = tempfile::tempdir().unwrap(); + let test_dir = temp_dir.path().join("empty_dir"); + std::fs::create_dir(&test_dir).unwrap(); + + let expr = format!(r#"builtins.readDir {}"#, test_dir.display()); + let result = eval(&expr); + + if let Value::AttrSet(attrs) = result { + assert_eq!(attrs.len(), 0); + } else { + panic!("Expected AttrSet, got {:?}", result); + } +} + +#[test] +fn read_dir_nonexistent_fails() { + let expr = r#"builtins.readDir "/nonexistent/directory""#; + let result = eval_result(expr); + + assert!(result.is_err()); +} + +#[test] +fn read_dir_on_file_fails() { + let temp_dir = tempfile::tempdir().unwrap(); + let test_file = temp_dir.path().join("test.txt"); + std::fs::write(&test_file, "Test content").unwrap(); + + let expr = format!(r#"builtins.readDir {}"#, test_file.display()); + let result = eval_result(&expr); + + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("not a directory")); +} +