use std::time::UNIX_EPOCH;
use super::super::generated::yosh::plugin::files::{DirEntry, FileStat};
use super::super::generated::yosh::plugin::types::ErrorCode;
use super::HostContext;
pub fn host_files_read_file(ctx: &HostContext, path: &str) -> Result<Vec<u8>, ErrorCode> {
ctx.ensure_bound()?;
if path.is_empty() {
return Err(ErrorCode::InvalidArgument);
}
match std::fs::read(path) {
Ok(bytes) => Ok(bytes),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(ErrorCode::NotFound),
Err(_) => Err(ErrorCode::IoFailed),
}
}
pub fn host_files_read_dir(ctx: &HostContext, path: &str) -> Result<Vec<DirEntry>, ErrorCode> {
ctx.ensure_bound()?;
if path.is_empty() {
return Err(ErrorCode::InvalidArgument);
}
let iter = match std::fs::read_dir(path) {
Ok(i) => i,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Err(ErrorCode::NotFound),
Err(_) => return Err(ErrorCode::IoFailed),
};
let mut out = Vec::new();
for entry in iter {
let entry = entry.map_err(|_| ErrorCode::IoFailed)?;
let ft = entry.file_type().map_err(|_| ErrorCode::IoFailed)?;
out.push(DirEntry {
name: entry.file_name().to_string_lossy().into_owned(),
is_file: ft.is_file(),
is_dir: ft.is_dir(),
is_symlink: ft.is_symlink(),
});
}
Ok(out)
}
pub fn host_files_metadata(ctx: &HostContext, path: &str) -> Result<FileStat, ErrorCode> {
ctx.ensure_bound()?;
if path.is_empty() {
return Err(ErrorCode::InvalidArgument);
}
let md = match std::fs::metadata(path) {
Ok(m) => m,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Err(ErrorCode::NotFound),
Err(_) => return Err(ErrorCode::IoFailed),
};
let mtime_secs = md
.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64)
.unwrap_or(-1);
Ok(FileStat {
is_file: md.is_file(),
is_dir: md.is_dir(),
is_symlink: md.file_type().is_symlink(),
size: md.len(),
mtime_secs,
})
}
pub fn host_files_write_file(ctx: &HostContext, path: &str, data: &[u8]) -> Result<(), ErrorCode> {
ctx.ensure_bound()?;
if path.is_empty() {
return Err(ErrorCode::InvalidArgument);
}
std::fs::write(path, data).map_err(|_| ErrorCode::IoFailed)
}
pub fn host_files_append_file(ctx: &HostContext, path: &str, data: &[u8]) -> Result<(), ErrorCode> {
ctx.ensure_bound()?;
if path.is_empty() {
return Err(ErrorCode::InvalidArgument);
}
use std::io::Write as _;
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|_| ErrorCode::IoFailed)?;
f.write_all(data).map_err(|_| ErrorCode::IoFailed)
}
pub fn host_files_create_dir(
ctx: &HostContext,
path: &str,
recursive: bool,
) -> Result<(), ErrorCode> {
ctx.ensure_bound()?;
if path.is_empty() {
return Err(ErrorCode::InvalidArgument);
}
let result = if recursive {
std::fs::create_dir_all(path)
} else {
std::fs::create_dir(path)
};
result.map_err(|_| ErrorCode::IoFailed)
}
pub fn host_files_remove_file(ctx: &HostContext, path: &str) -> Result<(), ErrorCode> {
ctx.ensure_bound()?;
if path.is_empty() {
return Err(ErrorCode::InvalidArgument);
}
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(ErrorCode::NotFound),
Err(_) => Err(ErrorCode::IoFailed),
}
}
pub fn host_files_remove_dir(
ctx: &HostContext,
path: &str,
recursive: bool,
) -> Result<(), ErrorCode> {
ctx.ensure_bound()?;
if path.is_empty() {
return Err(ErrorCode::InvalidArgument);
}
let result = if recursive {
std::fs::remove_dir_all(path)
} else {
std::fs::remove_dir(path)
};
match result {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(ErrorCode::NotFound),
Err(_) => Err(ErrorCode::IoFailed),
}
}
pub fn deny_files_read_file(_ctx: &HostContext, _path: &str) -> Result<Vec<u8>, ErrorCode> {
Err(ErrorCode::Denied)
}
pub fn deny_files_read_dir(_ctx: &HostContext, _path: &str) -> Result<Vec<DirEntry>, ErrorCode> {
Err(ErrorCode::Denied)
}
pub fn deny_files_metadata(_ctx: &HostContext, _path: &str) -> Result<FileStat, ErrorCode> {
Err(ErrorCode::Denied)
}
pub fn deny_files_write_file(
_ctx: &HostContext,
_path: &str,
_data: &[u8],
) -> Result<(), ErrorCode> {
Err(ErrorCode::Denied)
}
pub fn deny_files_append_file(
_ctx: &HostContext,
_path: &str,
_data: &[u8],
) -> Result<(), ErrorCode> {
Err(ErrorCode::Denied)
}
pub fn deny_files_create_dir(
_ctx: &HostContext,
_path: &str,
_recursive: bool,
) -> Result<(), ErrorCode> {
Err(ErrorCode::Denied)
}
pub fn deny_files_remove_file(_ctx: &HostContext, _path: &str) -> Result<(), ErrorCode> {
Err(ErrorCode::Denied)
}
pub fn deny_files_remove_dir(
_ctx: &HostContext,
_path: &str,
_recursive: bool,
) -> Result<(), ErrorCode> {
Err(ErrorCode::Denied)
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::{bound_env_ctx, null_env_ctx};
use super::*;
use crate::env::ShellEnv;
use tempfile::tempdir;
#[test]
fn files_read_file_denied_when_env_null() {
let ctx = null_env_ctx();
let result = host_files_read_file(&ctx, "/tmp/anything");
assert_eq!(result, Err(ErrorCode::Denied));
}
#[test]
fn host_files_read_file_roundtrip() {
let dir = tempdir().unwrap();
let path = dir.path().join("data.txt");
let payload = b"hello world".to_vec();
std::fs::write(&path, &payload).unwrap();
let mut env = ShellEnv::new("yosh", vec![]);
let ctx = bound_env_ctx(&mut env);
let result = host_files_read_file(&ctx, &path.to_string_lossy());
assert_eq!(result, Ok(payload));
}
#[test]
fn host_files_read_dir_returns_entries() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("a.txt"), b"").unwrap();
std::fs::create_dir(dir.path().join("sub")).unwrap();
let mut env = ShellEnv::new("yosh", vec![]);
let ctx = bound_env_ctx(&mut env);
let entries = host_files_read_dir(&ctx, &dir.path().to_string_lossy()).unwrap();
assert_eq!(entries.len(), 2);
let a = entries.iter().find(|e| e.name == "a.txt").expect("a.txt");
assert!(a.is_file);
assert!(!a.is_dir);
let sub = entries.iter().find(|e| e.name == "sub").expect("sub");
assert!(!sub.is_file);
assert!(sub.is_dir);
}
#[test]
fn host_files_metadata_distinguishes_file_and_dir() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("f");
std::fs::write(&file_path, b"abc").unwrap();
let mut env = ShellEnv::new("yosh", vec![]);
let ctx = bound_env_ctx(&mut env);
let f = host_files_metadata(&ctx, &file_path.to_string_lossy()).unwrap();
assert!(f.is_file);
assert!(!f.is_dir);
assert_eq!(f.size, 3);
let d = host_files_metadata(&ctx, &dir.path().to_string_lossy()).unwrap();
assert!(!d.is_file);
assert!(d.is_dir);
}
#[test]
fn host_files_read_file_returns_not_found_for_missing_path() {
let dir = tempdir().unwrap();
let missing = dir.path().join("does-not-exist.txt");
let mut env = ShellEnv::new("yosh", vec![]);
let ctx = bound_env_ctx(&mut env);
let result = host_files_read_file(&ctx, &missing.to_string_lossy());
assert_eq!(result, Err(ErrorCode::NotFound));
}
#[test]
fn host_files_read_file_invalid_argument_on_empty_path() {
let mut env = ShellEnv::new("yosh", vec![]);
let ctx = bound_env_ctx(&mut env);
let result = host_files_read_file(&ctx, "");
assert_eq!(result, Err(ErrorCode::InvalidArgument));
}
#[test]
fn host_files_remove_dir_io_failed_on_nonempty_without_recursive() {
let dir = tempdir().unwrap();
let inner = dir.path().join("d");
std::fs::create_dir(&inner).unwrap();
std::fs::write(inner.join("f"), b"x").unwrap();
let mut env = ShellEnv::new("yosh", vec![]);
let ctx = bound_env_ctx(&mut env);
let result = host_files_remove_dir(&ctx, &inner.to_string_lossy(), false);
assert_eq!(result, Err(ErrorCode::IoFailed));
assert!(inner.exists());
}
#[test]
fn host_files_append_file_appends() {
let dir = tempdir().unwrap();
let path = dir.path().join("log");
let mut env = ShellEnv::new("yosh", vec![]);
let ctx = bound_env_ctx(&mut env);
let p = path.to_string_lossy();
host_files_write_file(&ctx, &p, b"hello").unwrap();
host_files_append_file(&ctx, &p, b" world").unwrap();
let bytes = std::fs::read(&path).unwrap();
assert_eq!(bytes, b"hello world");
}
#[test]
fn host_files_create_dir_all_creates_intermediate_dirs() {
let dir = tempdir().unwrap();
let nested = dir.path().join("a/b/c");
let mut env = ShellEnv::new("yosh", vec![]);
let ctx = bound_env_ctx(&mut env);
host_files_create_dir(&ctx, &nested.to_string_lossy(), true).unwrap();
assert!(nested.is_dir());
assert!(dir.path().join("a").is_dir());
assert!(dir.path().join("a/b").is_dir());
}
#[test]
fn host_files_remove_dir_recursive_removes_subtree() {
let dir = tempdir().unwrap();
let root = dir.path().join("tree");
std::fs::create_dir_all(root.join("inner")).unwrap();
std::fs::write(root.join("f"), b"x").unwrap();
std::fs::write(root.join("inner/g"), b"y").unwrap();
let mut env = ShellEnv::new("yosh", vec![]);
let ctx = bound_env_ctx(&mut env);
host_files_remove_dir(&ctx, &root.to_string_lossy(), true).unwrap();
assert!(!root.exists());
}
}