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.
381 lines
12 KiB
Rust
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, "/");
|
|
}
|