feat: builtins.readType & builtins.readDir

This commit is contained in:
2026-01-24 22:17:23 +08:00
parent 13a7d761f4
commit 05b66070a3
4 changed files with 173 additions and 4 deletions

View File

@@ -292,8 +292,14 @@ const autoDetectAndFetch = (attrs: NixAttrs): NixAttrs => {
return { outPath: fetchurl(attrs) }; return { outPath: fetchurl(attrs) };
}; };
export const readDir = (path: NixValue): never => { export const readDir = (path: NixValue): NixAttrs => {
throw new Error("Not implemented: readDir"); const pathStr = coerceToPath(path);
const entries: Record<string, string> = 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 => { export const readFile = (path: NixValue): string => {
@@ -301,8 +307,9 @@ export const readFile = (path: NixValue): string => {
return Deno.core.ops.op_read_file(pathStr); return Deno.core.ops.op_read_file(pathStr);
}; };
export const readFileType = (path: NixValue): never => { export const readFileType = (path: NixValue): string => {
throw new Error("Not implemented: readFileType"); const pathStr = coerceToPath(path);
return Deno.core.ops.op_read_file_type(pathStr);
}; };
export const pathExists = (path: NixValue): boolean => { export const pathExists = (path: NixValue): boolean => {

View File

@@ -38,6 +38,8 @@ declare global {
function op_resolve_path(currentDir: string, path: string): string; function op_resolve_path(currentDir: string, path: string): string;
function op_import(path: string): string; function op_import(path: string): string;
function op_read_file(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<string, string>;
function op_path_exists(path: string): boolean; function op_path_exists(path: string): boolean;
function op_sha256_hex(data: string): string; function op_sha256_hex(data: string): string;
function op_make_store_path(ty: string, hash_hex: string, name: string): string; function op_make_store_path(ty: string, hash_hex: string, name: string): string;

View File

@@ -46,6 +46,8 @@ fn runtime_extension<Ctx: RuntimeContext>() -> Extension {
let mut ops = vec![ let mut ops = vec![
op_import::<Ctx>(), op_import::<Ctx>(),
op_read_file(), op_read_file(),
op_read_file_type(),
op_read_dir(),
op_path_exists(), op_path_exists(),
op_resolve_path(), op_resolve_path(),
op_sha256_hex(), op_sha256_hex(),
@@ -163,6 +165,64 @@ fn op_path_exists(#[string] path: String) -> bool {
std::path::Path::new(&path).exists() std::path::Path::new(&path).exists()
} }
#[deno_core::op2]
#[string]
fn op_read_file_type(#[string] path: String) -> std::result::Result<String, NixError> {
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<std::collections::HashMap<String, String>, 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] #[deno_core::op2]
#[string] #[string]
fn op_resolve_path( fn op_resolve_path(

View File

@@ -260,3 +260,103 @@ fn path_deterministic() {
// Same inputs should produce same store path // Same inputs should produce same store path
assert_eq!(result1, result2); 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"));
}