diff --git a/tests/rust/src/bin/dir_fd_op_failures.rs b/tests/rust/src/bin/dir_fd_op_failures.rs new file mode 100644 index 00000000..5c675444 --- /dev/null +++ b/tests/rust/src/bin/dir_fd_op_failures.rs @@ -0,0 +1,128 @@ +use std::{env, process}; +use wasi_tests::{assert_errno, open_scratch_directory}; + +unsafe fn test_fd_dir_ops(dir_fd: wasi::Fd) { + let stat = wasi::fd_filestat_get(dir_fd).expect("failed to fdstat"); + assert_eq!(stat.filetype, wasi::FILETYPE_DIRECTORY); + + let (pr_fd, pr_name_len) = (3..) + .map_while(|fd| wasi::fd_prestat_get(fd).ok().map(|stat| (fd, stat))) + .find_map(|(fd, wasi::Prestat { tag, u })| { + (tag == wasi::PREOPENTYPE_DIR.raw()).then_some((fd, u.dir.pr_name_len)) + }) + .expect("failed to find preopen directory"); + + let mut pr_name = vec![]; + let r = wasi::fd_prestat_dir_name(pr_fd, pr_name.as_mut_ptr(), 0); + assert_eq!(r, Err(wasi::ERRNO_NAMETOOLONG)); + + // Test that passing a larger than necessary buffer works correctly + let mut pr_name = vec![0; pr_name_len + 1]; + let r = wasi::fd_prestat_dir_name(pr_fd, pr_name.as_mut_ptr(), pr_name_len + 1); + assert_eq!(r, Ok(())); + + let mut read_buf = vec![0; 128].into_boxed_slice(); + let iovec = wasi::Iovec { + buf: read_buf.as_mut_ptr(), + buf_len: read_buf.len(), + }; + + // On posix, this fails with ERRNO_ISDIR + assert_errno!( + wasi::fd_read(dir_fd, &[iovec]).expect_err("fd_read error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + // On posix, this fails with ERRNO_ISDIR + assert_errno!( + wasi::fd_pread(dir_fd, &[iovec], 0).expect_err("fd_pread error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + let write_buf = vec![0; 128].into_boxed_slice(); + let ciovec = wasi::Ciovec { + buf: write_buf.as_ptr(), + buf_len: write_buf.len(), + }; + + assert_errno!( + wasi::fd_write(dir_fd, &[ciovec]).expect_err("fd_write error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + assert_errno!( + wasi::fd_pwrite(dir_fd, &[ciovec], 0).expect_err("fd_pwrite error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + // Divergence from posix: lseek(dirfd) will return 0 + assert_errno!( + wasi::fd_seek(dir_fd, 0, wasi::WHENCE_CUR).expect_err("fd_seek WHENCE_CUR error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + // Divergence from posix: lseek(dirfd) will return 0 + assert_errno!( + wasi::fd_seek(dir_fd, 0, wasi::WHENCE_SET).expect_err("fd_seek WHENCE_SET error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + // Divergence from posix: lseek(dirfd) will return 0 + assert_errno!( + wasi::fd_seek(dir_fd, 0, wasi::WHENCE_END).expect_err("fd_seek WHENCE_END error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + // Tell isnt in posix, its basically lseek with WHENCE_CUR above + assert_errno!( + wasi::fd_tell(dir_fd).expect_err("fd_tell error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + // fallocate(dirfd, FALLOC_FL_ZERO_RANGE, 0, 1) will fail with errno EBADF on linux. + // not available on mac os. + assert_errno!( + wasi::fd_allocate(dir_fd, 0, 0).expect_err("fd_allocate error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + // ftruncate(dirfd, 1) will fail with errno EINVAL on posix. + assert_errno!( + wasi::fd_filestat_set_size(dir_fd, 0).expect_err("fd_filestat_set_size error"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); +} + +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); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + unsafe { + test_fd_dir_ops(dir_fd); + } +} diff --git a/tests/rust/src/bin/path_open_nonblock.rs b/tests/rust/src/bin/path_open_nonblock.rs new file mode 100644 index 00000000..3ca99418 --- /dev/null +++ b/tests/rust/src/bin/path_open_nonblock.rs @@ -0,0 +1,30 @@ +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn try_path_open(dir_fd: wasi::Fd) { + let _fd = + wasi::path_open(dir_fd, 0, ".", 0, 0, 0, wasi::FDFLAGS_NONBLOCK).expect("opening the dir"); +} + +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); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { try_path_open(dir_fd) } +} diff --git a/tests/rust/src/bin/path_open_preopen.rs b/tests/rust/src/bin/path_open_preopen.rs new file mode 100644 index 00000000..8b4bc76e --- /dev/null +++ b/tests/rust/src/bin/path_open_preopen.rs @@ -0,0 +1,191 @@ +use std::{env, process}; + +unsafe fn find_first_preopened_fd(path: &str) -> Result { + let max_fd = (1 << 31) - 1; + + for i in 3..=max_fd { + match wasi::fd_prestat_get(i) { + Ok(prestat) => { + if prestat.tag != wasi::PREOPENTYPE_DIR.raw() { + continue; + } + let mut dst = Vec::with_capacity(prestat.u.dir.pr_name_len); + + if wasi::fd_prestat_dir_name(i, dst.as_mut_ptr(), dst.capacity()).is_err() { + continue; + } + dst.set_len(prestat.u.dir.pr_name_len); + + if dst == path.as_bytes() { + return Ok(i); + } + } + Err(_) => continue, + }; + } + + Err(format!("Failed to find a preopened directory")) +} + +unsafe fn path_open_preopen(preopened_fd: wasi::Fd) { + let prestat = wasi::fd_prestat_get(preopened_fd).expect("fd is a preopen"); + assert_eq!( + prestat.tag, + wasi::PREOPENTYPE_DIR.raw(), + "prestat is a directory" + ); + let mut dst = Vec::with_capacity(prestat.u.dir.pr_name_len); + wasi::fd_prestat_dir_name(preopened_fd, dst.as_mut_ptr(), dst.capacity()) + .expect("get preopen dir name"); + dst.set_len(prestat.u.dir.pr_name_len); + + let fdstat = wasi::fd_fdstat_get(preopened_fd).expect("get fdstat"); + + println!( + "preopen dir: {:?} base {:?} inheriting {:?}", + String::from_utf8_lossy(&dst), + fdstat.fs_rights_base, + fdstat.fs_rights_inheriting + ); + for (right, name) in directory_base_rights() { + assert!( + (fdstat.fs_rights_base & right) == right, + "fs_rights_base does not have required right `{name}`" + ); + } + for (right, name) in directory_inheriting_rights() { + assert!( + (fdstat.fs_rights_inheriting & right) == right, + "fs_rights_inheriting does not have required right `{name}`" + ); + } + + // Open with same rights it has now: + let _ = wasi::path_open( + preopened_fd, + 0, + ".", + 0, + fdstat.fs_rights_base, + fdstat.fs_rights_inheriting, + 0, + ) + .expect("open with same rights"); + + // Open with an empty set of rights: + let _ = wasi::path_open(preopened_fd, 0, ".", 0, 0, 0, 0).expect("open with empty rights"); + + // Open OFLAGS_DIRECTORY with an empty set of rights: + let _ = wasi::path_open(preopened_fd, 0, ".", wasi::OFLAGS_DIRECTORY, 0, 0, 0) + .expect("open with O_DIRECTORY empty rights"); + + // Open OFLAGS_DIRECTORY with just the read right: + let _ = wasi::path_open( + preopened_fd, + 0, + ".", + wasi::OFLAGS_DIRECTORY, + wasi::RIGHTS_FD_READ, + 0, + 0, + ) + .expect("open with O_DIRECTORY and read right"); + + if !wasi_tests::TESTCONFIG.errno_expect_windows() { + // Open OFLAGS_DIRECTORY and read/write rights should fail with isdir: + let err = wasi::path_open( + preopened_fd, + 0, + ".", + wasi::OFLAGS_DIRECTORY, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE, + 0, + 0, + ) + .err() + .expect("open with O_DIRECTORY and read/write should fail"); + assert_eq!( + err, + wasi::ERRNO_ISDIR, + "opening directory read/write should fail with ISDIR" + ); + } else { + // Open OFLAGS_DIRECTORY and read/write rights will succeed, only on windows: + let _ = wasi::path_open( + preopened_fd, + 0, + ".", + wasi::OFLAGS_DIRECTORY, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE, + 0, + 0, + ) + .expect("open with O_DIRECTORY and read/write should succeed on windows"); + } +} + +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); + process::exit(1); + }; + + let preopened_fd = unsafe { find_first_preopened_fd(arg.as_str()).unwrap() }; + + // Run the tests. + unsafe { path_open_preopen(preopened_fd) } +} + +// Hard-code the set of rights expected for a preopened directory. This is +// more brittle than we wanted to test for, but various userland +// implementations expect (at least) this set of rights to be present on all +// directories: + +fn directory_base_rights() -> Vec<(wasi::Rights, &'static str)> { + vec![ + (wasi::RIGHTS_PATH_CREATE_DIRECTORY, "PATH_CREATE_DIRECTORY"), + (wasi::RIGHTS_PATH_CREATE_FILE, "PATH_CREATE_FILE"), + (wasi::RIGHTS_PATH_LINK_SOURCE, "PATH_LINK_SOURCE"), + (wasi::RIGHTS_PATH_LINK_TARGET, "PATH_LINK_TARGET"), + (wasi::RIGHTS_PATH_OPEN, "PATH_OPEN"), + (wasi::RIGHTS_FD_READDIR, "FD_READDIR"), + (wasi::RIGHTS_PATH_READLINK, "PATH_READLINK"), + (wasi::RIGHTS_PATH_RENAME_SOURCE, "PATH_RENAME_SOURCE"), + (wasi::RIGHTS_PATH_RENAME_TARGET, "PATH_RENAME_TARGET"), + (wasi::RIGHTS_PATH_SYMLINK, "PATH_SYMLINK"), + (wasi::RIGHTS_PATH_REMOVE_DIRECTORY, "PATH_REMOVE_DIRECTORY"), + (wasi::RIGHTS_PATH_UNLINK_FILE, "PATH_UNLINK_FILE"), + (wasi::RIGHTS_PATH_FILESTAT_GET, "PATH_FILESTAT_GET"), + ( + wasi::RIGHTS_PATH_FILESTAT_SET_TIMES, + "PATH_FILESTAT_SET_TIMES", + ), + (wasi::RIGHTS_FD_FILESTAT_GET, "FD_FILESTAT_GET"), + (wasi::RIGHTS_FD_FILESTAT_SET_TIMES, "FD_FILESTAT_SET_TIMES"), + ] +} + +pub(crate) fn directory_inheriting_rights() -> Vec<(wasi::Rights, &'static str)> { + let mut rights = directory_base_rights(); + rights.extend_from_slice(&[ + (wasi::RIGHTS_FD_DATASYNC, "FD_DATASYNC"), + (wasi::RIGHTS_FD_READ, "FD_READ"), + (wasi::RIGHTS_FD_SEEK, "FD_SEEK"), + (wasi::RIGHTS_FD_FDSTAT_SET_FLAGS, "FD_FDSTAT_SET_FLAGS"), + (wasi::RIGHTS_FD_SYNC, "FD_SYNC"), + (wasi::RIGHTS_FD_TELL, "FD_TELL"), + (wasi::RIGHTS_FD_WRITE, "FD_WRITE"), + (wasi::RIGHTS_FD_ADVISE, "FD_ADVISE"), + (wasi::RIGHTS_FD_ALLOCATE, "FD_ALLOCATE"), + (wasi::RIGHTS_FD_FILESTAT_GET, "FD_FILESTAT_GET"), + (wasi::RIGHTS_FD_FILESTAT_SET_SIZE, "FD_FILESTAT_SET_SIZE"), + (wasi::RIGHTS_FD_FILESTAT_SET_TIMES, "FD_FILESTAT_SET_TIMES"), + (wasi::RIGHTS_POLL_FD_READWRITE, "POLL_FD_READWRITE"), + ]); + rights +} diff --git a/tests/rust/src/bin/path_open_read_write.rs b/tests/rust/src/bin/path_open_read_write.rs new file mode 100644 index 00000000..66478bd3 --- /dev/null +++ b/tests/rust/src/bin/path_open_read_write.rs @@ -0,0 +1,141 @@ +use std::{env, process}; +use wasi_tests::{assert_errno, create_file, open_scratch_directory}; + +const TEST_FILENAME: &'static str = "file.cleanup"; + +unsafe fn test_path_open_read_write(dir_fd: wasi::Fd) { + create_file(dir_fd, TEST_FILENAME); + + let f_readonly = wasi::path_open(dir_fd, 0, TEST_FILENAME, 0, wasi::RIGHTS_FD_READ, 0, 0) + .expect("open file readonly"); + + let stat = wasi::fd_fdstat_get(f_readonly).expect("get fdstat readonly"); + assert!( + stat.fs_rights_base & wasi::RIGHTS_FD_READ == wasi::RIGHTS_FD_READ, + "readonly has read right" + ); + assert!( + stat.fs_rights_base & wasi::RIGHTS_FD_WRITE == 0, + "readonly does not have write right" + ); + + let buffer = &mut [0u8; 100]; + let iovec = wasi::Iovec { + buf: buffer.as_mut_ptr(), + buf_len: buffer.len(), + }; + let nread = wasi::fd_read(f_readonly, &[iovec]).expect("reading readonly file"); + assert_eq!(nread, 0, "readonly file is empty"); + + let write_buffer = &[1u8; 50]; + let ciovec = wasi::Ciovec { + buf: write_buffer.as_ptr(), + buf_len: write_buffer.len(), + }; + assert_errno!( + wasi::fd_write(f_readonly, &[ciovec]) + .err() + .expect("read of writeonly fails"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + + wasi::fd_close(f_readonly).expect("close readonly"); + + // =============== WRITE ONLY ================== + let f_writeonly = wasi::path_open(dir_fd, 0, TEST_FILENAME, 0, wasi::RIGHTS_FD_WRITE, 0, 0) + .expect("open file writeonly"); + + let stat = wasi::fd_fdstat_get(f_writeonly).expect("get fdstat writeonly"); + assert!( + stat.fs_rights_base & wasi::RIGHTS_FD_READ == 0, + "writeonly does not have read right" + ); + assert!( + stat.fs_rights_base & wasi::RIGHTS_FD_WRITE == wasi::RIGHTS_FD_WRITE, + "writeonly has write right" + ); + + assert_errno!( + wasi::fd_read(f_writeonly, &[iovec]) + .err() + .expect("read of writeonly fails"), + wasi::ERRNO_BADF, + wasi::ERRNO_NOTCAPABLE + ); + let bytes_written = wasi::fd_write(f_writeonly, &[ciovec]).expect("write to writeonly"); + assert_eq!(bytes_written, write_buffer.len()); + + wasi::fd_close(f_writeonly).expect("close writeonly"); + + // ============== READ WRITE ======================= + + let f_readwrite = wasi::path_open( + dir_fd, + 0, + TEST_FILENAME, + 0, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE | wasi::RIGHTS_FD_FILESTAT_GET, + 0, + 0, + ) + .expect("open file readwrite"); + let stat = wasi::fd_fdstat_get(f_readwrite).expect("get fdstat readwrite"); + assert!( + stat.fs_rights_base & wasi::RIGHTS_FD_READ == wasi::RIGHTS_FD_READ, + "readwrite has read right" + ); + assert!( + stat.fs_rights_base & wasi::RIGHTS_FD_WRITE == wasi::RIGHTS_FD_WRITE, + "readwrite has write right" + ); + + let nread = wasi::fd_read(f_readwrite, &[iovec]).expect("reading readwrite file"); + assert_eq!( + nread, + write_buffer.len(), + "readwrite file contains contents from writeonly open" + ); + + let write_buffer_2 = &[2u8; 25]; + let ciovec = wasi::Ciovec { + buf: write_buffer_2.as_ptr(), + buf_len: write_buffer_2.len(), + }; + let bytes_written = wasi::fd_write(f_readwrite, &[ciovec]).expect("write to readwrite"); + assert_eq!(bytes_written, write_buffer_2.len()); + + let filestat = wasi::fd_filestat_get(f_readwrite).expect("get filestat readwrite"); + assert_eq!( + filestat.size as usize, + write_buffer.len() + write_buffer_2.len(), + "total written is both write buffers" + ); + + wasi::fd_close(f_readwrite).expect("close readwrite"); + + wasi::path_unlink_file(dir_fd, TEST_FILENAME).expect("removing a file"); +} + +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); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_open_read_write(dir_fd) } +} diff --git a/tests/rust/src/bin/remove_directory_trailing_slashes.rs b/tests/rust/src/bin/remove_directory_trailing_slashes.rs new file mode 100644 index 00000000..506d1ba5 --- /dev/null +++ b/tests/rust/src/bin/remove_directory_trailing_slashes.rs @@ -0,0 +1,63 @@ +use std::{env, process}; +use wasi_tests::{assert_errno, create_file, open_scratch_directory}; + +const TEST_FILENAME: &'static str = "file.cleanup"; +const TEST_DIRNAME: &'static str = "dir.cleanup"; + +unsafe fn test_remove_directory_trailing_slashes(dir_fd: wasi::Fd) { + // Create a directory in the scratch directory. + wasi::path_create_directory(dir_fd, TEST_DIRNAME).expect("creating a directory"); + + // Test that removing it succeeds. + wasi::path_remove_directory(dir_fd, TEST_DIRNAME) + .expect("remove_directory on a directory should succeed"); + + wasi::path_create_directory(dir_fd, TEST_DIRNAME).expect("creating a directory"); + + // Test that removing it with a trailing slash succeeds. + wasi::path_remove_directory(dir_fd, &format!("{}/", TEST_DIRNAME)) + .expect("remove_directory with a trailing slash on a directory should succeed"); + + // Create a temporary file. + create_file(dir_fd, TEST_FILENAME); + + // Test that removing it with no trailing slash fails. + assert_errno!( + wasi::path_remove_directory(dir_fd, TEST_FILENAME) + .expect_err("remove_directory without a trailing slash on a file should fail"), + wasi::ERRNO_NOTDIR + ); + + // Test that removing it with a trailing slash fails. + assert_errno!( + wasi::path_remove_directory(dir_fd, &format!("{}/", TEST_FILENAME)) + .expect_err("remove_directory with a trailing slash on a file should fail"), + unix => wasi::ERRNO_NOTDIR, + windows => wasi::ERRNO_NOENT + ); + + wasi::path_unlink_file(dir_fd, TEST_FILENAME).expect("removing a file"); +} + +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); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_remove_directory_trailing_slashes(dir_fd) } +} diff --git a/tests/rust/testsuite/dir_fd_op_failures.json b/tests/rust/testsuite/dir_fd_op_failures.json new file mode 100644 index 00000000..ef083796 --- /dev/null +++ b/tests/rust/testsuite/dir_fd_op_failures.json @@ -0,0 +1,4 @@ +{ + "dirs": ["fs-tests.dir"], + "args": ["fs-tests.dir"] +} \ No newline at end of file diff --git a/tests/rust/testsuite/path_open_nonblock.json b/tests/rust/testsuite/path_open_nonblock.json new file mode 100644 index 00000000..ef083796 --- /dev/null +++ b/tests/rust/testsuite/path_open_nonblock.json @@ -0,0 +1,4 @@ +{ + "dirs": ["fs-tests.dir"], + "args": ["fs-tests.dir"] +} \ No newline at end of file diff --git a/tests/rust/testsuite/path_open_preopen.json b/tests/rust/testsuite/path_open_preopen.json new file mode 100644 index 00000000..ef083796 --- /dev/null +++ b/tests/rust/testsuite/path_open_preopen.json @@ -0,0 +1,4 @@ +{ + "dirs": ["fs-tests.dir"], + "args": ["fs-tests.dir"] +} \ No newline at end of file diff --git a/tests/rust/testsuite/path_open_read_write.json b/tests/rust/testsuite/path_open_read_write.json new file mode 100644 index 00000000..ef083796 --- /dev/null +++ b/tests/rust/testsuite/path_open_read_write.json @@ -0,0 +1,4 @@ +{ + "dirs": ["fs-tests.dir"], + "args": ["fs-tests.dir"] +} \ No newline at end of file diff --git a/tests/rust/testsuite/remove_directory_trailing_slashes.json b/tests/rust/testsuite/remove_directory_trailing_slashes.json new file mode 100644 index 00000000..ef083796 --- /dev/null +++ b/tests/rust/testsuite/remove_directory_trailing_slashes.json @@ -0,0 +1,4 @@ +{ + "dirs": ["fs-tests.dir"], + "args": ["fs-tests.dir"] +} \ No newline at end of file