use std::sync::Arc;
use anyhow::{Result, bail};
use super::crates_bridge::crate_bridge;
use super::json_bridge::bridge_serde_json;
use super::native::Native;
use super::native_methods;
use super::value::{StructData, Value};
fn fs_native_call(func: &str, args: &[Value]) -> Result<Option<Value>> {
let s = |i: usize| -> Result<String> {
match args.get(i) {
Some(v) => Ok(path_like(v)),
None => bail!("missing argument {i} for fs::{func}"),
}
};
Ok(Some(match func {
"read_to_string" => wrap_io(std::fs::read_to_string(s(0)?)),
"read" => wrap_bytes(std::fs::read(s(0)?)),
"write" => wrap_unit(std::fs::write(s(0)?, s(1)?)),
"create_dir_all" => wrap_unit(std::fs::create_dir_all(s(0)?)),
"create_dir" => wrap_unit(std::fs::create_dir(s(0)?)),
"remove_file" => wrap_unit(std::fs::remove_file(s(0)?)),
"remove_dir_all" => wrap_unit(std::fs::remove_dir_all(s(0)?)),
"remove_dir" => wrap_unit(std::fs::remove_dir(s(0)?)),
"copy" => match std::fs::copy(s(0)?, s(1)?) {
Ok(n) => Value::ok(Value::Int(i64::try_from(n).unwrap_or(i64::MAX))),
Err(e) => Value::err(Value::str(e.to_string())),
},
"rename" => wrap_unit(std::fs::rename(s(0)?, s(1)?)),
"read_dir" => match std::fs::read_dir(s(0)?) {
Ok(rd) => {
let mut items = Vec::new();
for e in rd {
match e {
Ok(entry) => items.push(Value::ok(make_dir_entry(&entry))),
Err(err) => items.push(Value::err(Value::str(err.to_string()))),
}
}
Value::ok(Value::vec(items))
}
Err(e) => Value::err(Value::str(e.to_string())),
},
"canonicalize" => match std::fs::canonicalize(s(0)?) {
Ok(p) => Value::ok(make_path(p.display().to_string())),
Err(e) => Value::err(Value::str(e.to_string())),
},
"metadata" => match std::fs::metadata(s(0)?) {
Ok(m) => Value::ok(make_metadata(&m)),
Err(e) => Value::err(Value::str(e.to_string())),
},
"symlink_metadata" => match std::fs::symlink_metadata(s(0)?) {
Ok(m) => Value::ok(make_metadata(&m)),
Err(e) => Value::err(Value::str(e.to_string())),
},
"read_link" => match std::fs::read_link(s(0)?) {
Ok(p) => Value::ok(make_path(p.display().to_string())),
Err(e) => Value::err(Value::str(e.to_string())),
},
"hard_link" => wrap_unit(std::fs::hard_link(s(0)?, s(1)?)),
"symlink" | "symlink_file" | "symlink_dir" => wrap_unit(make_symlink(&s(0)?, &s(1)?)),
"set_permissions" => wrap_unit(set_permissions_impl(
&s(0)?,
args.get(1).and_then(perm_mode),
)),
_ => return crate_bridge("fs", func, args),
}))
}
pub(super) fn native_call(module: &str, func: &str, args: &[Value]) -> Result<Option<Value>> {
if module == "serde_json" {
return bridge_serde_json(func, args).map(Some);
}
if module == "fs" {
return fs_native_call(func, args);
}
let s = |i: usize| -> Result<String> {
match args.get(i) {
Some(v) => Ok(path_like(v)),
None => bail!("missing argument {i} for {module}::{func}"),
}
};
Ok(Some(match (module, func) {
("env", "args") => Value::vec(super::script_args().into_iter().map(Value::str).collect()),
("env", "var") => match std::env::var(s(0)?) {
Ok(v) => Value::ok(Value::str(v)),
Err(e) => Value::err(Value::str(e.to_string())),
},
("env", "current_dir") => match std::env::current_dir() {
Ok(p) => Value::ok(make_path(p.display().to_string())),
Err(e) => Value::err(Value::str(e.to_string())),
},
("env", "set_var") => {
unsafe { std::env::set_var(s(0)?, s(1)?) };
Value::Unit
}
("env", "remove_var") => {
unsafe { std::env::remove_var(s(0)?) };
Value::Unit
}
("env", "var_os") => match std::env::var_os(s(0)?) {
Some(v) => Value::some(make_os_string(v.to_string_lossy().into_owned())),
None => Value::none(),
},
("env", "vars" | "vars_os") => Value::vec(
std::env::vars()
.map(|(k, v)| Value::tuple(vec![Value::str(k), Value::str(v)]))
.collect(),
),
("env", "set_current_dir") => wrap_unit(std::env::set_current_dir(s(0)?)),
("env", "temp_dir") => make_path(std::env::temp_dir().display().to_string()),
("process", "exit") => {
let code = args
.first()
.and_then(as_i64)
.and_then(|c| i32::try_from(c).ok())
.unwrap_or(0);
std::process::exit(code);
}
("process", "abort") => std::process::abort(),
("process", "id") => Value::Int(i64::from(std::process::id())),
("io", "stdin") => make_std_stream(
"stdin",
Native::Reader(std::io::BufReader::new(Box::new(std::io::stdin()))),
),
("io", "stdout") => make_std_stream("stdout", Native::Writer(Box::new(std::io::stdout()))),
("io", "stderr") => make_std_stream("stderr", Native::Writer(Box::new(std::io::stderr()))),
_ => return crate_bridge(module, func, args),
}))
}
fn make_symlink(src: &str, dst: &str) -> std::io::Result<()> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(src, dst)
}
#[cfg(windows)]
{
if std::path::Path::new(src).is_dir() {
std::os::windows::fs::symlink_dir(src, dst)
} else {
std::os::windows::fs::symlink_file(src, dst)
}
}
}
fn perm_mode(v: &Value) -> Option<u32> {
if let Value::Struct(st) = v
&& &**st.name() == "Permissions"
{
return st
.get("mode")
.and_then(|m| as_i64(&m))
.and_then(|m| u32::try_from(m).ok());
}
None
}
fn set_permissions_impl(path: &str, mode: Option<u32>) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode.unwrap_or(0o644)))
}
#[cfg(windows)]
{
let mut permissions = std::fs::metadata(path)?.permissions();
permissions.set_readonly(mode.is_some_and(|mode| mode & 0o222 == 0));
std::fs::set_permissions(path, permissions)
}
}
pub(super) fn as_i64(v: &Value) -> Option<i64> {
match v {
Value::Int(i) => Some(*i),
_ => None,
}
}
pub(super) fn path_like(v: &Value) -> String {
match v {
Value::Str(s) => s.to_string(),
Value::Struct(st) if matches!(&**st.name(), "Path" | "PathBuf" | "OsString") => {
st.get("s").map(|s| s.display()).unwrap_or_default()
}
other => other.display(),
}
}
pub(super) fn make_std_stream(kind: &str, inner: Native) -> Value {
Value::struct_of(
"StdStream",
[
("kind".into(), Value::str(kind)),
("inner".into(), inner.wrap()),
],
)
}
pub(super) fn std_stream_method(
s: &Arc<StructData>,
name: &str,
args: &mut [Value],
) -> Result<Value> {
use std::io::IsTerminal;
if name == "is_terminal" {
let kind = s.get("kind").map(|v| v.display()).unwrap_or_default();
let tty = match kind.as_str() {
"stdin" => std::io::stdin().is_terminal(),
"stderr" => std::io::stderr().is_terminal(),
_ => std::io::stdout().is_terminal(),
};
return Ok(Value::Bool(tty));
}
if matches!(name, "lock" | "by_ref") {
return Ok(Value::Struct(s.clone()));
}
let inner = match s.get("inner") {
Some(Value::Native(h)) => h.clone(),
_ => bail!("std stream lost its handle"),
};
match native_methods::native_method(&inner, name, args)? {
Some(v) => Ok(v),
None => bail!("unknown method `{name}` on a std stream"),
}
}
pub(super) fn duration_from_value(v: &Value) -> Option<std::time::Duration> {
if let Value::Struct(s) = v
&& &**s.name() == "Duration"
{
let secs = u64::try_from(field_int(s, "secs")).unwrap_or_default();
let nanos = u32::try_from(field_int(s, "nanos")).unwrap_or_default();
return Some(std::time::Duration::new(secs, nanos));
}
None
}
pub(super) fn make_duration(d: std::time::Duration) -> Value {
Value::struct_of(
"Duration",
[
(
"secs".into(),
Value::Int(i64::try_from(d.as_secs()).unwrap_or(i64::MAX)),
),
("nanos".into(), Value::Int(i64::from(d.subsec_nanos()))),
],
)
}
pub(super) fn make_metadata(m: &std::fs::Metadata) -> Value {
let mut f: Vec<(Arc<str>, Value)> = vec![
(
"len".into(),
Value::Int(i64::try_from(m.len()).unwrap_or(i64::MAX)),
),
("is_dir".into(), Value::Bool(m.is_dir())),
("is_file".into(), Value::Bool(m.is_file())),
("is_symlink".into(), Value::Bool(m.is_symlink())),
("readonly".into(), Value::Bool(m.permissions().readonly())),
];
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::PermissionsExt;
f.push(("mode".into(), Value::Int(i64::from(m.permissions().mode()))));
f.push(("dev".into(), Value::Int(m.dev().cast_signed())));
f.push(("ino".into(), Value::Int(m.ino().cast_signed())));
f.push(("uid".into(), Value::Int(i64::from(m.uid()))));
f.push(("gid".into(), Value::Int(i64::from(m.gid()))));
f.push(("mtime".into(), Value::Int(m.mtime())));
}
if let Ok(t) = m.modified() {
f.push(("modified".into(), Native::SystemTime(t).wrap()));
}
Value::struct_of("Metadata", f)
}
pub(super) fn make_path(s: impl Into<String>) -> Value {
Value::struct_of("Path", [("s".into(), Value::str(s.into()))])
}
pub(super) fn make_os_string(s: impl Into<String>) -> Value {
Value::struct_of("OsString", [("s".into(), Value::str(s.into()))])
}
pub(super) fn os_string_method(s: &Arc<StructData>, method: &str) -> Result<Value> {
let value = s.get("s").map(|value| value.display()).unwrap_or_default();
Ok(match method {
"into" => make_path(value),
"to_string_lossy" | "to_str" => Value::str(value),
"is_empty" => Value::Bool(value.is_empty()),
_ => bail!("unknown method `{method}` on OsString"),
})
}
pub(super) fn make_dir_entry(entry: &std::fs::DirEntry) -> Value {
Value::struct_of(
"DirEntry",
[
(
"path".into(),
Value::str(entry.path().display().to_string()),
),
(
"name".into(),
Value::str(entry.file_name().to_string_lossy().into_owned()),
),
],
)
}
pub(super) fn make_file_type(path: &std::path::Path) -> Value {
let ft = path.symlink_metadata().map(|m| m.file_type());
let is = |f: &dyn Fn(&std::fs::FileType) -> bool| Value::Bool(ft.as_ref().is_ok_and(f));
Value::struct_of(
"FileType",
[
("is_dir".into(), is(&|t| t.is_dir())),
("is_file".into(), is(&|t| t.is_file())),
("is_symlink".into(), is(&|t| t.is_symlink())),
],
)
}
pub(super) fn path_string(s: &StructData, key: &str) -> String {
s.get(key).map(|v| v.display()).unwrap_or_default()
}
pub(super) fn path_method(st: &Arc<StructData>, method: &str, args: &[Value]) -> Result<Value> {
let s = path_string(st, "s");
let p = std::path::Path::new(&s);
let opt_str = |o: Option<&std::ffi::OsStr>| match o {
Some(v) => Value::some(Value::str(v.to_string_lossy().into_owned())),
None => Value::none(),
};
Ok(match method {
"display" | "to_string_lossy" => Value::str(s.clone()),
"to_str" => Value::some(Value::str(s.clone())),
"into_string" | "into_os_string" => Value::ok(Value::str(s.clone())),
"to_owned" | "to_path_buf" | "clone" | "as_path" | "as_os_str" => make_path(s.clone()),
"is_dir" => Value::Bool(p.is_dir()),
"is_file" => Value::Bool(p.is_file()),
"is_absolute" => Value::Bool(p.is_absolute()),
"exists" => Value::Bool(p.exists()),
"file_name" => match p.file_name() {
Some(n) => Value::some(make_path(n.to_string_lossy().into_owned())),
None => Value::none(),
},
"file_stem" => opt_str(p.file_stem()),
"extension" => opt_str(p.extension()),
"with_extension" => make_path(p.with_extension(arg_str(args, 0)).display().to_string()),
"parent" => match p.parent() {
Some(par) => Value::some(make_path(par.display().to_string())),
None => Value::none(),
},
"ancestors" => Value::vec(
p.ancestors()
.map(|ancestor| make_path(ancestor.display().to_string()))
.collect(),
),
"join" | "push" => {
let joined = p.join(args.first().map(Value::display).unwrap_or_default());
make_path(joined.display().to_string())
}
_ => bail!("unknown method `{method}` on Path"),
})
}
pub(super) fn dir_entry_method(s: &Arc<StructData>, method: &str) -> Result<Value> {
let path = path_string(s, "path");
Ok(match method {
"path" => make_path(path),
"file_name" => make_path(path_string(s, "name")),
"file_type" => Value::ok(make_file_type(std::path::Path::new(&path))),
_ => bail!("unknown method `{method}` on DirEntry"),
})
}
pub(super) fn file_type_method(s: &Arc<StructData>, method: &str) -> Result<Value> {
let get = |k: &str| s.get(k).unwrap_or(Value::Bool(false));
Ok(match method {
"is_dir" => get("is_dir"),
"is_file" => get("is_file"),
"is_symlink" => get("is_symlink"),
_ => bail!("unknown method `{method}` on FileType"),
})
}
pub(super) fn metadata_method(s: &Arc<StructData>, name: &str) -> Result<Value> {
let get = |k: &str| s.get(k).unwrap_or(Value::Unit);
Ok(match name {
"len" => get("len"),
"is_dir" => get("is_dir"),
"is_file" => get("is_file"),
"is_symlink" => get("is_symlink"),
"modified" | "created" | "accessed" => match s.get("modified") {
Some(v) => Value::ok(v),
None => Value::err(Value::str("timestamp not available".to_string())),
},
"mode" | "dev" | "ino" | "uid" | "gid" | "mtime" => get(name),
"permissions" => Value::struct_of(
"Permissions",
[
("mode".into(), get("mode")),
("readonly".into(), get("readonly")),
],
),
_ => bail!("unknown method `{name}` on Metadata"),
})
}
pub(super) fn wrap_io(r: std::io::Result<String>) -> Value {
match r {
Ok(s) => Value::ok(Value::str(s)),
Err(e) => Value::err(Value::str(e.to_string())),
}
}
pub(super) fn wrap_bytes(r: std::io::Result<Vec<u8>>) -> Value {
match r {
Ok(bytes) => Value::ok(Value::vec(
bytes
.into_iter()
.map(|b| Value::Int(i64::from(b)))
.collect(),
)),
Err(e) => Value::err(Value::str(e.to_string())),
}
}
pub(super) fn wrap_unit(r: std::io::Result<()>) -> Value {
match r {
Ok(()) => Value::ok(Value::Unit),
Err(e) => Value::err(Value::str(e.to_string())),
}
}
pub(super) fn field_int(s: &StructData, k: &str) -> i64 {
match s.get(k) {
Some(Value::Int(i)) => i,
_ => 0,
}
}
pub(super) fn arg_str(args: &[Value], i: usize) -> String {
args.get(i).map(path_like).unwrap_or_default()
}
pub(super) fn arg_int(args: &[Value], i: usize) -> i64 {
match args.get(i) {
Some(Value::Int(n)) => *n,
_ => 0,
}
}
pub(super) fn open_file(path: &str, opts: &std::fs::OpenOptions) -> Value {
match opts.open(path) {
Ok(f) => Value::ok(Native::File(std::io::BufReader::new(f)).wrap()),
Err(e) => Value::err(Value::str(e.to_string())),
}
}
pub(super) fn openoptions_method(s: &StructData, name: &str, args: &[Value]) -> Result<Value> {
const FLAGS: [&str; 6] = [
"read",
"write",
"append",
"create",
"create_new",
"truncate",
];
let field_bool = |k: &str| matches!(s.get(k), Some(Value::Bool(true)));
if FLAGS.contains(&name) {
let on = matches!(args.first(), Some(Value::Bool(true)));
let pairs = FLAGS.iter().map(|&k| {
(
Arc::from(k),
Value::Bool(if k == name { on } else { field_bool(k) }),
)
});
return Ok(Value::struct_of("OpenOptions", pairs));
}
if name == "open" {
let path = args.first().map(path_like).unwrap_or_default();
let mut opts = std::fs::OpenOptions::new();
opts.read(field_bool("read"))
.write(field_bool("write"))
.append(field_bool("append"))
.create(field_bool("create"))
.create_new(field_bool("create_new"))
.truncate(field_bool("truncate"));
return Ok(open_file(&path, &opts));
}
bail!("unknown method `{name}` on OpenOptions")
}
pub(super) fn bytes_to_string(arg: Option<&Value>) -> String {
match arg {
Some(Value::Str(s)) => s.to_string(),
Some(Value::Vec(v)) => {
let bytes: Vec<u8> = v
.lock()
.iter()
.filter_map(|x| match x {
Value::Int(i) => u8::try_from(*i).ok(),
_ => None,
})
.collect();
String::from_utf8_lossy(&bytes).into_owned()
}
_ => String::new(),
}
}