Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions crates/test-programs/src/bin/p1_stat_extreme_host_mtime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#![expect(unsafe_op_in_unsafe_fn, reason = "old code, not worth updating yet")]

use std::{env, process};
use test_programs::preview1::open_scratch_directory;

const FILENAME: &str = "extreme.dat";
const EXPECTED: &[u8] = b"hello";

unsafe fn test_stat_extreme_host_mtime(dir_fd: wasip1::Fd) {
let st = wasip1::path_filestat_get(dir_fd, 0, FILENAME).expect("path_filestat_get");
assert_eq!(st.size, EXPECTED.len() as u64, "size");
let _ = st.mtim;
let _ = st.atim;

let fd =
wasip1::path_open(dir_fd, 0, FILENAME, 0, wasip1::RIGHTS_FD_READ, 0, 0).expect("path_open");
let mut buf = [0u8; 16];
let nread = wasip1::fd_read(
fd,
&[wasip1::Iovec {
buf: buf.as_mut_ptr(),
buf_len: buf.len(),
}],
)
.expect("fd_read");
assert_eq!(&buf[..nread], EXPECTED, "contents");
wasip1::fd_close(fd).expect("fd_close");
}

fn main() {
let mut args = env::args();
let prog = args.next().unwrap();
let arg = if let Some(arg) = args.next() {
arg
} else {
eprintln!("usage: {prog} <scratch directory>");
process::exit(1);
};

let dir_fd = match open_scratch_directory(&arg) {
Ok(dir_fd) => dir_fd,
Err(err) => {
eprintln!("{err}");
process::exit(1);
}
};

unsafe { test_stat_extreme_host_mtime(dir_fd) }
}
20 changes: 12 additions & 8 deletions crates/wasi/src/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,18 +310,22 @@ impl DescriptorStat {
/// Creates a `DescriptorStat` from a `Metadata` plus the hard link
/// count.
fn new(meta: &Metadata, link_count: u64) -> Self {
fn datetime_from(t: std::time::SystemTime) -> Datetime {
// FIXME make this infallible or handle errors properly
Datetime::try_from(t).unwrap()
}

Self {
type_: meta.file_type().into(),
link_count,
size: meta.len(),
data_access_timestamp: meta.accessed().map(|t| datetime_from(t.into_std())).ok(),
data_modification_timestamp: meta.modified().map(|t| datetime_from(t.into_std())).ok(),
status_change_timestamp: meta.created().map(|t| datetime_from(t.into_std())).ok(),
data_access_timestamp: meta
.accessed()
.ok()
.and_then(|t| Datetime::try_from(t.into_std()).ok()),
data_modification_timestamp: meta
.modified()
.ok()
.and_then(|t| Datetime::try_from(t.into_std()).ok()),
status_change_timestamp: meta
.created()
.ok()
.and_then(|t| Datetime::try_from(t.into_std()).ok()),
}
}
}
Expand Down
10 changes: 6 additions & 4 deletions crates/wasi/src/p2/host/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,15 +590,17 @@ impl TryFrom<crate::filesystem::DescriptorStat> for types::DescriptorStat {
status_change_timestamp,
}: crate::filesystem::DescriptorStat,
) -> Result<Self, ErrorCode> {
// Internal timestamps use i64 seconds; wasi:clocks/wall-clock uses u64
// (non-negative). Times outside that range become missing rather than
// failing the whole stat (e.g. host-clamped far-past mtimes on macOS).
Ok(Self {
type_: type_.into(),
link_count,
size,
data_access_timestamp: data_access_timestamp.map(|t| t.try_into()).transpose()?,
data_access_timestamp: data_access_timestamp.and_then(|t| t.try_into().ok()),
data_modification_timestamp: data_modification_timestamp
.map(|t| t.try_into())
.transpose()?,
status_change_timestamp: status_change_timestamp.map(|t| t.try_into()).transpose()?,
.and_then(|t| t.try_into().ok()),
status_change_timestamp: status_change_timestamp.and_then(|t| t.try_into().ok()),
})
}
}
Expand Down
21 changes: 20 additions & 1 deletion crates/wasi/tests/all/p1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@ use wasmtime_wasi::p1::{WasiP1Ctx, add_to_linker_async};
use wasmtime_wasi::{WasiCtxBuilder, WasiView};

async fn run(path: &str, with_builder: impl FnOnce(&mut WasiCtxBuilder)) -> Result<()> {
run_with_workspace_setup(path, |_| Ok(()), with_builder).await
}

async fn run_with_workspace_setup(
Comment thread
SebTardif marked this conversation as resolved.
path: &str,
setup: impl FnOnce(&Path) -> Result<()>,
with_builder: impl FnOnce(&mut WasiCtxBuilder),
) -> Result<()> {
let path = Path::new(path);
let name = path.file_stem().unwrap().to_str().unwrap();
let engine = test_programs_artifacts::engine(|_config| {});
let mut linker = Linker::<Ctx<WasiP1Ctx>>::new(&engine);
add_to_linker_async(&mut linker, |t| &mut t.wasi)?;

let module = Module::from_file(&engine, path)?;
let (mut store, _td) = Ctx::new(&engine, name, |builder| {
let (mut store, _td) = Ctx::new_with_workspace_setup(&engine, name, setup, |builder| {
with_builder(builder);
builder.build_p1()
})?;
Expand Down Expand Up @@ -69,6 +77,17 @@ async fn p1_fd_filestat_get() {
async fn p1_fd_filestat_set() {
run(P1_FD_FILESTAT_SET, |_| {}).await.unwrap()
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn p1_stat_extreme_host_mtime() {
run_with_workspace_setup(
P1_STAT_EXTREME_HOST_MTIME,
crate::store::prepare_extreme_mtime_fixture,
|_| {},
)
.await
.unwrap()
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn p1_fd_flags_set() {
run(P1_FD_FLAGS_SET, |_| {}).await.unwrap()
Expand Down
20 changes: 19 additions & 1 deletion crates/wasi/tests/all/p2/async_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ use wasmtime_wasi::p2::add_to_linker_async;
use wasmtime_wasi::p2::bindings::Command;

async fn run(path: &str, with_builder: impl FnOnce(&mut WasiCtxBuilder)) -> Result<()> {
run_with_workspace_setup(path, |_| Ok(()), with_builder).await
}

async fn run_with_workspace_setup(
Comment thread
SebTardif marked this conversation as resolved.
path: &str,
setup: impl FnOnce(&Path) -> Result<()>,
with_builder: impl FnOnce(&mut WasiCtxBuilder),
) -> Result<()> {
let path = Path::new(path);
let name = path.file_stem().unwrap().to_str().unwrap();
let engine = test_programs_artifacts::engine(|_config| {});
let mut linker = Linker::new(&engine);
add_to_linker_async(&mut linker)?;

let (mut store, _td) = Ctx::new(&engine, name, |builder| {
let (mut store, _td) = Ctx::new_with_workspace_setup(&engine, name, setup, |builder| {
with_builder(builder);
MyWasiCtx::new(builder.build())
})?;
Expand Down Expand Up @@ -73,6 +81,16 @@ async fn p1_fd_filestat_set() {
run(P1_FD_FILESTAT_SET_COMPONENT, |_| {}).await.unwrap()
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn p1_stat_extreme_host_mtime() {
run_with_workspace_setup(
P1_STAT_EXTREME_HOST_MTIME_COMPONENT,
crate::store::prepare_extreme_mtime_fixture,
|_| {},
)
.await
.unwrap()
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn p1_fd_flags_set() {
run(P1_FD_FLAGS_SET_COMPONENT, |_| {}).await.unwrap()
}
Expand Down
19 changes: 18 additions & 1 deletion crates/wasi/tests/all/p2/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ use wasmtime_wasi::p2::add_to_linker_sync;
use wasmtime_wasi::p2::bindings::sync::Command;

fn run(path: &str, with_builder: impl Fn(&mut WasiCtxBuilder)) -> Result<()> {
run_with_workspace_setup(path, |_| Ok(()), with_builder)
}

fn run_with_workspace_setup(
Comment thread
SebTardif marked this conversation as resolved.
path: &str,
setup: impl Fn(&Path) -> Result<()>,
with_builder: impl Fn(&mut WasiCtxBuilder),
) -> Result<()> {
let path = Path::new(path);
let name = path.file_stem().unwrap().to_str().unwrap();
let engine = test_programs_artifacts::engine(|_| {});
Expand All @@ -17,7 +25,7 @@ fn run(path: &str, with_builder: impl Fn(&mut WasiCtxBuilder)) -> Result<()> {
let component = Component::from_file(&engine, path)?;

for blocking in [false, true] {
let (mut store, _td) = Ctx::new(&engine, name, |builder| {
let (mut store, _td) = Ctx::new_with_workspace_setup(&engine, name, &setup, |builder| {
with_builder(builder);
builder.allow_blocking_current_thread(blocking);
MyWasiCtx::new(builder.build())
Expand Down Expand Up @@ -77,6 +85,15 @@ fn p1_fd_filestat_set() {
run(P1_FD_FILESTAT_SET_COMPONENT, |_| {}).unwrap()
}
#[test_log::test]
fn p1_stat_extreme_host_mtime() {
run_with_workspace_setup(
P1_STAT_EXTREME_HOST_MTIME_COMPONENT,
crate::store::prepare_extreme_mtime_fixture,
|_| {},
)
.unwrap()
}
#[test_log::test]
fn p1_fd_flags_set() {
run(P1_FD_FLAGS_SET_COMPONENT, |_| {}).unwrap()
}
Expand Down
56 changes: 55 additions & 1 deletion crates/wasi/tests/all/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,23 @@ impl<T> Ctx<T> {
engine: &Engine,
name: &str,
configure: impl FnOnce(&mut WasiCtxBuilder) -> T,
) -> Result<(Store<Ctx<T>>, TempDir)> {
Self::new_with_workspace_setup(engine, name, |_| Ok(()), configure)
}

/// Like [`Self::new`], but allows seeding the preopened scratch directory
/// before the guest runs (for host-prepared filesystem fixtures).
pub fn new_with_workspace_setup(
engine: &Engine,
name: &str,
setup: impl FnOnce(&std::path::Path) -> Result<()>,
configure: impl FnOnce(&mut WasiCtxBuilder) -> T,
) -> Result<(Store<Ctx<T>>, TempDir)> {
const MAX_OUTPUT_SIZE: usize = 10 << 20;
let stdout = MemoryOutputPipe::new(MAX_OUTPUT_SIZE);
let stderr = MemoryOutputPipe::new(MAX_OUTPUT_SIZE);
let workspace = prepare_workspace(name)?;
setup(workspace.path())?;

// Create our wasi context.
let mut builder = WasiCtxBuilder::new();
Expand All @@ -54,7 +66,7 @@ impl<T> Ctx<T> {
stdout,
};

Ok((Store::new(&engine, ctx), workspace))
Ok((Store::new(engine, ctx), workspace))
}
}

Expand Down Expand Up @@ -92,3 +104,45 @@ impl WasiView for Ctx<MyWasiCtx> {
}
}
}

/// Best-effort far-past host `SystemTime` for stress-testing filestat conversion.
///
/// Prefer a value outside `Datetime`'s `i64` second range when the platform can
/// represent it (typical on Unix). Windows `SystemTime` is FILETIME-based and
/// cannot represent that far past, so fall back to the earliest practical
/// constructible time (around the Windows epoch, ~1601-01-01). The guest test
/// only requires that `path_filestat_get` does not panic the host.
fn extreme_host_mtime() -> std::time::SystemTime {
use std::time::{Duration, SystemTime};

// Outside i64 second range when representable (Unix).
if let Some(t) =
SystemTime::UNIX_EPOCH.checked_sub(Duration::from_secs((i64::MAX as u64).saturating_add(1)))
{
return t;
}
// Windows FILETIME lower bound ≈ 1601-01-01 UTC.
if let Some(t) = SystemTime::UNIX_EPOCH.checked_sub(Duration::from_secs(11_644_473_600)) {
return t;
}
SystemTime::UNIX_EPOCH
.checked_sub(Duration::from_secs(1))
.unwrap_or(SystemTime::UNIX_EPOCH)
}

/// Seed a preopened workspace with `extreme.dat` using a host mtime that may
/// fall outside WASI datetime ranges (or be clamped by the OS).
pub fn prepare_extreme_mtime_fixture(dir: &std::path::Path) -> Result<()> {
use std::fs::{File, FileTimes};
use std::io::Write;

let path = dir.join("extreme.dat");
File::create(&path)?.write_all(b"hello")?;
let extreme = extreme_host_mtime();
let f = File::options().write(true).open(&path)?;
let times = FileTimes::new().set_modified(extreme).set_accessed(extreme);
// Platforms may reject or clamp extreme times; the file still exists so the
// guest can open and stat without panicking the host.
let _ = f.set_times(times);
Ok(())
}
Loading