use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
use std::collections::BTreeSet;
use std::sync::Arc;
fn policy_walk_only(read_root: &std::path::Path) -> Policy {
let mut p = Policy::pure();
p.allow_effects = ["fs_walk".to_string()].into_iter().collect::<BTreeSet<_>>();
p.allow_fs_read = vec![read_root.to_path_buf()];
p
}
fn policy_walk_and_write(read_root: &std::path::Path, write_root: &std::path::Path) -> Policy {
let mut p = Policy::pure();
p.allow_effects = ["fs_walk".to_string(), "fs_write".to_string()]
.into_iter()
.collect::<BTreeSet<_>>();
p.allow_fs_read = vec![read_root.to_path_buf()];
p.allow_fs_write = vec![write_root.to_path_buf()];
p
}
fn run(src: &str, fn_name: &str, args: Vec<Value>, policy: Policy) -> Value {
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors:\n{errs:#?}");
}
let bc = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(policy).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(fn_name, args).unwrap_or_else(|e| panic!("call {fn_name}: {e}"))
}
fn unique_dir(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"lex-fs-{}-{}-{}",
std::process::id(),
name,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
const SRC: &str = r#"
import "std.fs" as fs
import "std.list" as list
fn does_exist(path :: Str) -> [fs_walk] Bool { fs.exists(path) }
fn is_a_dir(path :: Str) -> [fs_walk] Bool { fs.is_dir(path) }
fn count_walk(path :: Str) -> [fs_walk] Int {
match fs.walk(path) {
Ok(paths) => list.len(paths),
Err(_) => 0 - 1,
}
}
fn list_dir_count(path :: Str) -> [fs_walk] Int {
match fs.list_dir(path) {
Ok(paths) => list.len(paths),
Err(_) => 0 - 1,
}
}
fn make_dir(path :: Str) -> [fs_write] Bool {
match fs.mkdir_p(path) {
Ok(_) => true,
Err(_) => false,
}
}
fn stat_size(path :: Str) -> [fs_walk] Int {
match fs.stat(path) {
Ok(s) => s.size,
Err(_) => 0 - 1,
}
}
"#;
#[test]
fn exists_returns_true_for_a_real_path() {
let dir = unique_dir("exists_true");
let v = run(
SRC,
"does_exist",
vec![Value::Str(dir.to_string_lossy().to_string().into())],
policy_walk_only(&dir),
);
assert_eq!(v, Value::Bool(true));
}
#[test]
fn exists_returns_false_for_nonexistent_path() {
let dir = unique_dir("exists_false");
let phantom = dir.join("nope");
let v = run(
SRC,
"does_exist",
vec![Value::Str(phantom.to_string_lossy().to_string().into())],
policy_walk_only(&dir),
);
assert_eq!(v, Value::Bool(false));
}
#[test]
fn is_dir_distinguishes_dir_from_file() {
let dir = unique_dir("is_dir");
let file = dir.join("a.txt");
std::fs::write(&file, "hi").unwrap();
let v = run(SRC, "is_a_dir", vec![Value::Str(dir.to_string_lossy().to_string().into())], policy_walk_only(&dir));
assert_eq!(v, Value::Bool(true));
let v = run(SRC, "is_a_dir", vec![Value::Str(file.to_string_lossy().to_string().into())], policy_walk_only(&dir));
assert_eq!(v, Value::Bool(false));
}
#[test]
fn walk_returns_recursive_path_count() {
let dir = unique_dir("walk");
std::fs::create_dir(dir.join("sub")).unwrap();
std::fs::write(dir.join("a.txt"), "a").unwrap();
std::fs::write(dir.join("sub/b.txt"), "b").unwrap();
let v = run(
SRC,
"count_walk",
vec![Value::Str(dir.to_string_lossy().to_string().into())],
policy_walk_only(&dir),
);
assert_eq!(v, Value::Int(4));
}
#[test]
fn list_dir_returns_immediate_children_only() {
let dir = unique_dir("list_dir");
std::fs::create_dir(dir.join("sub")).unwrap();
std::fs::write(dir.join("a.txt"), "a").unwrap();
std::fs::write(dir.join("sub/b.txt"), "b").unwrap();
let v = run(
SRC,
"list_dir_count",
vec![Value::Str(dir.to_string_lossy().to_string().into())],
policy_walk_only(&dir),
);
assert_eq!(v, Value::Int(2));
}
#[test]
fn mkdir_p_creates_nested() {
let root = unique_dir("mkdir_root");
let nested = root.join("a/b/c");
let v = run(
SRC,
"make_dir",
vec![Value::Str(nested.to_string_lossy().to_string().into())],
policy_walk_and_write(&root, &root),
);
assert_eq!(v, Value::Bool(true));
assert!(nested.exists());
}
#[test]
fn mkdir_p_outside_write_root_returns_err() {
let allowed = unique_dir("mkdir_allowed");
let outside = unique_dir("mkdir_outside").join("new_subdir");
let v = run(
SRC,
"make_dir",
vec![Value::Str(outside.to_string_lossy().to_string().into())],
policy_walk_and_write(&allowed, &allowed),
);
assert_eq!(v, Value::Bool(false));
}
#[test]
fn stat_returns_size_of_file() {
let dir = unique_dir("stat");
let file = dir.join("payload.txt");
std::fs::write(&file, b"abcdef").unwrap();
let v = run(
SRC,
"stat_size",
vec![Value::Str(file.to_string_lossy().to_string().into())],
policy_walk_only(&dir),
);
assert_eq!(v, Value::Int(6));
}
fn policy_read_only(read_root: &std::path::Path) -> Policy {
let mut p = Policy::pure();
p.allow_effects = ["fs_read".to_string()].into_iter().collect::<BTreeSet<_>>();
p.allow_fs_read = vec![read_root.to_path_buf()];
p
}
fn policy_write_only(write_root: &std::path::Path) -> Policy {
let mut p = Policy::pure();
p.allow_effects = ["fs_write".to_string()].into_iter().collect::<BTreeSet<_>>();
p.allow_fs_write = vec![write_root.to_path_buf()];
p
}
const READ_SRC: &str = r#"
import "std.fs" as fs
fn read_it(p :: Str) -> [fs_read] Str {
match fs.read_to_string(p) {
Err(m) => m,
Ok(t) => t,
}
}
"#;
const WRITE_SRC: &str = r#"
import "std.fs" as fs
fn write_it(p :: Str, body :: Str) -> [fs_write] Str {
match fs.write(p, body) {
Err(m) => m,
Ok(_) => "WROTE",
}
}
"#;
#[test]
fn fs_read_to_string_reads_inside_the_allowlist() {
let dir = unique_dir("read-ok");
let file = dir.join("in.txt");
std::fs::write(&file, "hello-from-fs").unwrap();
let got = run(
READ_SRC,
"read_it",
vec![Value::Str(file.to_string_lossy().into_owned().into())],
policy_read_only(&dir),
);
assert_eq!(got, Value::Str("hello-from-fs".into()));
}
#[test]
fn fs_read_to_string_refuses_outside_the_allowlist() {
let dir = unique_dir("read-deny");
let outside = unique_dir("read-deny-other").join("secret.txt");
std::fs::write(&outside, "should-not-be-readable").unwrap();
let prog = parse_source(READ_SRC).expect("parse");
let stages = canonicalize_program(&prog);
lex_types::check_program(&stages).expect("type errors");
let bc = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(policy_read_only(&dir)).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
let err = vm
.call(
"read_it",
vec![Value::Str(outside.to_string_lossy().into_owned().into())],
)
.expect_err("read outside --allow-fs-read must be refused");
assert!(
format!("{err}").contains("outside --allow-fs-read"),
"unexpected error: {err}"
);
}
#[test]
fn fs_write_writes_inside_the_allowlist() {
let dir = unique_dir("write-ok");
let file = dir.join("out.txt");
let got = run(
WRITE_SRC,
"write_it",
vec![
Value::Str(file.to_string_lossy().into_owned().into()),
Value::Str("written-by-fs".into()),
],
policy_write_only(&dir),
);
assert_eq!(got, Value::Str("WROTE".into()));
assert_eq!(std::fs::read_to_string(&file).unwrap(), "written-by-fs");
}
#[test]
fn fs_write_refuses_outside_the_allowlist() {
let dir = unique_dir("write-deny");
let outside = unique_dir("write-deny-other").join("nope.txt");
let prog = parse_source(WRITE_SRC).expect("parse");
let stages = canonicalize_program(&prog);
lex_types::check_program(&stages).expect("type errors");
let bc = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(policy_write_only(&dir)).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
let err = vm
.call(
"write_it",
vec![
Value::Str(outside.to_string_lossy().into_owned().into()),
Value::Str("nope".into()),
],
)
.expect_err("write outside --allow-fs-write must be refused");
assert!(
format!("{err}").contains("outside --allow-fs-write"),
"unexpected error: {err}"
);
assert!(!outside.exists(), "refused write must not create the file");
}
#[test]
fn granting_io_does_not_reach_fs_content_ops() {
let dir = unique_dir("io-not-fs");
let file = dir.join("in.txt");
std::fs::write(&file, "x").unwrap();
let mut p = Policy::pure();
p.allow_effects = ["io".to_string()].into_iter().collect::<BTreeSet<_>>();
p.allow_fs_read = vec![dir.clone()];
let prog = parse_source(READ_SRC).expect("parse");
let stages = canonicalize_program(&prog);
lex_types::check_program(&stages).expect("type errors");
let bc = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(p).with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
let err = vm
.call(
"read_it",
vec![Value::Str(file.to_string_lossy().into_owned().into())],
)
.expect_err("[io] must not satisfy an [fs_read] program");
assert!(
format!("{err}").contains("fs_read"),
"unexpected error: {err}"
);
}