#![cfg(test)]
#![allow(non_snake_case)]
use std::ffi::OsStr;
use std::fs;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use super::backend::{
FsBackend, EACCES, EBADF, EEXIST, EINVAL, EISDIR, ENOENT, ENOTDIR, EPERM,
};
use super::posix::PosixFs;
use super::protocol::{
SetattrIn, FATTR_GID, FATTR_MODE, FATTR_SIZE, FATTR_UID, FUSE_ROOT_ID, S_IFDIR, S_IFLNK,
S_IFMT, S_IFREG,
};
fn tmpdir(name: &str) -> PathBuf {
let pid = unsafe { libc::getpid() };
let p = std::env::temp_dir().join(format!("posixfs-compliance-{pid}-{name}"));
let _ = fs::remove_dir_all(&p);
fs::create_dir_all(&p).unwrap();
p
}
fn make_setattr(valid: u32) -> SetattrIn {
SetattrIn {
valid,
padding: 0,
fh: 0,
size: 0,
lock_owner: 0,
atime: 0,
mtime: 0,
ctime: 0,
atimensec: 0,
mtimensec: 0,
ctimensec: 0,
mode: 0,
unused4: 0,
uid: 0,
gid: 0,
unused5: 0,
}
}
mod lookup_semantics {
use super::*;
#[test]
fn lookup_of_broken_symlink_returns_symlink_kind_not_target_kind() {
let dir = tmpdir("lookup-broken-kind");
std::os::unix::fs::symlink("not-here.txt", dir.join("dangling")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("dangling")).unwrap();
assert_eq!(
e.attr.mode & S_IFMT,
S_IFLNK,
"broken symlink must report S_IFLNK ({:#o}) in lookup attr.mode; got mode={:#o}. \
A stat()-via-libc that follows would error ENOENT — we must call lstat() instead.",
S_IFLNK,
e.attr.mode
);
}
#[test]
fn lookup_of_broken_symlink_attr_size_equals_target_path_length() {
let dir = tmpdir("lookup-broken-size");
let target = "some/relative/missing-target.txt";
std::os::unix::fs::symlink(target, dir.join("dangling")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("dangling")).unwrap();
assert_eq!(
e.attr.size,
target.len() as u64,
"symlink size MUST equal byte length of target-path (POSIX lstat); \
got {} expected {} (target={:?}). \
readlink(2) callers pre-size their buffer to lstat.size; a wrong \
value here truncates.",
e.attr.size,
target.len(),
target
);
}
#[test]
fn lookup_of_symlink_to_dir_returns_symlink_not_dir_attrs() {
let dir = tmpdir("lookup-sym-to-dir");
fs::create_dir_all(dir.join("real_dir")).unwrap();
std::os::unix::fs::symlink("real_dir", dir.join("alias")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("alias")).unwrap();
assert_eq!(
e.attr.mode & S_IFMT,
S_IFLNK,
"symlink-to-directory must report S_IFLNK ({:#o}), NOT S_IFDIR ({:#o}); \
got mode={:#o}. If we say S_IFDIR here, the guest will treat the dentry \
as a real dir and skip readlink/openat lookup-via-target which is wrong.",
S_IFLNK,
S_IFDIR,
e.attr.mode
);
}
#[test]
fn lookup_of_dotdot_rejected() {
let dir = tmpdir("lookup-dotdot");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.lookup(FUSE_ROOT_ID, OsStr::new("..")).unwrap_err();
assert_eq!(
err, EINVAL,
"lookup('..') must return EINVAL (-22); we never allow path traversal \
through `..` at the FUSE protocol boundary even though the host fs \
would happily resolve it."
);
}
#[test]
#[ignore = "BUG: name_safe() in posix.rs does not yet reject embedded newlines; \
see crates/supermachine/src/fuse/posix.rs:587 `name_safe`. \
Defensive — POSIX itself doesn't forbid `\\n`, but many tools \
break on it. Currently host fs creates the file with a newline \
in its name. Either implement the filter or close this ignore."]
fn lookup_of_name_with_embedded_newline_rejected() {
let dir = tmpdir("lookup-newline");
let fs = PosixFs::new(&dir).unwrap();
let bad = OsStr::from_bytes(b"foo\nbar");
let err = fs.lookup(FUSE_ROOT_ID, bad).unwrap_err();
assert_eq!(
err, EINVAL,
"name with embedded newline should be rejected — defensive policy \
matches POSIX 'path component must be portable filename' (per \
SUSv4 3.282); got err={err}"
);
}
#[test]
fn lookup_of_name_longer_than_NAME_MAX_rejected() {
let dir = tmpdir("lookup-toolong");
let fs = PosixFs::new(&dir).unwrap();
let long: Vec<u8> = vec![b'a'; 300];
let err = fs
.lookup(FUSE_ROOT_ID, OsStr::from_bytes(&long))
.unwrap_err();
assert_eq!(
err, -36,
"lookup(<300-char-name>) must return Linux ENAMETOOLONG=-36, \
NOT macOS ENAMETOOLONG=-63 (which the guest's libc decodes as \
EREMOTE on Linux). errno translator at posix.rs `host_to_linux_errno` \
must map 63→36. got err={err}"
);
}
}
mod stat_semantics {
use super::*;
#[test]
fn getattr_of_regular_file_returns_S_IFREG_with_correct_size() {
let dir = tmpdir("stat-reg");
let payload = b"hello world";
fs::write(dir.join("f"), payload).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let attr = fs.getattr(e.nodeid, None).unwrap();
assert_eq!(
attr.mode & S_IFMT,
S_IFREG,
"regular file must report S_IFREG ({:#o}); got mode={:#o}",
S_IFREG,
attr.mode
);
assert_eq!(
attr.size,
payload.len() as u64,
"size for newly-written {} byte file must be {}; got {}",
payload.len(),
payload.len(),
attr.size
);
}
#[test]
fn getattr_of_directory_returns_S_IFDIR_with_nlink_ge_2() {
let dir = tmpdir("stat-dir");
fs::create_dir_all(dir.join("sub1")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let attr = fs.getattr(FUSE_ROOT_ID, None).unwrap();
assert_eq!(
attr.mode & S_IFMT,
S_IFDIR,
"root must report S_IFDIR ({:#o}); got mode={:#o}",
S_IFDIR,
attr.mode
);
assert!(
attr.nlink >= 2,
"directory nlink must be >= 2 (counting `.` + parent's `..` slot); \
got nlink={}. Linux readdir + many shells assume this; nlink=1 \
is the 'broken' signal that bypasses optimizations.",
attr.nlink
);
}
#[test]
fn getattr_of_symlink_returns_S_IFLNK_with_size_eq_target_path_length() {
let dir = tmpdir("stat-sym");
fs::write(dir.join("real.bin"), vec![0u8; 4096]).unwrap();
std::os::unix::fs::symlink("real.bin", dir.join("link")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("link")).unwrap();
let attr = fs.getattr(e.nodeid, None).unwrap();
assert_eq!(
attr.mode & S_IFMT,
S_IFLNK,
"getattr on symlink must report S_IFLNK; got mode={:#o}. \
If this fires, posix.rs::getattr is calling std::fs::metadata \
(= stat, follows) instead of std::fs::symlink_metadata (= lstat). \
This was 0.7.4's bug.",
attr.mode
);
assert_eq!(
attr.size,
"real.bin".len() as u64,
"symlink size MUST equal strlen(target). got {}, expected {}. \
4096 would mean we returned the TARGET's size — readlink(2) \
callers pre-size by lstat.size and would truncate.",
attr.size,
"real.bin".len()
);
}
#[test]
fn getattr_of_recently_truncated_file_size_is_zero() {
let dir = tmpdir("stat-trunc");
fs::write(dir.join("f"), vec![0xAAu8; 100]).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let mut req = make_setattr(FATTR_SIZE);
req.size = 0;
let post = fs.setattr(e.nodeid, None, req).unwrap();
assert_eq!(
post.size, 0,
"setattr returned size {}; expected 0 (we just truncated)",
post.size
);
let attr = fs.getattr(e.nodeid, None).unwrap();
assert_eq!(
attr.size, 0,
"getattr after truncate(0) must report size=0; got {}. \
A non-zero here = stale cache OR truncate didn't reach disk.",
attr.size
);
}
}
mod write_semantics {
use super::*;
#[test]
#[ignore = "BUG: PosixFs::open() in posix.rs masks O_TRUNC out (see open() \
at posix.rs:715 — `flags as i32 & libc::O_ACCMODE`). The guest \
kernel issues a separate FUSE_SETATTR with FATTR_SIZE=0 instead, \
so end-to-end the file IS truncated. But the unit-level contract \
'OPEN with O_TRUNC zeros the file' is not honored at this layer. \
Decide: either fold the host's open(O_TRUNC) into the backend \
OR document the contract as 'guest kernel must send SETATTR first'."]
fn open_O_TRUNC_zeros_existing_file() {
let dir = tmpdir("write-otrunc");
fs::write(dir.join("f"), vec![0xAAu8; 100]).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs
.open(
e.nodeid,
(libc::O_WRONLY | libc::O_TRUNC) as u32,
)
.unwrap();
let attr = fs.getattr(e.nodeid, None).unwrap();
assert_eq!(
attr.size, 0,
"open(O_WRONLY|O_TRUNC) must zero an existing file; got size={}. \
POSIX open(2): 'If O_TRUNC is set and the file already exists, \
the file shall be truncated to zero length'. If the backend masks \
this out, the guest's libc-level open(O_TRUNC) breaks subtly: \
read-back returns stale bytes instead of EOF.",
attr.size
);
fs.release(e.nodeid, fh).unwrap();
}
#[test]
fn open_O_CREAT_O_EXCL_existing_returns_EEXIST() {
let dir = tmpdir("write-excl");
let fs = PosixFs::new(&dir).unwrap();
let (_e1, fh1) = fs
.create(
FUSE_ROOT_ID,
OsStr::new("lock"),
0o644,
libc::O_RDWR as u32,
)
.unwrap();
let err = fs
.create(
FUSE_ROOT_ID,
OsStr::new("lock"),
0o644,
libc::O_RDWR as u32,
)
.unwrap_err();
assert_eq!(
err, EEXIST,
"second create(O_EXCL) on existing name must return EEXIST (-17); \
got err={err}. POSIX open(2) O_CREAT|O_EXCL contract — any other \
value breaks lock-file algorithms (test-and-set on a file).",
);
let _ = fs.release(0, fh1);
}
#[test]
fn write_then_read_returns_same_bytes() {
let dir = tmpdir("write-rt");
let fs = PosixFs::new(&dir).unwrap();
let (e, fh) = fs
.create(
FUSE_ROOT_ID,
OsStr::new("rt"),
0o644,
libc::O_RDWR as u32,
)
.unwrap();
let n = fs.write(e.nodeid, fh, 0, b"round-trip").unwrap();
assert_eq!(n, 10);
let buf = fs.read(e.nodeid, fh, 0, 64).unwrap();
assert_eq!(
buf, b"round-trip",
"read after write returned {:?}; expected {:?}",
buf, b"round-trip"
);
fs.release(e.nodeid, fh).unwrap();
}
#[test]
fn write_at_offset_holes_read_as_zero() {
let dir = tmpdir("write-hole");
let fs = PosixFs::new(&dir).unwrap();
let (e, fh) = fs
.create(
FUSE_ROOT_ID,
OsStr::new("sparse"),
0o644,
libc::O_RDWR as u32,
)
.unwrap();
let off: u64 = 1 << 20;
fs.write(e.nodeid, fh, off, &[0xFF]).unwrap();
let early = fs.read(e.nodeid, fh, 0, 100).unwrap();
assert_eq!(
early.len(),
100,
"read across hole returned {} bytes; expected 100. \
Sparse files: read in a hole returns zeros without short-read.",
early.len()
);
assert!(
early.iter().all(|&b| b == 0),
"hole region must read as zero bytes; got first non-zero at \
offset {} (value {:#04x}). POSIX sparse semantics.",
early.iter().position(|&b| b != 0).unwrap_or(usize::MAX),
early.iter().find(|&&b| b != 0).copied().unwrap_or(0),
);
fs.release(e.nodeid, fh).unwrap();
}
#[test]
fn write_after_close_returns_EBADF() {
let dir = tmpdir("write-eof-fh");
let fs = PosixFs::new(&dir).unwrap();
let (e, fh) = fs
.create(
FUSE_ROOT_ID,
OsStr::new("f"),
0o644,
libc::O_RDWR as u32,
)
.unwrap();
fs.release(e.nodeid, fh).unwrap();
let err = fs.write(e.nodeid, fh, 0, b"x").unwrap_err();
assert_eq!(
err, EBADF,
"write to released fh must return EBADF (-9); got err={err}. \
POSIX close(2) invalidates the fd; subsequent write(2) gives EBADF."
);
}
}
mod rmdir_unlink_semantics {
use super::*;
#[test]
fn rmdir_nonempty_returns_ENOTEMPTY_eq_39() {
let dir = tmpdir("rmdir-ne");
fs::create_dir_all(dir.join("sub")).unwrap();
fs::write(dir.join("sub/inside"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.rmdir(FUSE_ROOT_ID, OsStr::new("sub")).unwrap_err();
assert_eq!(
err, -39,
"rmdir(non-empty) must return Linux ENOTEMPTY=-39 over the wire, \
NOT macOS-host -66 (which the guest decodes as EREMOTE). \
The 66→39 translation lives in posix.rs `host_to_linux_errno`; \
if this test fails, that map is broken. got err={err}"
);
}
#[test]
fn rmdir_of_regular_file_returns_ENOTDIR_eq_20() {
let dir = tmpdir("rmdir-notdir");
fs::write(dir.join("notadir"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.rmdir(FUSE_ROOT_ID, OsStr::new("notadir")).unwrap_err();
assert_eq!(
err, ENOTDIR,
"rmdir(regular_file) must return Linux ENOTDIR=-20; got err={err}. \
POSIX: 'If the path argument refers to a path whose final component \
is neither a symbolic link nor a directory, rmdir() shall fail.'"
);
}
#[test]
fn unlink_of_directory_returns_EISDIR_eq_21() {
let dir = tmpdir("unlink-dir");
fs::create_dir_all(dir.join("sub")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.unlink(FUSE_ROOT_ID, OsStr::new("sub")).unwrap_err();
assert!(
err == EISDIR || err == EPERM,
"unlink(directory) must return EISDIR (-21, Linux) or EPERM (-1, macOS); \
got err={err}. POSIX leaves both as valid for this case (unlink on dir \
'shall fail and may return EPERM or EISDIR'). The errno translator at \
posix.rs preserves whichever the host fs returns."
);
}
#[test]
fn rmdir_of_nonexistent_returns_ENOENT_eq_2() {
let dir = tmpdir("rmdir-nope");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.rmdir(FUSE_ROOT_ID, OsStr::new("ghost")).unwrap_err();
assert_eq!(
err, ENOENT,
"rmdir(nonexistent) must return ENOENT=-2; got err={err}. \
POSIX-identical on both macOS and Linux (errno 2)."
);
}
}
mod rename_semantics {
use super::*;
#[test]
fn rename_overwrites_existing_destination() {
let dir = tmpdir("rename-overwrite");
fs::write(dir.join("src"), b"new contents").unwrap();
fs::write(dir.join("dst"), b"old contents").unwrap();
let fs = PosixFs::new(&dir).unwrap();
fs.rename(
FUSE_ROOT_ID,
OsStr::new("src"),
FUSE_ROOT_ID,
OsStr::new("dst"),
0,
)
.unwrap();
assert!(
!dir.join("src").exists(),
"source must be gone after rename"
);
assert_eq!(
fs::read(dir.join("dst")).unwrap(),
b"new contents",
"destination must have the source's contents (atomic replace)"
);
}
#[test]
fn rename_directory_into_itself_returns_EINVAL() {
let dir = tmpdir("rename-self");
fs::create_dir_all(dir.join("a")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let a = fs.lookup(FUSE_ROOT_ID, OsStr::new("a")).unwrap();
let err = fs
.rename(
FUSE_ROOT_ID,
OsStr::new("a"),
a.nodeid,
OsStr::new("b"),
0,
)
.unwrap_err();
assert_eq!(
err, EINVAL,
"rename(dir, dir/sub) must return EINVAL=-22; got err={err}. \
POSIX rename(2): 'A loop in the renaming hierarchy is forbidden.' \
macOS errno=22, Linux errno=22 (POSIX-identical)."
);
}
#[test]
fn rename_across_dirs_works() {
let dir = tmpdir("rename-xdir");
fs::create_dir_all(dir.join("a")).unwrap();
fs::create_dir_all(dir.join("b")).unwrap();
fs::write(dir.join("a/f"), b"cross").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let a = fs.lookup(FUSE_ROOT_ID, OsStr::new("a")).unwrap();
let b = fs.lookup(FUSE_ROOT_ID, OsStr::new("b")).unwrap();
fs.rename(a.nodeid, OsStr::new("f"), b.nodeid, OsStr::new("f"), 0)
.unwrap();
assert!(!dir.join("a/f").exists());
assert_eq!(fs::read(dir.join("b/f")).unwrap(), b"cross");
}
#[test]
fn rename_preserves_nodeid_for_renamed_inode() {
let dir = tmpdir("rename-ino");
fs::write(dir.join("orig"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let before = fs.lookup(FUSE_ROOT_ID, OsStr::new("orig")).unwrap();
let nodeid_before = before.nodeid;
fs.rename(
FUSE_ROOT_ID,
OsStr::new("orig"),
FUSE_ROOT_ID,
OsStr::new("renamed"),
0,
)
.unwrap();
let after = fs.lookup(FUSE_ROOT_ID, OsStr::new("renamed")).unwrap();
assert_eq!(
after.nodeid, nodeid_before,
"nodeid must survive rename — was {}, now {}. Guest dentry cache \
keys on nodeid; a fresh allocation would silently invalidate every \
pre-rename fh the guest is holding.",
nodeid_before, after.nodeid
);
}
#[test]
fn rename_of_open_file_keeps_fh_valid() {
let dir = tmpdir("rename-fh");
fs::write(dir.join("src"), b"keep me reading").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("src")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDONLY as u32).unwrap();
fs.rename(
FUSE_ROOT_ID,
OsStr::new("src"),
FUSE_ROOT_ID,
OsStr::new("dst"),
0,
)
.unwrap();
let buf = fs.read(e.nodeid, fh, 0, 64).unwrap();
assert_eq!(
buf, b"keep me reading",
"fh held across rename must still read the original content; \
got {:?}. POSIX rename(2): an open fd refers to the open-file \
description, not the name — the data follows the inode.",
buf
);
fs.release(e.nodeid, fh).unwrap();
}
}
mod symlink_semantics {
use super::*;
#[test]
fn symlink_create_then_readlink_round_trip() {
let dir = tmpdir("sym-rt");
let fs = PosixFs::new(&dir).unwrap();
let e = fs
.symlink(FUSE_ROOT_ID, OsStr::new("link"), OsStr::new("payload"))
.unwrap();
let target = fs.readlink(e.nodeid).unwrap();
assert_eq!(
target, b"payload",
"readlink must return the exact bytes we passed to symlink; \
got {:?} expected b\"payload\"",
target
);
}
#[test]
fn symlink_target_can_contain_dotdot_and_absolute() {
let dir = tmpdir("sym-opaque-target");
let fs = PosixFs::new(&dir).unwrap();
let weird = "../../../absolutely/nowhere";
let e = fs
.symlink(FUSE_ROOT_ID, OsStr::new("weird"), OsStr::new(weird))
.unwrap();
let got = fs.readlink(e.nodeid).unwrap();
assert_eq!(
got,
weird.as_bytes(),
"POSIX: symlink target bytes are opaque — `..` and absolute paths \
must round-trip verbatim. got {:?} expected {:?}",
got,
weird.as_bytes()
);
}
#[test]
fn symlink_target_longer_than_PATH_MAX_rejected_with_ENAMETOOLONG() {
let dir = tmpdir("sym-toolong");
let fs = PosixFs::new(&dir).unwrap();
let huge: Vec<u8> = vec![b'a'; 5000];
let target_os = OsStr::from_bytes(&huge);
let err = fs
.symlink(FUSE_ROOT_ID, OsStr::new("toolong"), target_os)
.unwrap_err();
assert_eq!(
err, -36,
"symlink with 5000-byte target must return Linux ENAMETOOLONG=-36, \
NOT macOS-host -63 (which decodes as EREMOTE on Linux). errno \
translator at posix.rs `host_to_linux_errno` owns the 63→36 map. \
got err={err}"
);
}
#[test]
fn readlink_on_regular_file_returns_EINVAL_eq_22() {
let dir = tmpdir("readlink-reg");
fs::write(dir.join("notalink"), b"hi").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("notalink")).unwrap();
let err = fs.readlink(e.nodeid).unwrap_err();
assert_eq!(
err, EINVAL,
"readlink(regular_file) must return EINVAL=-22; got err={err}. \
POSIX readlink(2): 'The named file is not a symbolic link.'"
);
}
#[test]
fn lookup_through_internal_symlink_resolves() {
let dir = tmpdir("sym-internal");
fs::create_dir_all(dir.join("real")).unwrap();
fs::write(dir.join("real/data.txt"), b"internal").unwrap();
std::os::unix::fs::symlink("real/data.txt", dir.join("link")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("link")).unwrap();
assert_eq!(
e.attr.mode & S_IFMT,
S_IFLNK,
"internal-target symlink still reports S_IFLNK at lookup; \
the guest decides whether to deref via its own kernel."
);
let target = fs.readlink(e.nodeid).unwrap();
assert_eq!(
target, b"real/data.txt",
"readlink on internal symlink returns the stored target bytes."
);
}
}
mod errno_translation {
use super::*;
#[test]
fn host_to_linux_errno_ENOTEMPTY_maps_correctly() {
let dir = tmpdir("err-enotempty");
fs::create_dir_all(dir.join("sub")).unwrap();
fs::write(dir.join("sub/inside"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.rmdir(FUSE_ROOT_ID, OsStr::new("sub")).unwrap_err();
assert_eq!(
err, -39,
"ENOTEMPTY contract: macOS host errno 66 MUST translate to Linux 39 \
on the FUSE wire. The guest's libc decodes -66 as EREMOTE (the bug). \
got err={err}"
);
}
#[test]
fn host_to_linux_errno_ENAMETOOLONG_maps_correctly() {
let dir = tmpdir("err-enametoolong");
let fs = PosixFs::new(&dir).unwrap();
let huge: Vec<u8> = vec![b'a'; 5000];
let err = fs
.symlink(
FUSE_ROOT_ID,
OsStr::new("over"),
OsStr::from_bytes(&huge),
)
.unwrap_err();
assert_eq!(
err, -36,
"ENAMETOOLONG contract: macOS host errno 63 MUST translate to Linux 36; \
the guest decodes -63 as EREMOTE. got err={err}"
);
}
#[test]
fn host_to_linux_errno_EEXIST_passthrough() {
let dir = tmpdir("err-eexist");
fs::write(dir.join("present"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs
.create(
FUSE_ROOT_ID,
OsStr::new("present"),
0o644,
libc::O_RDWR as u32,
)
.unwrap_err();
assert_eq!(
err, EEXIST,
"EEXIST is errno=17 on both macOS and Linux — passthrough. \
got err={err}. If this fails, the translator broke a row that \
shouldn't have moved."
);
}
#[test]
fn host_to_linux_errno_ENOTDIR_passthrough() {
let dir = tmpdir("err-enotdir");
fs::write(dir.join("f"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.rmdir(FUSE_ROOT_ID, OsStr::new("f")).unwrap_err();
assert_eq!(
err, ENOTDIR,
"ENOTDIR is errno=20 on both macOS and Linux — passthrough. \
got err={err}"
);
}
#[test]
fn host_to_linux_errno_ENOENT_passthrough() {
let dir = tmpdir("err-enoent");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.rmdir(FUSE_ROOT_ID, OsStr::new("ghost")).unwrap_err();
assert_eq!(
err, ENOENT,
"ENOENT is errno=2 on both sides — passthrough. got err={err}"
);
}
#[test]
fn host_to_linux_errno_EINVAL_passthrough() {
let dir = tmpdir("err-einval");
fs::create_dir_all(dir.join("a")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let a = fs.lookup(FUSE_ROOT_ID, OsStr::new("a")).unwrap();
let err = fs
.rename(
FUSE_ROOT_ID,
OsStr::new("a"),
a.nodeid,
OsStr::new("b"),
0,
)
.unwrap_err();
assert_eq!(
err, EINVAL,
"EINVAL=22 is shared; rename(dir, dir/sub) is the canonical \
POSIX EINVAL trigger. got err={err}"
);
}
#[test]
fn host_to_linux_errno_EBADF_passthrough() {
let dir = tmpdir("err-ebadf");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.read(FUSE_ROOT_ID, 0xdead_beef, 0, 8).unwrap_err();
assert_eq!(
err, EBADF,
"EBADF=9 on both sides; backend allocates fh, unknown fh → EBADF. \
got err={err}"
);
}
#[test]
fn host_to_linux_errno_EACCES_passthrough() {
let dir = tmpdir("err-eacces");
let outside = tmpdir("err-eacces-out");
fs::write(outside.join("x"), b"secret").unwrap();
std::os::unix::fs::symlink(&outside, dir.join("escape")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.lookup(FUSE_ROOT_ID, OsStr::new("escape")).unwrap_err();
assert_eq!(
err, EACCES,
"EACCES=13 on both sides; external symlink under Opaque policy \
surfaces EACCES at LOOKUP. got err={err}"
);
}
}
mod permission_semantics {
use super::*;
#[test]
fn chmod_then_getattr_reflects_new_mode() {
let dir = tmpdir("perm-chmod");
let path = dir.join("f");
fs::write(&path, b"x").unwrap();
std::fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(0o644))
.unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let mut req = make_setattr(FATTR_MODE);
req.mode = 0o755;
let post = fs.setattr(e.nodeid, None, req).unwrap();
assert_eq!(
post.mode & 0o7777,
0o755,
"setattr(MODE=0o755) must return new mode in attr.mode (low 12 bits); \
got mode={:#o}",
post.mode & 0o7777
);
let observed = fs.getattr(e.nodeid, None).unwrap();
assert_eq!(
observed.mode & 0o7777,
0o755,
"subsequent getattr must observe the new mode; got mode={:#o}",
observed.mode & 0o7777
);
}
#[test]
#[ignore = "requires non-root runtime + setuid bit drop semantics; \
lift to an integration test that boots a guest as non-root \
and verifies S_ISUID drops on chmod via os.chmod"]
fn chmod_clears_setuid_when_dropping_perms_on_owner() {
}
#[test]
fn chown_changes_uid_gid_or_returns_eperm_as_nonroot() {
let dir = tmpdir("perm-chown");
let path = dir.join("f");
fs::write(&path, b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let me_uid = unsafe { libc::getuid() } as u32;
let me_gid = unsafe { libc::getgid() } as u32;
let mut req = make_setattr(FATTR_UID | FATTR_GID);
req.uid = me_uid;
req.gid = me_gid;
let post = fs.setattr(e.nodeid, None, req).unwrap();
assert_eq!(
post.uid, me_uid,
"setattr(UID=me) must reflect in attr.uid; got {} expected {}",
post.uid, me_uid
);
assert_eq!(
post.gid, me_gid,
"setattr(GID=me) must reflect in attr.gid; got {} expected {}",
post.gid, me_gid
);
let md = fs::metadata(&path).unwrap();
assert_eq!(md.uid(), me_uid);
assert_eq!(md.gid(), me_gid);
}
}
mod mkdir_semantics {
use super::*;
use super::super::backend::EEXIST;
#[test]
fn mkdir_existing_path_returns_EEXIST_eq_17() {
let dir = tmpdir("mkdir-exists");
fs::create_dir_all(dir.join("already")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.mkdir(FUSE_ROOT_ID, OsStr::new("already"), 0o755).unwrap_err();
assert_eq!(
err, EEXIST,
"mkdir(existing_dir) must return EEXIST=-17; got err={err}. \
POSIX: 'mkdir() shall fail if the named file exists.'"
);
}
#[test]
fn mkdir_over_existing_file_returns_EEXIST() {
let dir = tmpdir("mkdir-over-file");
fs::write(dir.join("a"), b"hello").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs.mkdir(FUSE_ROOT_ID, OsStr::new("a"), 0o755).unwrap_err();
assert_eq!(
err, EEXIST,
"mkdir(name_that_is_a_regular_file) must return EEXIST=-17; \
got err={err}. The existence check precedes the type check."
);
}
#[test]
fn mkdir_creates_directory_with_S_IFDIR_type() {
let dir = tmpdir("mkdir-mode");
let fs = PosixFs::new(&dir).unwrap();
let entry = fs.mkdir(FUSE_ROOT_ID, OsStr::new("new"), 0o755).unwrap();
assert_eq!(
entry.attr.mode & S_IFMT,
S_IFDIR,
"mkdir() return-Entry must report type=S_IFDIR; got mode={:#o}",
entry.attr.mode
);
let md = fs::metadata(dir.join("new")).unwrap();
assert!(md.is_dir(), "host-side metadata must agree: is_dir");
}
#[test]
fn mkdir_then_lookup_returns_same_nodeid() {
let dir = tmpdir("mkdir-stable-id");
let fs = PosixFs::new(&dir).unwrap();
let mk = fs.mkdir(FUSE_ROOT_ID, OsStr::new("d"), 0o755).unwrap();
let lk = fs.lookup(FUSE_ROOT_ID, OsStr::new("d")).unwrap();
assert_eq!(
mk.nodeid, lk.nodeid,
"mkdir's returned nodeid ({}) must equal subsequent lookup's ({})",
mk.nodeid, lk.nodeid
);
}
#[test]
fn nested_mkdir_allocates_distinct_nodeids() {
let dir = tmpdir("mkdir-nested");
let fs = PosixFs::new(&dir).unwrap();
let parent = fs.mkdir(FUSE_ROOT_ID, OsStr::new("p"), 0o755).unwrap();
let child = fs.mkdir(parent.nodeid, OsStr::new("c"), 0o755).unwrap();
assert_ne!(
parent.nodeid, child.nodeid,
"nested mkdir must allocate distinct nodeids; both got {}",
parent.nodeid
);
assert!(fs::metadata(dir.join("p/c")).unwrap().is_dir());
}
}
mod link_semantics {
use super::*;
use super::super::backend::{EEXIST, EPERM};
#[test]
fn link_creates_second_dentry_same_inode() {
let dir = tmpdir("link-basic");
fs::write(dir.join("orig"), b"shared bytes").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let orig = fs.lookup(FUSE_ROOT_ID, OsStr::new("orig")).unwrap();
let _new = fs
.link(orig.nodeid, FUSE_ROOT_ID, OsStr::new("alias"))
.unwrap();
let md_orig = fs::metadata(dir.join("orig")).unwrap();
let md_alias = fs::metadata(dir.join("alias")).unwrap();
assert_eq!(
md_orig.nlink(),
2,
"link() must bump nlink on the source inode to 2; got {}",
md_orig.nlink()
);
assert_eq!(
md_orig.ino(),
md_alias.ino(),
"hardlinked names must point to the SAME host inode \
(orig.ino={}, alias.ino={})",
md_orig.ino(),
md_alias.ino()
);
}
#[test]
fn link_to_existing_destination_returns_EEXIST() {
let dir = tmpdir("link-dst-exists");
fs::write(dir.join("a"), b"a").unwrap();
fs::write(dir.join("b"), b"b").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let a = fs.lookup(FUSE_ROOT_ID, OsStr::new("a")).unwrap();
let err = fs.link(a.nodeid, FUSE_ROOT_ID, OsStr::new("b")).unwrap_err();
assert_eq!(
err, EEXIST,
"link() onto an existing destination must return EEXIST=-17; \
got err={err}. (link does NOT overwrite — that's rename's job.)"
);
}
#[test]
fn link_of_directory_returns_EPERM_or_EISDIR() {
let dir = tmpdir("link-dir");
fs::create_dir_all(dir.join("d")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let d = fs.lookup(FUSE_ROOT_ID, OsStr::new("d")).unwrap();
let err = fs
.link(d.nodeid, FUSE_ROOT_ID, OsStr::new("d2"))
.unwrap_err();
assert!(
err == EPERM || err == EISDIR,
"link(directory, _) must refuse with EPERM=-1 or EISDIR=-21; \
got err={err}. POSIX permits both."
);
}
#[test]
fn link_of_symlink_hardlinks_symlink_not_target() {
let dir = tmpdir("link-of-symlink");
fs::write(dir.join("target.txt"), b"real").unwrap();
std::os::unix::fs::symlink("target.txt", dir.join("sym")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let sym = fs.lookup(FUSE_ROOT_ID, OsStr::new("sym")).unwrap();
let _ = fs
.link(sym.nodeid, FUSE_ROOT_ID, OsStr::new("sym-link"))
.unwrap();
let md = fs::symlink_metadata(dir.join("sym-link")).unwrap();
assert!(
md.file_type().is_symlink(),
"link() of a symlink must hardlink the SYMLINK inode, not \
its target; got file_type={:?}",
md.file_type()
);
}
}
mod open_flag_semantics {
use super::*;
#[test]
fn open_directory_for_write_returns_EISDIR() {
let dir = tmpdir("open-dir-write");
fs::create_dir_all(dir.join("d")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let d = fs.lookup(FUSE_ROOT_ID, OsStr::new("d")).unwrap();
let err = fs.open(d.nodeid, libc::O_RDWR as u32).unwrap_err();
assert_eq!(
err, EISDIR,
"open(directory, O_RDWR) must return EISDIR=-21; got err={err}. \
Our backend rejects on Kind::Dir before reaching the host open()."
);
}
#[test]
fn open_nonexistent_no_creat_returns_ENOENT() {
let dir = tmpdir("open-nope");
let fs = PosixFs::new(&dir).unwrap();
let err = fs
.lookup(FUSE_ROOT_ID, OsStr::new("ghost"))
.unwrap_err();
assert_eq!(
err, ENOENT,
"lookup(nonexistent) must return ENOENT=-2; got err={err}. \
(open() at the FUSE layer pre-resolves via LOOKUP so this is \
the path the kernel actually walks.)"
);
}
#[test]
fn create_O_EXCL_on_existing_returns_EEXIST() {
let dir = tmpdir("create-exists");
fs::write(dir.join("x"), b"hi").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let err = fs
.create(
FUSE_ROOT_ID,
OsStr::new("x"),
0o644,
(libc::O_RDWR | libc::O_CREAT | libc::O_EXCL) as u32,
)
.unwrap_err();
assert_eq!(
err, super::super::backend::EEXIST,
"create(O_EXCL) on existing path must return EEXIST=-17; got err={err}. \
POSIX: 'O_CREAT|O_EXCL ... shall fail if the file exists.'"
);
}
#[test]
fn create_under_nonexistent_parent_returns_ENOENT() {
let dir = tmpdir("create-no-parent");
let fs = PosixFs::new(&dir).unwrap();
let err = fs
.create(
u64::MAX,
OsStr::new("x"),
0o644,
(libc::O_RDWR | libc::O_CREAT) as u32,
)
.unwrap_err();
assert_eq!(
err, ENOENT,
"create() under unknown parent_nodeid must return ENOENT=-2; got err={err}"
);
}
}
mod truncate_semantics {
use super::*;
#[test]
fn truncate_extend_zero_fills() {
let dir = tmpdir("trunc-extend");
fs::write(dir.join("f"), b"hello").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let mut req = make_setattr(FATTR_SIZE);
req.size = 1024;
let post = fs.setattr(e.nodeid, None, req).unwrap();
assert_eq!(post.size, 1024, "setattr-extend must update reported size");
let bytes = fs::read(dir.join("f")).unwrap();
assert_eq!(bytes.len(), 1024);
assert_eq!(&bytes[0..5], b"hello");
assert!(
bytes[5..].iter().all(|&b| b == 0),
"POSIX: extend must zero-fill the gap; found non-zero bytes"
);
}
#[test]
fn truncate_to_zero_empties_file() {
let dir = tmpdir("trunc-zero");
fs::write(dir.join("f"), b"some content here").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let mut req = make_setattr(FATTR_SIZE);
req.size = 0;
fs.setattr(e.nodeid, None, req).unwrap();
assert_eq!(fs::metadata(dir.join("f")).unwrap().len(), 0);
}
#[test]
fn truncate_shrink_discards_tail() {
let dir = tmpdir("trunc-shrink");
fs::write(dir.join("f"), b"abcdefghij").unwrap(); let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let mut req = make_setattr(FATTR_SIZE);
req.size = 4;
fs.setattr(e.nodeid, None, req).unwrap();
assert_eq!(fs::read(dir.join("f")).unwrap(), b"abcd");
}
}
mod readlink_semantics {
use super::*;
#[test]
fn readlink_of_regular_file_returns_EINVAL() {
let dir = tmpdir("readlink-notlink");
fs::write(dir.join("f"), b"hello").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let err = fs.readlink(e.nodeid).unwrap_err();
assert_eq!(
err, EINVAL,
"readlink(regular_file) must return EINVAL=-22; got err={err}. \
POSIX explicitly requires this for non-symlinks."
);
}
#[test]
fn readlink_returns_raw_target_bytes_no_canonicalization() {
let dir = tmpdir("readlink-raw");
std::os::unix::fs::symlink("../weird/./path/", dir.join("s")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("s")).unwrap();
let bytes = fs.readlink(e.nodeid).unwrap();
assert_eq!(
bytes, b"../weird/./path/",
"readlink must return EXACT stored bytes; got {:?}",
String::from_utf8_lossy(&bytes)
);
}
#[test]
fn readlink_preserves_trailing_slash_in_target() {
let dir = tmpdir("readlink-slash");
std::os::unix::fs::symlink("dir/", dir.join("s")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("s")).unwrap();
let bytes = fs.readlink(e.nodeid).unwrap();
assert_eq!(bytes, b"dir/");
}
}
mod read_write_edge_cases {
use super::*;
use super::super::backend::FsBackend;
#[test]
fn read_past_EOF_returns_zero_bytes() {
let dir = tmpdir("read-past-eof");
fs::write(dir.join("f"), b"hi").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDONLY as u32).unwrap();
let bytes = fs.read(e.nodeid, fh, 100, 16).unwrap();
assert_eq!(
bytes.len(),
0,
"read(offset=100, len=16) on a 2-byte file must return 0 bytes; \
got {} bytes",
bytes.len()
);
let _ = fs.release(e.nodeid, fh);
}
#[test]
fn read_offset_0_returns_file_bytes_verbatim() {
let dir = tmpdir("read-0");
let payload: &[u8] = &[0x01, 0xFF, 0x42, 0x00, 0x7E];
fs::write(dir.join("f"), payload).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDONLY as u32).unwrap();
let bytes = fs.read(e.nodeid, fh, 0, 64).unwrap();
assert_eq!(bytes, payload);
let _ = fs.release(e.nodeid, fh);
}
#[test]
fn read_size_larger_than_file_returns_actual_bytes() {
let dir = tmpdir("read-large");
fs::write(dir.join("f"), b"abc").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDONLY as u32).unwrap();
let bytes = fs.read(e.nodeid, fh, 0, 1024).unwrap();
assert_eq!(
bytes.len(),
3,
"read(size=1024) on a 3-byte file must return exactly 3 bytes; \
got {}; this asserts no padding/over-read.",
bytes.len()
);
assert_eq!(bytes, b"abc");
let _ = fs.release(e.nodeid, fh);
}
#[test]
fn release_of_unknown_fh_returns_EBADF() {
let dir = tmpdir("release-bad");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.release(FUSE_ROOT_ID, 9999).unwrap_err();
assert_eq!(
err, EBADF,
"release(unknown_fh) must return EBADF=-9; got err={err}"
);
}
#[test]
fn write_size_zero_is_no_op() {
let dir = tmpdir("write-zero");
fs::write(dir.join("f"), b"orig").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDWR as u32).unwrap();
let n = fs.write(e.nodeid, fh, 0, b"").unwrap();
assert_eq!(n, 0, "write(size=0) must return 0; got {n}");
assert_eq!(
fs::read(dir.join("f")).unwrap(),
b"orig",
"write(size=0) at offset 0 must not modify the file"
);
let _ = fs.release(e.nodeid, fh);
}
}
mod errno_translation_additional {
#[test]
fn ENOTSUP_maps_45_to_95() {
#[cfg(target_os = "macos")]
{
assert_eq!(libc::ENOTSUP, 45, "macOS sys/errno.h baseline");
assert_eq!(libc::EOPNOTSUPP, 102, "macOS BSD form");
}
}
#[test]
fn EUSERS_and_multihop_constants_match_audit_baseline() {
#[cfg(target_os = "macos")]
{
assert_eq!(libc::EUSERS, 68);
assert_eq!(libc::EREMOTE, 71);
assert_eq!(libc::EMULTIHOP, 95);
assert_eq!(libc::ENOLINK, 97);
assert_eq!(libc::ENOSR, 98);
assert_eq!(libc::ENOSTR, 99);
assert_eq!(libc::EPROTO, 100);
assert_eq!(libc::ETIME, 101);
}
}
}
mod chmod_symlink_semantics {
use super::*;
#[test]
fn chmod_on_symlink_changes_target_mode_not_symlink_mode() {
let dir = tmpdir("chmod-sym");
fs::write(dir.join("target"), b"the real file").unwrap();
fs::set_permissions(
dir.join("target"),
std::os::unix::fs::PermissionsExt::from_mode(0o644),
)
.unwrap();
std::os::unix::fs::symlink("target", dir.join("sym")).unwrap();
let fs = PosixFs::new(&dir).unwrap();
let sym = fs.lookup(FUSE_ROOT_ID, OsStr::new("sym")).unwrap();
assert_eq!(
sym.attr.mode & S_IFMT,
S_IFLNK,
"lookup of symlink must report S_IFLNK, not target's S_IFREG"
);
let mut req = make_setattr(FATTR_MODE);
req.mode = 0o600;
let _ = fs.setattr(sym.nodeid, None, req).unwrap();
let target_md = fs::metadata(dir.join("target")).unwrap();
let target_mode = target_md.mode() & 0o7777;
assert_eq!(
target_mode, 0o600,
"chmod(symlink) on a follow-symlink platform must change \
the TARGET's mode to 0o600; got 0o{target_mode:o}. \
POSIX-conformant: 'If the named file is a symbolic link, \
chmod() shall set the file mode of the file referenced \
by the symbolic link.'"
);
let sym_md = fs::symlink_metadata(dir.join("sym")).unwrap();
let sym_mode = sym_md.mode() & 0o7777;
assert_ne!(
sym_mode, 0o600,
"chmod(symlink) must NOT lchmod the symlink itself; \
symlink-mode is now 0o{sym_mode:o}. If this regresses to 0o600 \
then setattr is accidentally calling lchmod/fchmodat with \
AT_SYMLINK_NOFOLLOW."
);
}
#[test]
fn chmod_on_regular_file_changes_its_mode() {
let dir = tmpdir("chmod-regular");
fs::write(dir.join("f"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let mut req = make_setattr(FATTR_MODE);
req.mode = 0o640;
fs.setattr(e.nodeid, None, req).unwrap();
let md = fs::metadata(dir.join("f")).unwrap();
assert_eq!(md.mode() & 0o7777, 0o640);
}
}
mod fsync_durability_semantics {
use super::*;
#[test]
fn fsync_after_write_succeeds() {
let dir = tmpdir("fsync-happy");
fs::write(dir.join("f"), b"seed").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDWR as u32).unwrap();
let _ = fs.write(e.nodeid, fh, 0, b"updated").unwrap();
fs.fsync(e.nodeid, fh, false).expect("fsync(datasync=false) must succeed");
fs.fsync(e.nodeid, fh, true).expect("fsync(datasync=true) must succeed");
let _ = fs.release(e.nodeid, fh);
}
#[test]
fn fsync_of_unknown_fh_returns_EBADF() {
let dir = tmpdir("fsync-bad-fh");
let fs = PosixFs::new(&dir).unwrap();
let err = fs.fsync(FUSE_ROOT_ID, 9999, false).unwrap_err();
assert_eq!(
err, EBADF,
"fsync(unknown_fh) must return EBADF=-9; got err={err}"
);
}
#[test]
fn fsync_weak_env_var_takes_plain_fsync_path() {
let dir = tmpdir("fsync-weak");
fs::write(dir.join("f"), b"x").unwrap();
let fs = PosixFs::new(&dir).unwrap();
let e = fs.lookup(FUSE_ROOT_ID, OsStr::new("f")).unwrap();
let fh = fs.open(e.nodeid, libc::O_RDWR as u32).unwrap();
std::env::set_var("SUPERMACHINE_FSYNC_WEAK", "1");
let res = fs.fsync(e.nodeid, fh, false);
std::env::remove_var("SUPERMACHINE_FSYNC_WEAK");
res.expect("weak-fsync path must succeed for a normal fh");
let _ = fs.release(e.nodeid, fh);
}
}