diff --git a/.gitignore b/.gitignore index 986678dc..82ac95e0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist .claude/settings.local.json *.tsbuildinfo .DS_Store +/.vscode/settings.json diff --git a/crates/fspy/tests/node_fs.rs b/crates/fspy/tests/node_fs.rs index 3bed927f..54f6b786 100644 --- a/crates/fspy/tests/node_fs.rs +++ b/crates/fspy/tests/node_fs.rs @@ -1,17 +1,21 @@ mod test_utils; -use std::env::{current_dir, vars_os}; +use std::{ + env::{current_dir, vars_os}, + ffi::OsStr, +}; use fspy::{AccessMode, PathAccessIterable}; use test_log::test; use test_utils::assert_contains; -async fn track_node_script(script: &str) -> anyhow::Result { +async fn track_node_script(script: &str, args: &[&OsStr]) -> anyhow::Result { let mut command = fspy::Command::new("node"); command .arg("-e") .envs(vars_os()) // https://github.com/jdx/mise/discussions/5968 - .arg(script); + .arg(script) + .args(args); let child = command.spawn().await?; let termination = child.wait_handle.await?; assert!(termination.status.success()); @@ -20,14 +24,65 @@ async fn track_node_script(script: &str) -> anyhow::Result { #[test(tokio::test)] async fn read_sync() -> anyhow::Result<()> { - let accesses = track_node_script("try { fs.readFileSync('hello') } catch {}").await?; + let accesses = track_node_script("try { fs.readFileSync('hello') } catch {}", &[]).await?; assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::Read); Ok(()) } +#[test(tokio::test)] +async fn exist_sync() -> anyhow::Result<()> { + let accesses = track_node_script("try { fs.existsSync('hello') } catch {}", &[]).await?; + assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::Read); + Ok(()) +} + +#[test(tokio::test)] +async fn stat_sync() -> anyhow::Result<()> { + let accesses = track_node_script("try { fs.statSync('hello') } catch {}", &[]).await?; + assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::Read); + Ok(()) +} + +#[test(tokio::test)] +async fn create_read_stream() -> anyhow::Result<()> { + let accesses = track_node_script( + "try { fs.createReadStream('hello').on('error', () => {}) } catch {}", + &[], + ) + .await?; + assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::Read); + Ok(()) +} + +#[test(tokio::test)] +async fn create_write_stream() -> anyhow::Result<()> { + let tmpdir = tempfile::tempdir()?; + let file_path = tmpdir.path().join("hello"); + let accesses = track_node_script( + "try { fs.createWriteStream(process.argv[1]).on('error', () => {}) } catch {}", + &[file_path.as_os_str()], + ) + .await?; + assert_contains(&accesses, file_path.as_path(), AccessMode::Write); + Ok(()) +} + +#[test(tokio::test)] +async fn write_sync() -> anyhow::Result<()> { + let tmpdir = tempfile::tempdir()?; + let file_path = tmpdir.path().join("hello"); + let accesses = track_node_script( + "try { fs.writeFileSync(process.argv[1], '') } catch {}", + &[file_path.as_os_str()], + ) + .await?; + assert_contains(&accesses, &file_path, AccessMode::Write); + Ok(()) +} + #[test(tokio::test)] async fn read_dir_sync() -> anyhow::Result<()> { - let accesses = track_node_script("try { fs.readdirSync('.') } catch {}").await?; + let accesses = track_node_script("try { fs.readdirSync('.') } catch {}", &[]).await?; assert_contains(&accesses, ¤t_dir().unwrap(), AccessMode::ReadDir); Ok(()) } @@ -39,9 +94,10 @@ async fn subprocess() -> anyhow::Result<()> { } else { r"'/bin/sh', ['-c', 'cat hello']" }; - let accesses = track_node_script(&format!( - "try {{ child_process.spawnSync({cmd}, {{ stdio: 'ignore' }}) }} catch {{}}" - )) + let accesses = track_node_script( + &format!("try {{ child_process.spawnSync({cmd}, {{ stdio: 'ignore' }}) }} catch {{}}"), + &[], + ) .await?; assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::Read); Ok(()) diff --git a/crates/fspy/tests/rust_std.rs b/crates/fspy/tests/rust_std.rs index b7cb0af2..7db2d7d2 100644 --- a/crates/fspy/tests/rust_std.rs +++ b/crates/fspy/tests/rust_std.rs @@ -3,7 +3,6 @@ mod test_utils; use std::{ env::current_dir, fs::{File, OpenOptions}, - path::Path, process::Stdio, }; @@ -13,7 +12,7 @@ use test_utils::assert_contains; #[test(tokio::test)] async fn open_read() -> anyhow::Result<()> { - let accesses = track_child!({ + let accesses = track_child!((), |(): ()| { let _ = File::open("hello"); }) .await?; @@ -24,39 +23,32 @@ async fn open_read() -> anyhow::Result<()> { #[test(tokio::test)] async fn open_write() -> anyhow::Result<()> { - let accesses = track_child!({ - let path = format!("{}/hello", env!("CARGO_TARGET_TMPDIR")); - let _ = OpenOptions::new().write(true).open(path); + let tmp_dir = tempfile::tempdir()?; + let tmp_path = tmp_dir.path().join("hello"); + let tmp_path_str = tmp_path.to_str().unwrap().to_owned(); + let accesses = track_child!(tmp_path_str, |tmp_path_str: String| { + let _ = OpenOptions::new().write(true).open(tmp_path_str); }) .await?; - assert_contains( - &accesses, - Path::new(env!("CARGO_TARGET_TMPDIR")).join("hello").as_path(), - AccessMode::Write, - ); + assert_contains(&accesses, tmp_path.as_path(), AccessMode::Write); Ok(()) } #[test(tokio::test)] async fn readdir() -> anyhow::Result<()> { - let accesses = track_child!({ - let path = format!("{}/hello", env!("CARGO_TARGET_TMPDIR")); - let _ = std::fs::read_dir(path); + let accesses = track_child!((), |(): ()| { + let _ = std::fs::read_dir("hello_dir"); }) .await?; - assert_contains( - &accesses, - Path::new(env!("CARGO_TARGET_TMPDIR")).join("hello").as_path(), - AccessMode::ReadDir, - ); + assert_contains(&accesses, current_dir()?.join("hello_dir").as_path(), AccessMode::ReadDir); Ok(()) } #[test(tokio::test)] async fn subprocess() -> anyhow::Result<()> { - let accesses = track_child!({ + let accesses = track_child!((), |(): ()| { let mut command = if cfg!(windows) { let mut command = std::process::Command::new("cmd"); command.arg("/c").arg("type hello"); diff --git a/crates/fspy/tests/rust_tokio.rs b/crates/fspy/tests/rust_tokio.rs index 30ab1a41..437d6473 100644 --- a/crates/fspy/tests/rust_tokio.rs +++ b/crates/fspy/tests/rust_tokio.rs @@ -1,6 +1,6 @@ mod test_utils; -use std::{env::current_dir, path::Path, process::Stdio}; +use std::{env::current_dir, process::Stdio}; use fspy::AccessMode; use test_log::test; @@ -9,7 +9,7 @@ use tokio::fs::OpenOptions; #[test(tokio::test)] async fn open_read() -> anyhow::Result<()> { - let accesses = track_child!({ + let accesses = track_child!((), |(): ()| { tokio::runtime::Builder::new_current_thread().enable_io().build().unwrap().block_on( async { let _ = tokio::fs::File::open("hello").await; @@ -24,49 +24,40 @@ async fn open_read() -> anyhow::Result<()> { #[test(tokio::test)] async fn open_write() -> anyhow::Result<()> { - let accesses = track_child!({ - let path = format!("{}/hello", env!("CARGO_TARGET_TMPDIR")); - + let tmp_dir = tempfile::tempdir()?; + let tmp_path = tmp_dir.path().join("hello"); + let tmp_path_str = tmp_path.to_str().unwrap().to_owned(); + let accesses = track_child!(tmp_path_str, |tmp_path_str: String| { tokio::runtime::Builder::new_current_thread().enable_io().build().unwrap().block_on( async { - let _ = OpenOptions::new().write(true).open(path).await; + let _ = OpenOptions::new().write(true).open(tmp_path_str).await; }, ); }) .await?; - assert_contains( - &accesses, - Path::new(env!("CARGO_TARGET_TMPDIR")).join("hello").as_path(), - AccessMode::Write, - ); + assert_contains(&accesses, tmp_path.as_path(), AccessMode::Write); Ok(()) } #[test(tokio::test)] async fn readdir() -> anyhow::Result<()> { - let accesses = track_child!({ - let path = format!("{}/hello", env!("CARGO_TARGET_TMPDIR")); - + let accesses = track_child!((), |(): ()| { tokio::runtime::Builder::new_current_thread().enable_io().build().unwrap().block_on( async { - let _ = tokio::fs::read_dir(path).await; + let _ = tokio::fs::read_dir("hello_dir").await; }, ); }) .await?; - assert_contains( - &accesses, - Path::new(env!("CARGO_TARGET_TMPDIR")).join("hello").as_path(), - AccessMode::ReadDir, - ); + assert_contains(&accesses, current_dir()?.join("hello_dir").as_path(), AccessMode::ReadDir); Ok(()) } #[test(tokio::test)] async fn subprocess() -> anyhow::Result<()> { - let accesses = track_child!({ + let accesses = track_child!((), |(): ()| { tokio::runtime::Builder::new_current_thread().enable_io().build().unwrap().block_on( async { let mut command = if cfg!(windows) { diff --git a/crates/fspy/tests/test_utils.rs b/crates/fspy/tests/test_utils.rs index bfaf9d57..3aaa0edb 100644 --- a/crates/fspy/tests/test_utils.rs +++ b/crates/fspy/tests/test_utils.rs @@ -34,10 +34,8 @@ pub fn assert_contains( #[macro_export] macro_rules! track_child { - ($body: block) => {{ - let std_cmd = $crate::test_utils::command_executing!((), |(): ()| { - let _ = $body; - }); + ($arg: expr, $body: expr) => {{ + let std_cmd = $crate::test_utils::command_executing!($arg, $body); $crate::test_utils::spawn_std(std_cmd) }}; } diff --git a/crates/fspy_preload_unix/src/interceptions/access.rs b/crates/fspy_preload_unix/src/interceptions/access.rs new file mode 100644 index 00000000..b581010b --- /dev/null +++ b/crates/fspy_preload_unix/src/interceptions/access.rs @@ -0,0 +1,28 @@ +use fspy_shared::ipc::AccessMode; +use libc::{c_char, c_int}; + +use crate::{ + client::{convert::PathAt, handle_open}, + macros::intercept, +}; + +intercept!(access(64): unsafe extern "C" fn(pathname: *const c_char, mode: c_int) -> c_int); +unsafe extern "C" fn access(pathname: *const c_char, mode: c_int) -> c_int { + unsafe { + handle_open(pathname, AccessMode::Read); + } + unsafe { access::original()(pathname, mode) } +} + +intercept!(faccessat(64): unsafe extern "C" fn(dirfd: c_int, pathname: *const c_char, mode: c_int, flags: c_int) -> c_int); +unsafe extern "C" fn faccessat( + dirfd: c_int, + pathname: *const c_char, + mode: c_int, + flags: c_int, +) -> c_int { + unsafe { + handle_open(PathAt(dirfd, pathname), AccessMode::Read); + } + unsafe { faccessat::original()(dirfd, pathname, mode, flags) } +} diff --git a/crates/fspy_preload_unix/src/interceptions/linux_syscall.rs b/crates/fspy_preload_unix/src/interceptions/linux_syscall.rs new file mode 100644 index 00000000..1948d7ec --- /dev/null +++ b/crates/fspy_preload_unix/src/interceptions/linux_syscall.rs @@ -0,0 +1,31 @@ +use fspy_shared::ipc::AccessMode; +use libc::{c_char, c_int, c_long}; + +use crate::{ + client::{convert::PathAt, handle_open}, + macros::intercept, +}; + +intercept!(syscall(64): unsafe extern "C" fn(c_long, args: ...) -> c_long); +unsafe extern "C" fn syscall(syscall_no: c_long, mut args: ...) -> c_long { + // https://github.com/bminor/glibc/blob/efc8642051e6c4fe5165e8986c1338ba2c180de6/sysdeps/unix/sysv/linux/syscall.c#L23 + let a0 = unsafe { args.arg::() }; + let a1 = unsafe { args.arg::() }; + let a2 = unsafe { args.arg::() }; + let a3 = unsafe { args.arg::() }; + let a4 = unsafe { args.arg::() }; + let a5 = unsafe { args.arg::() }; + + match syscall_no { + libc::SYS_statx => { + // c-style conversion is expected: (4294967196 -> -100 aka libc::AT_FDCWD) + let dirfd = a0 as c_int; + let pathname = a1 as *const c_char; + unsafe { + handle_open(PathAt(dirfd, pathname), AccessMode::Read); + } + } + _ => {} + } + unsafe { syscall::original()(syscall_no, a0, a1, a2, a3, a4, a5) } +} diff --git a/crates/fspy_preload_unix/src/interceptions/mod.rs b/crates/fspy_preload_unix/src/interceptions/mod.rs index 046d6f69..0d3742ea 100644 --- a/crates/fspy_preload_unix/src/interceptions/mod.rs +++ b/crates/fspy_preload_unix/src/interceptions/mod.rs @@ -1,4 +1,8 @@ +mod access; mod dirent; mod open; mod spawn; mod stat; + +#[cfg(target_os = "linux")] +mod linux_syscall; diff --git a/crates/fspy_preload_windows/src/windows/convert.rs b/crates/fspy_preload_windows/src/windows/convert.rs index 1cc21bdd..6787bc13 100644 --- a/crates/fspy_preload_windows/src/windows/convert.rs +++ b/crates/fspy_preload_windows/src/windows/convert.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use fspy_shared::ipc::AccessMode; use widestring::{U16CStr, U16CString, U16Str}; use winapi::{ @@ -9,7 +11,7 @@ use crate::windows::winapi_utils::{ access_mask_to_mode, combine_paths, get_path_name, get_u16_str, }; -pub trait ToAccessMode { +pub trait ToAccessMode: Debug { unsafe fn to_access_mode(self) -> AccessMode; } diff --git a/crates/fspy_preload_windows/src/windows/winapi_utils.rs b/crates/fspy_preload_windows/src/windows/winapi_utils.rs index 0c7c304e..3991563c 100644 --- a/crates/fspy_preload_windows/src/windows/winapi_utils.rs +++ b/crates/fspy_preload_windows/src/windows/winapi_utils.rs @@ -12,7 +12,10 @@ use winapi::{ }, um::{ fileapi::GetFinalPathNameByHandleW, - winnt::{ACCESS_MASK, GENERIC_READ, GENERIC_WRITE}, + winnt::{ + ACCESS_MASK, FILE_APPEND_DATA, FILE_READ_DATA, FILE_WRITE_DATA, GENERIC_READ, + GENERIC_WRITE, + }, }, }; use winsafe::{GetLastError, co}; @@ -72,8 +75,8 @@ pub unsafe fn get_path_name(handle: HANDLE) -> winsafe::SysResult AccessMode { - let has_write = (desired_access & GENERIC_WRITE) != 0; - let has_read = (desired_access & GENERIC_READ) != 0; + let has_write = (desired_access & (FILE_WRITE_DATA | FILE_APPEND_DATA | GENERIC_WRITE)) != 0; + let has_read = (desired_access & (FILE_READ_DATA | GENERIC_READ)) != 0; if has_write { if has_read { AccessMode::ReadWrite } else { AccessMode::Write } } else {