Files
sausage/vfs/tests/guests/files/src/main.rs
T
surma 151550242b sausage-vfs: review fixes (fallible growth, symlink flags, stream cancel, sized reads, timestamps, error kinds, linker shadowing)
API: Layer::stat(path, follow), Layer::readlink, Open::follow,
Stat::symlink and Entry::symlink. Process::idle documents the shutdown
order: abort, await the task, await idle, release locks.
2026-10-23 00:49:46 +01:00

381 lines
12 KiB
Rust

//! Runs independently selected WASI filesystem test topics.
use std::{
fs::{self, OpenOptions},
io::ErrorKind,
io::{Read, Seek, SeekFrom, Write},
};
/// Select one topic so host failures identify the failing contract.
fn main() {
match std::env::var("VFS_TOPIC").unwrap().as_str() {
"basic" => basic(),
"review_streams" => review_streams(),
"review_times" => review_times(),
"review_paths" => review_paths(),
"review_links" => review_links(),
"review_preopen" => review_preopen(),
"trees" => trees(),
"options" => options(),
"rename" => rename(),
"sparse" => sparse(),
"streams" => streams(),
"mounts" => mounts(),
"shared_write" => fs::write("/run/shared", b"shared between guests").unwrap(),
"shared_read" => assert_eq!(fs::read("/run/shared").unwrap(), b"shared between guests"),
"sqlite" => sqlite(),
"tempfile" => temporary(),
topic => panic!("unknown topic: {topic}"),
}
}
/// Basic operations preserve file contents, sizes, and relative path resolution.
fn basic() {
fs::create_dir("/work").unwrap();
fs::write("/work/a", b"hello").unwrap();
let mut f = OpenOptions::new()
.read(true)
.write(true)
.open("/work/a")
.unwrap();
f.seek(SeekFrom::Start(1)).unwrap();
f.write_all(b"A").unwrap();
f.set_len(4).unwrap();
f.sync_all().unwrap();
assert_eq!(fs::read("work/../work/a").unwrap(), b"hAll");
assert_eq!(fs::metadata("/work/a").unwrap().len(), 4);
fs::remove_file("/work/a").unwrap();
fs::remove_dir("/work").unwrap();
}
/// Recursive folder operations and directory entries expose the complete tree.
fn trees() {
fs::create_dir_all("/tree/a/b").unwrap();
fs::create_dir_all("/tree/a/b").unwrap();
fs::write("/tree/a/file", b"contents").unwrap();
let entries = fs::read_dir("/tree/a")
.unwrap()
.map(Result::unwrap)
.collect::<Vec<_>>();
assert_eq!(entries.len(), 2);
for entry in entries {
let metadata = entry.metadata().unwrap();
let directory = entry.file_name() == "b";
assert_eq!(entry.file_type().unwrap().is_dir(), directory);
assert_eq!(metadata.is_dir(), directory);
assert_eq!(metadata.is_file(), !directory);
if !directory {
assert_eq!(metadata.len(), 8);
}
}
fs::remove_dir_all("/tree").unwrap();
assert_eq!(
fs::metadata("/tree").unwrap_err().kind(),
ErrorKind::NotFound
);
}
/// Open flags and filesystem errors match Rust's filesystem contract.
fn options() {
OpenOptions::new()
.write(true)
.create_new(true)
.open("/file")
.unwrap();
assert_eq!(
OpenOptions::new()
.write(true)
.create_new(true)
.open("/file")
.unwrap_err()
.kind(),
ErrorKind::AlreadyExists
);
fs::write("/file", b"old").unwrap();
OpenOptions::new()
.write(true)
.truncate(true)
.open("/file")
.unwrap();
assert!(fs::read("/file").unwrap().is_empty());
let mut append = OpenOptions::new().append(true).open("/file").unwrap();
append.write_all(b"one").unwrap();
append.seek(SeekFrom::Start(0)).unwrap();
append.write_all(b"two").unwrap();
assert_eq!(fs::read("/file").unwrap(), b"onetwo");
assert_eq!(
fs::read("/missing").unwrap_err().kind(),
ErrorKind::NotFound
);
assert_eq!(
fs::write("/missing/child", b"x").unwrap_err().kind(),
ErrorKind::NotFound
);
assert_eq!(
fs::read("/file/child").unwrap_err().kind(),
ErrorKind::NotADirectory
);
fs::create_dir("/folder").unwrap();
fs::write("/folder/child", b"x").unwrap();
assert_eq!(
fs::remove_dir("/folder").unwrap_err().kind(),
ErrorKind::DirectoryNotEmpty
);
}
/// Renames replace files and preserve the contents of moved folders.
fn rename() {
fs::write("/source", b"new").unwrap();
fs::write("/target", b"old").unwrap();
fs::rename("/source", "/target").unwrap();
assert_eq!(fs::read("/target").unwrap(), b"new");
fs::create_dir_all("/folder/nested").unwrap();
fs::write("/folder/nested/file", b"nested").unwrap();
fs::rename("/folder", "/moved").unwrap();
assert_eq!(fs::read("/moved/nested/file").unwrap(), b"nested");
assert_eq!(
fs::rename("/target", "/absent/target").unwrap_err().kind(),
ErrorKind::NotFound
);
assert_eq!(fs::read("/target").unwrap(), b"new");
}
/// A write after a seek beyond EOF fills the gap with zeros.
fn sparse() {
let mut f = OpenOptions::new()
.write(true)
.create_new(true)
.open("/sparse")
.unwrap();
f.write_all(b"start").unwrap();
f.seek(SeekFrom::Start(8192)).unwrap();
f.write_all(b"end").unwrap();
let bytes = fs::read("/sparse").unwrap();
assert_eq!(&bytes[..5], b"start");
assert!(bytes[5..8192].iter().all(|b| *b == 0));
assert_eq!(&bytes[8192..], b"end");
}
/// One write_all and one read_to_end transfer several MiB through WASI streams.
fn streams() {
use wasip2::filesystem::{
preopens,
types::{DescriptorFlags, OpenFlags, PathFlags},
};
let root = preopens::get_directories().remove(0).0;
let file = root
.open_at(
PathFlags::empty(),
"large",
OpenFlags::CREATE,
DescriptorFlags::READ | DescriptorFlags::WRITE,
)
.unwrap();
let bytes = (0..3 * 1024 * 1024 + 137)
.map(|i| (i % 251) as u8)
.collect::<Vec<_>>();
let mut output = file.write_via_stream(0).unwrap();
Write::write_all(&mut output, &bytes).unwrap();
Write::flush(&mut output).unwrap();
drop(output);
let mut input = file.read_via_stream(0).unwrap();
let mut received = Vec::new();
Read::read_to_end(&mut input, &mut received).unwrap();
let checksum = |data: &[u8]| {
data.iter()
.fold(0u64, |sum, b| sum.wrapping_mul(31).wrapping_add(*b as u64))
};
assert_eq!(received.len(), bytes.len());
assert_eq!(checksum(&received), checksum(&bytes));
assert_eq!(received, bytes);
}
/// Mount roots remain folders and read-only mounts reject mutations.
fn mounts() {
assert_eq!(
fs::write("/", b"x").unwrap_err().kind(),
ErrorKind::IsADirectory
);
assert_eq!(
fs::write("/proc", b"x").unwrap_err().kind(),
ErrorKind::ReadOnlyFilesystem
);
assert_eq!(
fs::write("/run", b"x").unwrap_err().kind(),
ErrorKind::IsADirectory
);
assert_eq!(
fs::write("/proc/no", b"x").unwrap_err().kind(),
ErrorKind::ReadOnlyFilesystem
);
let root = fs::read_dir("/")
.unwrap()
.map(|e| e.unwrap().file_name())
.collect::<Vec<_>>();
for name in ["proc", "run", "self"] {
assert!(root.iter().any(|n| n == name));
}
assert_eq!(fs::read("/proc/value").unwrap(), b"snapshot");
fs::write("/file", b"root write").unwrap();
assert_eq!(fs::read("/proc/../file").unwrap(), b"root write");
assert_eq!(
fs::rename("/file", "/run/file").unwrap_err().kind(),
ErrorKind::CrossesDevices
);
fs::write("/run/file", b"memory").unwrap();
assert_eq!(fs::read("/run/file").unwrap(), b"memory");
}
/// SQLite commits many rows in one transaction and reads them after reopen.
fn sqlite() {
let mut db = rusqlite::Connection::open("/database.sqlite").unwrap();
db.execute_batch("CREATE TABLE data (value INTEGER)")
.unwrap();
let tx = db.transaction().unwrap();
for i in 0..1000 {
tx.execute("INSERT INTO data VALUES (?1)", [i]).unwrap();
}
tx.commit().unwrap();
drop(db);
let db = rusqlite::Connection::open("/database.sqlite").unwrap();
let result: (i64, i64) = db
.query_row("SELECT count(*), sum(value) FROM data", [], |r| {
Ok((r.get(0)?, r.get(1)?))
})
.unwrap();
assert_eq!(result, (1000, 999 * 1000 / 2));
}
/// Named temporary files persist their bytes at the selected destination.
fn temporary() {
let mut unnamed = tempfile::tempfile_in("/tmp").unwrap();
unnamed.write_all(b"unnamed").unwrap();
unnamed.seek(SeekFrom::Start(0)).unwrap();
let mut bytes = Vec::new();
unnamed.read_to_end(&mut bytes).unwrap();
assert_eq!(bytes, b"unnamed");
let mut named = tempfile::NamedTempFile::new_in("/tmp").unwrap();
let original = named.path().to_owned();
named.write_all(b"persisted").unwrap();
named.persist("/persisted").unwrap();
assert!(!original.exists());
assert_eq!(fs::read("/persisted").unwrap(), b"persisted");
}
/// Dropped output streams finish writes and small reads do not cache later bytes.
fn review_streams() {
use wasip2::filesystem::{
preopens,
types::{DescriptorFlags, OpenFlags, PathFlags},
};
let root = preopens::get_directories().remove(0).0;
let file = root
.open_at(
PathFlags::empty(),
"stream-review",
OpenFlags::CREATE,
DescriptorFlags::READ | DescriptorFlags::WRITE,
)
.unwrap();
let output = file.write_via_stream(0).unwrap();
assert!(output.check_write().unwrap() > 0);
output.write(b"abcd").unwrap();
drop(output);
assert_eq!(fs::read("/stream-review").unwrap(), b"abcd");
let input = file.read_via_stream(0).unwrap();
assert_eq!(input.blocking_read(1).unwrap(), b"a");
file.write_via_stream(1)
.unwrap()
.blocking_write_and_flush(b"Z")
.unwrap();
assert_eq!(input.blocking_read(1).unwrap(), b"Z");
}
/// Timestamp operations accept directories and return overflow rather than panicking.
fn review_times() {
use wasip2::clocks::wall_clock::Datetime;
use wasip2::filesystem::{
preopens,
types::{ErrorCode, NewTimestamp, PathFlags},
};
let root = preopens::get_directories().remove(0).0;
fs::create_dir("/times").unwrap();
root.set_times_at(
PathFlags::SYMLINK_FOLLOW,
"times",
NewTimestamp::NoChange,
NewTimestamp::Timestamp(Datetime {
seconds: 123,
nanoseconds: 0,
}),
)
.unwrap();
assert_eq!(
fs::metadata("/times").unwrap().modified().unwrap(),
std::time::UNIX_EPOCH + std::time::Duration::from_secs(123)
);
assert_eq!(
root.set_times(
NewTimestamp::Timestamp(Datetime {
seconds: u64::MAX,
nanoseconds: u32::MAX
}),
NewTimestamp::NoChange
),
Err(ErrorCode::Overflow)
);
}
/// Trailing slashes require folders and readlink rejects ordinary files with invalid.
fn review_paths() {
use wasip2::filesystem::{preopens, types::ErrorCode};
fs::write("/file", b"x").unwrap();
assert_eq!(
fs::read("/file/").unwrap_err().kind(),
ErrorKind::NotADirectory
);
let root = preopens::get_directories().remove(0).0;
assert_eq!(root.readlink_at("file"), Err(ErrorCode::Invalid));
assert_eq!(root.remove_directory_at("run"), Err(ErrorCode::Busy));
}
/// Removing an internal link leaves its target intact and external links cannot escape.
fn review_links() {
use wasip2::filesystem::{
preopens,
types::{DescriptorFlags, DescriptorType, OpenFlags, PathFlags},
};
let root = preopens::get_directories().remove(0).0;
assert_eq!(
root.stat_at(PathFlags::empty(), "link").unwrap().type_,
DescriptorType::SymbolicLink
);
assert_eq!(root.readlink_at("link").unwrap(), "target");
assert!(
root.open_at(
PathFlags::empty(),
"link",
OpenFlags::DIRECTORY,
DescriptorFlags::READ
)
.is_err()
);
assert_eq!(
root.open_at(
PathFlags::SYMLINK_FOLLOW,
"link",
OpenFlags::DIRECTORY,
DescriptorFlags::READ
)
.unwrap()
.get_type()
.unwrap(),
DescriptorType::Directory
);
fs::remove_dir_all("/link").unwrap();
assert_eq!(fs::read("/target/keep").unwrap(), b"keep");
assert!(fs::read("/outside-link/secret").is_err());
}
/// The component sees exactly one virtual preopen rather than native WASI preopens.
fn review_preopen() {
let dirs = wasip2::filesystem::preopens::get_directories();
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].1, "/");
}