diff --git a/Cargo.lock b/Cargo.lock index a67cf7a95..a9b55aad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1449,8 +1449,6 @@ dependencies = [ "bitflags", "buddy_system_allocator", "hashbrown", - "litebox_broker_core", - "litebox_broker_host", "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index b62d35f52..62dfe461d 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -35,7 +35,7 @@ fn ratchet_globals() -> Result<()> { ratchet( &[ ("dev_bench/", 1), - ("litebox/", 8), + ("litebox/", 7), ("litebox_broker_core/", 1), ("litebox_broker_transport_linux_userland/", 1), ("litebox_broker_userland/", 1), diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 903377283..6a0996212 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -15,7 +15,6 @@ buddy_system_allocator = { version = "0.11.0", default-features = false, feature # Depend on (currently unreleased) slabmalloc `main`, which contains some fixes on top of `0.11.0` slabmalloc = { git = "https://github.com/gz/rust-slabmalloc.git", rev = "19480b2e82704210abafe575fb9699184c1be110" } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } -litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core" } litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport" } @@ -35,11 +34,5 @@ lock_tracing = ["litebox_platform/lock_tracing"] panic_on_unclosed_fd_drop = [] enforce_singleton_litebox_instance = [] -# TODO: Remove these dev-dependencies together with `fs::file_tests`, once production callers -# exercise the broker file API end to end. -[dev-dependencies] -litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core", features = ["test-support"] } -litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host", features = ["test-support"] } - [lints] workspace = true diff --git a/litebox/src/fs/errors.rs b/litebox/src/fs/errors.rs index b5fe006aa..77b3a5b94 100644 --- a/litebox/src/fs/errors.rs +++ b/litebox/src/fs/errors.rs @@ -1,20 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Possible errors from [`Resolver`] - -#[expect( - unused_imports, - reason = "used for doc string links to work out, but not for code" -)] -use super::resolver::Resolver; +//! Errors from LiteBox file operations. use thiserror::Error; // XXX(jayb): We probably need to introduce a notion of `Stale` to many/most of these errors, in // order to more correctly support network-attached file systems. -/// Possible errors from [`Resolver::open`] +/// Possible errors from [`crate::LiteBox::open_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum OpenError { @@ -34,12 +28,12 @@ pub enum OpenError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::close`] +/// Possible errors from [`crate::LiteBox::close_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum CloseError {} -/// Possible errors from [`Resolver::read`] +/// Possible errors from [`crate::LiteBox::read_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum ReadError { @@ -53,7 +47,7 @@ pub enum ReadError { Io, } -/// Possible errors from [`Resolver::write`] +/// Possible errors from [`crate::LiteBox::write_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum WriteError { @@ -67,7 +61,7 @@ pub enum WriteError { Io, } -/// Possible errors from [`Resolver::seek`] +/// Possible errors from [`crate::LiteBox::seek_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum SeekError { @@ -85,7 +79,7 @@ pub enum SeekError { Io, } -/// Possible errors from [`Resolver::truncate`] +/// Possible errors from [`crate::LiteBox::truncate_file`]. #[derive(Error, Debug)] pub enum TruncateError { #[error("fd has been closed already")] @@ -100,7 +94,7 @@ pub enum TruncateError { Io, } -/// Possible errors from [`Resolver::chmod`] +/// Possible errors from [`crate::LiteBox::chmod_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum ChmodError { @@ -117,7 +111,7 @@ pub enum ChmodError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::chown`] +/// Possible errors from [`crate::LiteBox::chown_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum ChownError { @@ -134,7 +128,7 @@ pub enum ChownError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::unlink`] +/// Possible errors from [`crate::LiteBox::unlink_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum UnlinkError { @@ -150,7 +144,7 @@ pub enum UnlinkError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::mkdir`] +/// Possible errors from [`crate::LiteBox::mkdir_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum MkdirError { @@ -166,7 +160,7 @@ pub enum MkdirError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::rmdir`] +/// Possible errors from [`crate::LiteBox::rmdir_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum RmdirError { @@ -188,7 +182,7 @@ pub enum RmdirError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::read_dir`] +/// Possible errors from [`crate::LiteBox::read_file_directory`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum ReadDirError { @@ -202,7 +196,7 @@ pub enum ReadDirError { Io, } -/// Possible errors from [`Resolver::file_status`] +/// Possible errors from [`crate::LiteBox::path_file_status`] and [`crate::LiteBox::file_status`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum FileStatusError { @@ -214,16 +208,6 @@ pub enum FileStatusError { PathError(#[from] PathError), } -/// Possible errors from a backend walk -#[non_exhaustive] -#[derive(Error, Debug)] -pub enum WalkError { - #[error("I/O error")] - Io, - #[error(transparent)] - PathError(#[from] PathError), -} - /// Possible errors in any file-system function due to path errors. #[derive(Error, Debug)] pub enum PathError { @@ -234,7 +218,7 @@ pub enum PathError { #[cfg(debug_assertions)] dir: alloc::string::String, #[cfg(debug_assertions)] - perms: crate::fs::Mode, + perms: litebox_broker_protocol::fs::FileMode, }, #[error("invalid characters, not permitted by underlying file system")] InvalidPathname, diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs index ffed84073..78292f29f 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -429,14 +429,11 @@ fn read_directory_broker_error(error: crate::broker::error::BrokerControlError) fn path_error(error: FileError) -> Option { match error { FileError::NoSuchFileOrDirectory => Some(PathError::NoSuchFileOrDirectory), - // TODO: `PathError` still carries the compatibility `crate::fs::Mode` used by the local - // resolver path. It becomes a protocol `FileMode` once the resolver compatibility surface - // is removed. FileError::NoSearchPermissions => Some(PathError::NoSearchPerms { #[cfg(debug_assertions)] dir: String::new(), #[cfg(debug_assertions)] - perms: super::Mode::empty(), + perms: Mode::empty(), }), FileError::InvalidPathname => Some(PathError::InvalidPathname), FileError::MissingComponent => Some(PathError::MissingComponent), diff --git a/litebox/src/fs/file_tests.rs b/litebox/src/fs/file_tests.rs deleted file mode 100644 index 20be1e816..000000000 --- a/litebox/src/fs/file_tests.rs +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! TEMPORARY: coverage for the broker-backed file API while it coexists with the local resolver. -//! -//! These tests exist only because no production caller uses [`crate::fs::Context`] and -//! [`crate::fs::FileFd`] yet. Remove this module (and the broker test-support dev-dependencies it -//! needs) once the shims call the broker file API and cover it end to end. - -use alloc::sync::Arc; -use alloc::vec::Vec; - -use litebox_broker_core::fs::in_mem::{InMem, InitialNode}; -use litebox_broker_core::fs::resolver::Resolver as BrokerResolver; -use litebox_broker_core::test_support::TestBrokerCoreBuilder; -use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; -use litebox_broker_host::test_support::InProcessBrokerSetup; -use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::fs::{ - FileAccessMode, FileMode, FileOpenFlags, FileSeekWhence, FileType, FileUser, - MAX_FILE_TRANSFER_SIZE, -}; -use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_SLOT_SIZE; -use spin::mutex::SpinMutex; - -use crate::LiteBox; -use crate::fs::errors::{OpenError, ReadError, SeekError}; -use crate::fs::{Context, FileFd}; -use crate::platform::mock::MockPlatform; - -/// The process-wide broker core. Only one may exist per process, so every association in this test -/// binary shares one filesystem; tests must use disjoint paths. -static BROKER: SpinMutex> = SpinMutex::new(None); - -fn test_broker() -> BrokerCore { - let mut broker = BROKER.lock(); - broker - .get_or_insert_with(|| { - let fs = - BrokerResolver::::new(InMem::::new_initialized([( - "/", - InitialNode::Directory { - mode: FileMode::from_u32_bits_truncate(0o777), - owner: FileUser { user: 0, group: 0 }, - }, - )])); - TestBrokerCoreBuilder::new(PolicyEngine::with_unauthenticated_rights( - ObjectRights::all(), - )) - .with_file_service(Arc::new(fs)) - .build() - .expect("the test broker core must be constructible") - }) - .clone() -} - -fn broker_litebox() -> LiteBox { - let setup = InProcessBrokerSetup::new(test_broker()); - let (broker_local, ()) = BrokerLocal::negotiate(setup, |setup| { - let memory = setup.shared_memory(); - Ok((setup.activate(), memory, ())) - }) - .expect("the in-process broker association must negotiate"); - LiteBox::new_with_broker_local(MockPlatform::new(), broker_local) -} - -fn create_file(litebox: &LiteBox, context: &Context, path: &str) -> FileFd { - litebox - .open_file( - context, - path, - FileAccessMode::ReadWrite, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, - FileMode::from_u32_bits_truncate(0o644), - ) - .expect("creating a new file must succeed") -} - -#[test] -fn broker_file_round_trip() { - let litebox = broker_litebox(); - let mut context = Context::new(); - litebox - .mkdir_file( - &context, - "/round_trip", - FileMode::from_u32_bits_truncate(0o755), - ) - .unwrap(); - context.set_cwd(context.resolve("/round_trip").unwrap()); - - let fd = create_file(&litebox, &context, "./data"); - assert_eq!(litebox.write_file(&fd, b"broker data", None).unwrap(), 11); - assert_eq!( - litebox - .seek_file(&fd, 7, FileSeekWhence::RelativeToBeginning) - .unwrap(), - 7 - ); - let mut buffer = [0; 4]; - assert_eq!(litebox.read_file(&fd, &mut buffer, None).unwrap(), 4); - assert_eq!(&buffer, b"data"); - assert_eq!(litebox.read_file(&fd, &mut buffer, Some(0)).unwrap(), 4); - assert_eq!(&buffer, b"brok"); - - let status = litebox.file_status(&fd).unwrap(); - assert_eq!(status.file_type, FileType::RegularFile); - assert_eq!(status.size, 11); - assert_eq!(status.owner.user, context.acting_user().user); - - litebox.truncate_file(&fd, 4, true).unwrap(); - assert_eq!(litebox.path_file_status(&context, "data").unwrap().size, 4); - - litebox - .chmod_file(&context, "data", FileMode::from_u32_bits_truncate(0o600)) - .unwrap(); - litebox.chown_file(&context, "data", Some(7), None).unwrap(); - let status = litebox - .path_file_status(&context, "/round_trip/data") - .unwrap(); - assert_eq!(status.mode, FileMode::from_u32_bits_truncate(0o600)); - assert_eq!(status.owner.user, 7); - - let directory = litebox - .open_file( - &context, - "/round_trip", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, - FileMode::empty(), - ) - .unwrap(); - let names: Vec<_> = litebox - .read_file_directory(&directory) - .unwrap() - .into_iter() - .map(|entry| entry.name) - .collect(); - assert!(names.iter().any(|name| name == "data")); - litebox.close_file(&directory).unwrap(); - - litebox.close_file(&fd).unwrap(); - litebox.unlink_file(&context, "data").unwrap(); - litebox.rmdir_file(&Context::new(), "/round_trip").unwrap(); - assert!(matches!( - litebox.path_file_status(&context, "/round_trip"), - Err(crate::fs::errors::FileStatusError::PathError(_)) - )); -} - -#[test] -fn broker_file_round_trips_maximum_transfer() { - let litebox = broker_litebox(); - let context = Context::new(); - let fd = create_file(&litebox, &context, "/maximum_transfer"); - let data = (0..MAX_FILE_TRANSFER_SIZE as usize) - .map(|index| u8::try_from(index % 251).unwrap()) - .collect::>(); - - assert_eq!(litebox.write_file(&fd, &data, None).unwrap(), data.len()); - assert_eq!( - litebox - .seek_file(&fd, 0, FileSeekWhence::RelativeToBeginning) - .unwrap(), - 0 - ); - let mut output = alloc::vec![0; data.len()]; - assert_eq!( - litebox.read_file(&fd, &mut output, None).unwrap(), - data.len() - ); - assert_eq!(output, data); - - let short_length = SHARED_BUFFER_SLOT_SIZE as usize + 3; - litebox.truncate_file(&fd, short_length, true).unwrap(); - output.fill(0xa5); - assert_eq!( - litebox.read_file(&fd, &mut output, None).unwrap(), - short_length - ); - assert_eq!(output[..short_length], data[..short_length]); - assert!(output[short_length..].iter().all(|byte| *byte == 0xa5)); - - litebox.close_file(&fd).unwrap(); - litebox.unlink_file(&context, "/maximum_transfer").unwrap(); -} - -#[test] -fn closed_descriptor_operations_report_closed_fd() { - let litebox = broker_litebox(); - let context = Context::new(); - litebox - .mkdir_file(&context, "/closed", FileMode::from_u32_bits_truncate(0o755)) - .unwrap(); - - let fd = create_file(&litebox, &context, "/closed/data"); - litebox.write_file(&fd, b"data", None).unwrap(); - litebox.close_file(&fd).unwrap(); - - let mut buffer = [0; 4]; - assert!(matches!( - litebox.read_file(&fd, &mut buffer, None), - Err(ReadError::ClosedFd) - )); - assert!(matches!( - litebox.seek_file(&fd, 0, FileSeekWhence::RelativeToCurrentOffset), - Err(SeekError::ClosedFd) - )); - - // The broker object is released with the descriptor, so the path can be replaced. - litebox.unlink_file(&context, "/closed/data").unwrap(); - let fd = create_file(&litebox, &context, "/closed/data"); - litebox.close_file(&fd).unwrap(); - litebox.unlink_file(&context, "/closed/data").unwrap(); - litebox.rmdir_file(&context, "/closed").unwrap(); -} - -#[test] -fn broker_file_requires_a_broker() { - let litebox = LiteBox::new(MockPlatform::new()); - assert!(matches!( - litebox.open_file( - &Context::new(), - "/missing", - FileAccessMode::ReadOnly, - FileOpenFlags::NONE, - FileMode::empty(), - ), - Err(OpenError::Io) - )); -} diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index bb44d9f37..65362b3a4 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -1,312 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Guest-facing filesystem facade. +//! Guest-facing file operations and local descriptor integration. //! //! Filesystem resolution and backend implementations live in `litebox_broker_core`. This module -//! retains LiteBox's guest values, descriptor integration, and compatibility module paths. -//! -//! [`Context`], [`FileFd`], and the broker-backed [`crate::LiteBox`] file operations are the -//! broker file API. The [`resolver`] module and the compatibility modules below remain until every -//! caller has migrated onto them. - -use bitflags::bitflags; - -use core::ffi::c_uint; -use core::num::NonZeroUsize; +//! retains caller context and local descriptors. Shared file values are defined in +//! [`litebox_broker_protocol::fs`]. pub mod errors; mod file; -pub mod resolver; - -// TODO: Remove this module once production callers exercise the broker file API. -#[cfg(test)] -mod file_tests; pub use file::{BrokerFile, Context, FileFd, ResolvedPath}; - -// TODO: Remove these implementation-facing compatibility modules once LiteBox uses the broker -// file APIs exclusively. They temporarily preserve local filesystem construction while resolver -// and backend ownership moves into broker core. -#[doc(hidden)] -pub mod backend { - pub use litebox_broker_core::fs::backend::*; -} - -#[doc(hidden)] -pub mod composer { - pub use litebox_broker_core::fs::composer::*; -} - -#[doc(hidden)] -pub mod devices { - pub use litebox_broker_core::fs::devices::*; -} - -#[doc(hidden)] -pub mod in_mem { - pub use litebox_broker_core::fs::in_mem::{InMem, InMemDirHandle, InMemFileHandle}; - - /// A node used to pre-populate an [`InMem`] backend, via [`InMem::new_initialized`]. - pub enum InitialNode { - /// A directory. - Directory { - /// Permission bits for the directory. - mode: super::Mode, - /// Owning user and group. - owner: super::UserInfo, - }, - /// A regular file, along with its contents. - File { - /// Permission bits for the file. - mode: super::Mode, - /// Owning user and group. - owner: super::UserInfo, - /// The file's contents. - /// - /// Borrowed data is kept borrowed until the first write to the file, which makes this - /// the cheap way to set up large read-heavy files (such as executables). - data: alloc::borrow::Cow<'static, [u8]>, - }, - } - - impl From for litebox_broker_core::fs::in_mem::InitialNode { - fn from(node: InitialNode) -> Self { - match node { - InitialNode::Directory { mode, owner } => Self::Directory { - mode: litebox_broker_core::fs::Mode::from_u32_bits_truncate(mode.bits()), - owner: litebox_broker_core::fs::UserInfo { - user: owner.user, - group: owner.group, - }, - }, - InitialNode::File { mode, owner, data } => Self::File { - mode: litebox_broker_core::fs::Mode::from_u32_bits_truncate(mode.bits()), - owner: litebox_broker_core::fs::UserInfo { - user: owner.user, - group: owner.group, - }, - data, - }, - } - } - } -} - -#[doc(hidden)] -pub mod nine_p { - pub use litebox_broker_core::fs::nine_p::*; -} - -#[doc(hidden)] -pub mod overlay { - pub use litebox_broker_core::fs::overlay::*; -} - -#[doc(hidden)] -pub mod tar_ro { - pub use litebox_broker_core::fs::tar_ro::*; -} - -#[cfg(test)] -mod tests; - -bitflags! { - /// `S_I*` constants for open, ... - #[repr(transparent)] - #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] - pub struct Mode: c_uint { - /// `S_IRWXU`: user (file owner) has read, write, and execute permission - const RWXU = 0o00700; - /// `S_IRUSR`: user has read permission - const RUSR = 0o00400; - /// `S_IWUSR`: user has write permission - const WUSR = 0o00200; - /// `S_IXUSR`: user has execute permission - const XUSR = 0o00100; - /// `S_IRWXG`: group has read, write, and execute permission - const RWXG = 0o00070; - /// `S_IRGRP`: group has read permission - const RGRP = 0o00040; - /// `S_IWGRP`: group has write permission - const WGRP = 0o00020; - /// `S_IXGRP`: group has execute permission - const XGRP = 0o00010; - /// `S_IRWXO`: others have read, write, and execute permission - const RWXO = 0o00007; - /// `S_IROTH`: others have read permission - const ROTH = 0o00004; - /// `S_IWOTH`: others have write permission - const WOTH = 0o00002; - /// `S_IXOTH`: others have execute permission - const XOTH = 0o00001; - /// `S_ISUID`: set-user-ID bit - const SUID = 0o0004000; - /// `S_ISGID`: set-group-ID bit (see inode(7)). - const SGID = 0o0002000; - /// `S_ISVTX`: sticky bit (see inode(7)). - const SVTX = 0o0001000; - /// - const _ = !0; - } -} - -/// Types of files on a file-system. -/// -/// See [`resolver::Resolver::file_status`]. -/// -/// This is the canonical broker protocol object kind; LiteBox does not define its own. -pub use litebox_broker_protocol::fs::FileType; - -bitflags! { - /// `O_*` constants for use with open, ... - #[repr(transparent)] - #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] - pub struct OFlags: c_uint { - /// `O_RDONLY`: read-only - const RDONLY = 0x0; - /// `O_WRONLY`: write-only - const WRONLY = 0x1; - /// `O_RDWR`: read/write. - /// - /// This is not equal to `RDONLY | WRONLY`. It's a distinct flag. - const RDWR = 0x2; - /// `O_APPEND`: append mode - const APPEND = 0x400; - /// `O_ASYNC`: signal-driven I/O - const ASYNC = 0x2000; - /// `O_CLOEXEC`: close-on-exec flag - const CLOEXEC = 0x80000; - /// `O_CREAT`: if path does not exist, create it as a regular file - const CREAT = 0x40; - /// `O_DIRECT`: try to minimize cache effects of I/O for this file - #[cfg(target_arch = "x86_64")] - const DIRECT = 0x4000; - #[cfg(target_arch = "aarch64")] - const DIRECT = 0x10000; - /// `O_DIRECTORY`: fail if not a directory - #[cfg(target_arch = "x86_64")] - const DIRECTORY = 0x10000; - #[cfg(target_arch = "aarch64")] - const DIRECTORY = 0x4000; - /// `O_DSYNC`: write operations on the file will complete according to the requirements of - /// synchronized I/O *data* integrity completion. - const DSYNC = 0x1000; - /// `O_EXCL`: exclusive use - const EXCL = 0x80; - /// `O_LARGEFILE`: allow large file support - #[cfg(target_arch = "x86_64")] - const LARGEFILE = 0x8000; - #[cfg(target_arch = "aarch64")] - const LARGEFILE = 0x20000; - /// `O_NOATIME`: do not update access time - const NOATIME = 0x40000; - /// `O_NOCTTY`: do not assign controlling terminal - const NOCTTY = 0x100; - /// `O_NOFOLLOW`: fail if the path does not point to a regular file - #[cfg(target_arch = "x86_64")] - const NOFOLLOW = 0x20000; - #[cfg(target_arch = "aarch64")] - const NOFOLLOW = 0x8000; - /// `O_NDELAY`: non-blocking mode (same as NONBLOCK) - const NDELAY = 0x800; - /// `O_NONBLOCK`: non-blocking mode (same as NDELAY) - const NONBLOCK = 0x800; - /// `O_PATH`: open a file descriptor for path resolution only - const PATH = 0x200000; - /// `O_SYNC`: write operations on the file will complete according to the requirements of - /// synchronized I/O file integrity completion (by contrast with the synchronized I/O data - /// integrity completion provided by `O_DSYNC`.) - const SYNC = 0x101000; - /// `O_TMPFILE`: create an unnamed temporary file - #[cfg(target_arch = "x86_64")] - const TMPFILE = 0x410000; - #[cfg(target_arch = "aarch64")] - const TMPFILE = 0x404000; - /// `O_TRUNC`: truncate the file to zero length - const TRUNC = 0x200; - /// - const _ = !0; - - /// All file status flags + access modes - const STATUS_FLAGS_MASK = Self::APPEND.bits() - | Self::NONBLOCK.bits() - | Self::DSYNC.bits() - | Self::ASYNC.bits() - | Self::DIRECT.bits() - | Self::LARGEFILE.bits() - | Self::NOATIME.bits() - | Self::SYNC.bits() - | Self::PATH.bits() - | Self::RDONLY.bits() - | Self::WRONLY.bits() - | Self::RDWR.bits(); - } -} - -/// The `whence` directive to [`resolver::Resolver::seek`] -#[derive(Copy, Clone)] -pub enum SeekWhence { - /// The file offset is set to `offset` bytes. - RelativeToBeginning, - /// The file offset is set to its current location plus `offset` bytes. - RelativeToCurrentOffset, - /// The file offset is set to the size of the file plus `offset` bytes. - RelativeToEnd, -} - -/// The status of a file/directory/... on the file-system, inspired by `stat(3type)`. -/// -/// This is explicitly a non-exhaustive struct with public members. As LiteBox evolves, more -/// elements might be added to this struct, allowing file systems to provide richer information -/// about the status of files. However, users of LiteBox must not depend on the completeness or even -/// layout of this particular type. -#[non_exhaustive] -pub struct FileStatus { - /// File type - pub file_type: FileType, - /// Permissions for the file - pub mode: Mode, - /// Size of the file, in bytes. This value considered informative if this is a regular file. - pub size: usize, - /// Owner of the file - pub owner: UserInfo, - /// Information about this particular node - pub node_info: NodeInfo, - /// Block size for file system I/O - pub blksize: usize, -} - -/// User information -#[derive(Clone, Copy, Debug)] -pub struct UserInfo { - /// User ID for the owner - pub user: u16, - /// Group ID for the owner - pub group: u16, -} - -/// Device/Inode information -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -pub struct NodeInfo { - /// Device number - pub dev: usize, - /// Inode number - pub ino: usize, - /// Device that is being referred to (will be `Some(...)` only if special file) - pub rdev: Option, -} - -/// Directory entries returned by [`resolver::Resolver::read_dir`] -#[derive(Debug)] -#[non_exhaustive] -pub struct DirEntry { - pub name: alloc::string::String, - pub file_type: FileType, - pub ino_info: Option, -} - -impl UserInfo { - /// The root user - pub const ROOT: Self = Self { user: 0, group: 0 }; -} diff --git a/litebox/src/fs/resolver.rs b/litebox/src/fs/resolver.rs deleted file mode 100644 index 9657e9a7a..000000000 --- a/litebox/src/fs/resolver.rs +++ /dev/null @@ -1,654 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Guest filesystem facade backed directly by the broker-core filesystem engine. - -use alloc::string::{String, ToString}; -use alloc::sync::Arc; -use alloc::vec; -use alloc::vec::Vec; -use core::num::NonZeroUsize; - -use litebox_broker_core::fs as broker_fs; -use litebox_broker_core::fs::backend::DeviceIo; -use litebox_broker_core::fs::resolver::{Resolver as BrokerResolver, ResolverEntry}; - -use crate::path::Arg; -use crate::{LiteBox, fd::TypedFd, sync}; - -use super::errors::{ - ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError, - ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, -}; -use super::{DirEntry, FileStatus, Mode, NodeInfo, OFlags, SeekWhence, UserInfo}; - -/// The guest-facing filesystem entry point. -pub struct Resolver< - Platform: sync::RawSyncPrimitivesProvider, - Backend: broker_fs::backend::Backend + 'static, -> { - litebox: LiteBox, - engine: BrokerResolver, -} - -impl - Resolver -{ - /// Construct a new resolver over `backend`. - #[must_use] - pub fn new(litebox: &LiteBox, backend: Backend) -> Self { - Self { - litebox: litebox.clone(), - engine: BrokerResolver::new(backend), - } - } -} - -impl DeviceIo for LiteBox { - fn read_stdin(&self, output: &mut [u8]) -> Result { - LiteBox::read_stdio(self, output).map_err(|_| broker_fs::errors::ReadError::Io) - } - - fn write_stdio( - &self, - stream: litebox_broker_protocol::stdio::StdioOutputStream, - input: &[u8], - ) -> Result { - LiteBox::write_stdio(self, stream, input).map_err(|_| broker_fs::errors::WriteError::Io) - } - - fn fill_random(&self, output: &mut [u8]) -> Result<(), broker_fs::errors::ReadError> { - LiteBox::fill_random(self, output).map_err(|_| broker_fs::errors::ReadError::Io) - } -} - -/// Per-call resolution context. The user may hold and mutate this as they wish. -/// -/// This struct is deliberately cheap to clone. -// NOTE(jayb): I generally dislike getters/setters for fields of a data-like struct (e.g., see -// acting_user and set_acting_user here), but I'm putting these here since I am not yet convinced -// that we won't need more things in the context, nor am I convinced that we might not need the -// ability to lock down how contexts are made/used. In some sense, I am forcing some chokepoints -// here. In the future, we might flatten these out and just allow access to the fields directly. -#[derive(Clone, Debug)] -pub struct Context { - /// Current working directory. - cwd: Arc, - /// Effective user for permission checks. - user_info: UserInfo, -} - -impl Context { - /// The user that operations on this context act as. - #[must_use] - pub fn acting_user(&self) -> UserInfo { - self.user_info - } - - /// Set the user that operations on this context act as. - pub fn set_acting_user(&mut self, user: UserInfo) { - self.user_info = user; - } - - /// The current working directory. - #[must_use] - pub fn cwd(&self) -> &ResolvedPath { - &self.cwd - } - - /// Set the current working directory. - pub fn set_cwd(&mut self, cwd: ResolvedPath) { - self.cwd = Arc::new(cwd); - } - - /// A new default context, anchored at `/` for a non-root user. - pub fn new() -> Context { - Self { - cwd: Arc::new(ResolvedPath { components: vec![] }), - user_info: UserInfo { - user: 1000, - group: 1000, - }, - } - } - - /// Resolve `path` against the current context. - // XXX(jayb): if/when we support chroot, we might need to tweak this to not allow "escaping" - // outside the chrooted part. - // XXX(jayb): since we are migrating all resolution into the resolver, we probably don't need - // `Arg` anymore, so could get rid of it in the future. - pub fn resolve(&self, path: impl Arg) -> Result { - let mut components = if path.as_rust_str()?.starts_with('/') { - vec![] - } else { - self.cwd.components.clone() - }; - for component in path.components()? { - match component { - "" | "." => {} - ".." => { - let _ = components.pop(); - } - _ => { - components.push(component.into()); - } - } - } - Ok(ResolvedPath { components }) - } -} - -impl Default for Context { - fn default() -> Self { - Self::new() - } -} - -/// Absolute normalized path, must only be created from [`Context::resolve`]. -/// -/// Note that a resolved path does not imply that it exists within the file system, merely that it -/// is an absolute normalized path. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ResolvedPath { - // Note: an empty path is equivalent to `/`. - components: Vec, -} - -impl core::fmt::Display for ResolvedPath { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - for component in &self.components { - write!(f, "/{component}")?; - } - if self.components.is_empty() { - f.write_str("/")?; - } - Ok(()) - } -} - -impl - Resolver -{ - /// Opens a file. - /// - /// The `mode` is only significant when creating a file. - pub fn open( - &self, - context: &Context, - path: impl Arg, - flags: OFlags, - mode: Mode, - ) -> Result, OpenError> { - let path = context.resolve(path)?.to_string(); - let entry = self - .engine - .open( - broker_user_info(context.acting_user()), - &path, - broker_open_flags(flags), - broker_mode(mode), - ) - .map_err(guest_open_error)?; - Ok(self.litebox.descriptor_table_mut().insert(entry)) - } - - /// Close the file at `fd`. - /// - /// Future operations on the `fd` will start to return `ClosedFd` errors. - pub fn close(&self, fd: &TypedFd) -> Result<(), CloseError> { - let mut descriptors = self.litebox.descriptor_table_mut(); - let removed = descriptors.remove(fd); - drop(descriptors); - // Some backends might block while closing an fd, so release the descriptor-table lock - // before dropping the backend handle. - drop(removed); - Ok(()) - } - - /// Read from a file descriptor at `offset` into a buffer. - /// - /// If `offset` is None, the read will start at the current file offset and update the file - /// offset to the end of the read. - /// If `offset` is Some, the file offset is not changed. - /// - /// # Panics - /// - /// Panics if the updated file offset would overflow `usize`. - pub fn read( - &self, - fd: &TypedFd, - buf: &mut [u8], - offset: Option, - ) -> Result { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(ReadError::ClosedFd)?; - let mut entry = entry.get_entry_mut(); - // XXX(jayb): This deliberately preserves the current descriptor-entry lock across backend - // I/O. A later PR can introduce a smaller position/append serialization primitive. - self.engine - .read(&self.litebox, &mut entry.entry, buf, offset) - .map_err(guest_read_error) - } - - /// Write from a buffer to a file descriptor at `offset`. - /// - /// If `offset` is None, the write will start at the current file offset and update the file - /// offset to the end of the write. - /// If `offset` is Some, the file offset is not changed. - /// - /// # Panics - /// - /// Panics if the updated file offset would overflow `usize`. - pub fn write( - &self, - fd: &TypedFd, - buf: &[u8], - offset: Option, - ) -> Result { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(WriteError::ClosedFd)?; - let mut entry = entry.get_entry_mut(); - // XXX(jayb): This deliberately preserves the current descriptor-entry lock across backend - // I/O. A later PR can introduce a smaller position/append serialization primitive. - self.engine - .write(&self.litebox, &mut entry.entry, buf, offset) - .map_err(guest_write_error) - } - - /// Reposition the read/write file offset, by changing it to `offset` relative to `whence`. - /// - /// Returns the resulting offset (in bytes from start of file) on success. - pub fn seek( - &self, - fd: &TypedFd, - offset: isize, - whence: SeekWhence, - ) -> Result { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(SeekError::ClosedFd)?; - let mut entry = entry.get_entry_mut(); - self.engine - .seek(&mut entry.entry, offset, broker_seek_whence(whence)) - .map_err(guest_seek_error) - } - - /// Truncate the file to the specified length. - /// - /// If shorter than existing size, extra data is lost. If longer than existing size, resize by - /// adding `\0`s. - /// - /// If `reset_offset` is true, the offset is reset to zero; otherwise, it remains unchanged. - pub fn truncate( - &self, - fd: &TypedFd, - length: usize, - reset_offset: bool, - ) -> Result<(), TruncateError> { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(TruncateError::ClosedFd)?; - let mut entry = entry.get_entry_mut(); - self.engine - .truncate(&mut entry.entry, length, reset_offset) - .map_err(guest_truncate_error) - } - - /// Change the permissions of a file. - pub fn chmod(&self, context: &Context, path: impl Arg, mode: Mode) -> Result<(), ChmodError> { - let path = context.resolve(path)?.to_string(); - self.engine - .chmod( - broker_user_info(context.acting_user()), - &path, - broker_mode(mode), - ) - .map_err(guest_chmod_error) - } - - /// Change the owner of a file. - pub fn chown( - &self, - context: &Context, - path: impl Arg, - user: Option, - group: Option, - ) -> Result<(), ChownError> { - let path = context.resolve(path)?.to_string(); - self.engine - .chown(broker_user_info(context.acting_user()), &path, user, group) - .map_err(guest_chown_error) - } - - /// Unlink a file. - pub fn unlink(&self, context: &Context, path: impl Arg) -> Result<(), UnlinkError> { - let path = context.resolve(path)?.to_string(); - self.engine - .unlink(broker_user_info(context.acting_user()), &path) - .map_err(guest_unlink_error) - } - - /// Create a new directory. - pub fn mkdir(&self, context: &Context, path: impl Arg, mode: Mode) -> Result<(), MkdirError> { - let path = context.resolve(path)?.to_string(); - self.engine - .mkdir( - broker_user_info(context.acting_user()), - &path, - broker_mode(mode), - ) - .map_err(guest_mkdir_error) - } - - /// Remove a directory. - pub fn rmdir(&self, context: &Context, path: impl Arg) -> Result<(), RmdirError> { - let path = context.resolve(path)?.to_string(); - self.engine - .rmdir(broker_user_info(context.acting_user()), &path) - .map_err(guest_rmdir_error) - } - - /// Read directory entries from a directory file descriptor. - /// - /// Returns a list of file/directory names including synthesized `.` and `..` entries. - pub fn read_dir(&self, fd: &TypedFd) -> Result, ReadDirError> { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(ReadDirError::ClosedFd)?; - let entry = entry.get_entry(); - self.engine - .read_dir(&entry.entry) - .map_err(guest_read_dir_error) - .and_then(guest_directory_entries) - } - - /// Obtain the status of a path. - pub fn file_status( - &self, - context: &Context, - path: impl Arg, - ) -> Result { - let path = context.resolve(path)?.to_string(); - self.engine - .file_status(broker_user_info(context.acting_user()), &path) - .map_err(guest_file_status_error) - .and_then(guest_file_status) - } - - /// Equivalent to [`Self::file_status`], but on an open `fd`. - pub fn fd_file_status(&self, fd: &TypedFd) -> Result { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(FileStatusError::ClosedFd)?; - let entry = entry.get_entry(); - self.engine - .handle_status(&entry.entry) - .map_err(guest_file_status_error) - .and_then(guest_file_status) - } - - /// Get static backing data for a file, if available and supported. - /// - /// This method returns the (entire) underlying static byte slice if the file's contents are - /// backed by borrowed static data (e.g., set up via [`super::in_mem::InitialNode::File`]). - /// - /// Returns `None` if no static backing data is available/supported. - pub fn get_static_backing_data(&self, fd: &TypedFd) -> Option<&'static [u8]> { - let entry = self.litebox.descriptor_table().entry_handle(fd)?; - let entry = entry.get_entry(); - self.engine.get_static_backing_data(&entry.entry) - } -} - -// TODO: Remove most of the guest/core conversion helpers below once LiteBox uses the broker file -// APIs exclusively. They temporarily preserve LiteBox's guest-facing types while this facade calls -// the broker-core engine directly. -fn broker_mode(mode: Mode) -> broker_fs::Mode { - broker_fs::Mode::from_u32_bits_truncate(mode.bits()) -} - -fn guest_mode(mode: broker_fs::Mode) -> Mode { - Mode::from_bits_retain(mode.bits().into()) -} - -fn broker_open_flags(flags: OFlags) -> broker_fs::OFlags { - broker_fs::OFlags::from_bits_retain(flags.bits()) -} - -fn broker_user_info(user: UserInfo) -> broker_fs::UserInfo { - broker_fs::UserInfo { - user: user.user, - group: user.group, - } -} - -fn guest_user_info(user: broker_fs::UserInfo) -> UserInfo { - UserInfo { - user: user.user, - group: user.group, - } -} - -fn broker_seek_whence(whence: SeekWhence) -> broker_fs::SeekWhence { - match whence { - SeekWhence::RelativeToBeginning => broker_fs::SeekWhence::RelativeToBeginning, - SeekWhence::RelativeToCurrentOffset => broker_fs::SeekWhence::RelativeToCurrentOffset, - SeekWhence::RelativeToEnd => broker_fs::SeekWhence::RelativeToEnd, - } -} - -/// Narrows protocol node identity to LiteBox's guest-facing pointer-sized widths. -/// -/// Returns `None` when a value does not fit, which only happens on hosts whose `usize` is narrower -/// than the protocol's 64-bit identity fields. -fn guest_node_info(node: broker_fs::NodeInfo) -> Option { - Some(NodeInfo { - dev: usize::try_from(node.dev).ok()?, - ino: usize::try_from(node.ino).ok()?, - rdev: node.rdev.map(NonZeroUsize::try_from).transpose().ok()?, - }) -} - -fn guest_file_status(status: broker_fs::FileStatus) -> Result { - Ok(FileStatus { - file_type: status.file_type, - mode: guest_mode(status.mode), - size: usize::try_from(status.size).map_err(|_| FileStatusError::Io)?, - owner: guest_user_info(status.owner), - node_info: guest_node_info(status.node_info).ok_or(FileStatusError::Io)?, - blksize: usize::try_from(status.blksize).map_err(|_| FileStatusError::Io)?, - }) -} - -fn guest_directory_entries( - entries: Vec, -) -> Result, ReadDirError> { - entries - .into_iter() - .map(|entry| { - let ino_info = match entry.ino_info { - Some(node) => Some(guest_node_info(node).ok_or(ReadDirError::Io)?), - None => None, - }; - Ok(DirEntry { - name: entry.name, - file_type: entry.file_type, - ino_info, - }) - }) - .collect() -} - -fn guest_path_error(error: broker_fs::errors::PathError) -> PathError { - match error { - broker_fs::errors::PathError::NoSuchFileOrDirectory => PathError::NoSuchFileOrDirectory, - broker_fs::errors::PathError::NoSearchPerms { - #[cfg(debug_assertions)] - dir, - #[cfg(debug_assertions)] - perms, - } => PathError::NoSearchPerms { - #[cfg(debug_assertions)] - dir, - #[cfg(debug_assertions)] - perms: guest_mode(perms), - }, - broker_fs::errors::PathError::InvalidPathname => PathError::InvalidPathname, - broker_fs::errors::PathError::MissingComponent => PathError::MissingComponent, - broker_fs::errors::PathError::ComponentNotADirectory => PathError::ComponentNotADirectory, - } -} - -fn guest_open_error(error: broker_fs::errors::OpenError) -> OpenError { - match error { - broker_fs::errors::OpenError::AccessNotAllowed => OpenError::AccessNotAllowed, - broker_fs::errors::OpenError::NoWritePerms => OpenError::NoWritePerms, - broker_fs::errors::OpenError::ReadOnlyFileSystem => OpenError::ReadOnlyFileSystem, - broker_fs::errors::OpenError::AlreadyExists => OpenError::AlreadyExists, - broker_fs::errors::OpenError::TruncateError(error) => { - OpenError::TruncateError(guest_truncate_error(error)) - } - broker_fs::errors::OpenError::Io => OpenError::Io, - broker_fs::errors::OpenError::PathError(error) => { - OpenError::PathError(guest_path_error(error)) - } - } -} - -fn guest_read_error(error: broker_fs::errors::ReadError) -> ReadError { - match error { - broker_fs::errors::ReadError::ClosedFd => ReadError::ClosedFd, - broker_fs::errors::ReadError::NotAFile => ReadError::NotAFile, - broker_fs::errors::ReadError::NotForReading => ReadError::NotForReading, - broker_fs::errors::ReadError::Io => ReadError::Io, - } -} - -fn guest_write_error(error: broker_fs::errors::WriteError) -> WriteError { - match error { - broker_fs::errors::WriteError::ClosedFd => WriteError::ClosedFd, - broker_fs::errors::WriteError::NotAFile => WriteError::NotAFile, - broker_fs::errors::WriteError::NotForWriting => WriteError::NotForWriting, - broker_fs::errors::WriteError::Io => WriteError::Io, - } -} - -fn guest_seek_error(error: broker_fs::errors::SeekError) -> SeekError { - match error { - broker_fs::errors::SeekError::ClosedFd => SeekError::ClosedFd, - broker_fs::errors::SeekError::NotAFile => SeekError::NotAFile, - broker_fs::errors::SeekError::InvalidOffset => SeekError::InvalidOffset, - broker_fs::errors::SeekError::NonSeekable => SeekError::NonSeekable, - broker_fs::errors::SeekError::Io => SeekError::Io, - } -} - -fn guest_truncate_error(error: broker_fs::errors::TruncateError) -> TruncateError { - match error { - broker_fs::errors::TruncateError::ClosedFd => TruncateError::ClosedFd, - broker_fs::errors::TruncateError::IsDirectory => TruncateError::IsDirectory, - broker_fs::errors::TruncateError::NotForWriting => TruncateError::NotForWriting, - broker_fs::errors::TruncateError::IsTerminalDevice => TruncateError::IsTerminalDevice, - broker_fs::errors::TruncateError::Io => TruncateError::Io, - } -} - -fn guest_chmod_error(error: broker_fs::errors::ChmodError) -> ChmodError { - match error { - broker_fs::errors::ChmodError::NotTheOwner => ChmodError::NotTheOwner, - broker_fs::errors::ChmodError::ReadOnlyFileSystem => ChmodError::ReadOnlyFileSystem, - broker_fs::errors::ChmodError::Io => ChmodError::Io, - broker_fs::errors::ChmodError::PathError(error) => { - ChmodError::PathError(guest_path_error(error)) - } - } -} - -fn guest_chown_error(error: broker_fs::errors::ChownError) -> ChownError { - match error { - broker_fs::errors::ChownError::NotTheOwner => ChownError::NotTheOwner, - broker_fs::errors::ChownError::ReadOnlyFileSystem => ChownError::ReadOnlyFileSystem, - broker_fs::errors::ChownError::Io => ChownError::Io, - broker_fs::errors::ChownError::PathError(error) => { - ChownError::PathError(guest_path_error(error)) - } - } -} - -fn guest_unlink_error(error: broker_fs::errors::UnlinkError) -> UnlinkError { - match error { - broker_fs::errors::UnlinkError::NoWritePerms => UnlinkError::NoWritePerms, - broker_fs::errors::UnlinkError::IsADirectory => UnlinkError::IsADirectory, - broker_fs::errors::UnlinkError::ReadOnlyFileSystem => UnlinkError::ReadOnlyFileSystem, - broker_fs::errors::UnlinkError::Io => UnlinkError::Io, - broker_fs::errors::UnlinkError::PathError(error) => { - UnlinkError::PathError(guest_path_error(error)) - } - } -} - -fn guest_mkdir_error(error: broker_fs::errors::MkdirError) -> MkdirError { - match error { - broker_fs::errors::MkdirError::NoWritePerms => MkdirError::NoWritePerms, - broker_fs::errors::MkdirError::AlreadyExists => MkdirError::AlreadyExists, - broker_fs::errors::MkdirError::ReadOnlyFileSystem => MkdirError::ReadOnlyFileSystem, - broker_fs::errors::MkdirError::Io => MkdirError::Io, - broker_fs::errors::MkdirError::PathError(error) => { - MkdirError::PathError(guest_path_error(error)) - } - } -} - -fn guest_rmdir_error(error: broker_fs::errors::RmdirError) -> RmdirError { - match error { - broker_fs::errors::RmdirError::NoWritePerms => RmdirError::NoWritePerms, - broker_fs::errors::RmdirError::Busy => RmdirError::Busy, - broker_fs::errors::RmdirError::NotEmpty => RmdirError::NotEmpty, - broker_fs::errors::RmdirError::NotADirectory => RmdirError::NotADirectory, - broker_fs::errors::RmdirError::ReadOnlyFileSystem => RmdirError::ReadOnlyFileSystem, - broker_fs::errors::RmdirError::Io => RmdirError::Io, - broker_fs::errors::RmdirError::PathError(error) => { - RmdirError::PathError(guest_path_error(error)) - } - } -} - -fn guest_read_dir_error(error: broker_fs::errors::ReadDirError) -> ReadDirError { - match error { - broker_fs::errors::ReadDirError::ClosedFd => ReadDirError::ClosedFd, - broker_fs::errors::ReadDirError::NotADirectory => ReadDirError::NotADirectory, - broker_fs::errors::ReadDirError::Io => ReadDirError::Io, - } -} - -fn guest_file_status_error(error: broker_fs::errors::FileStatusError) -> FileStatusError { - match error { - broker_fs::errors::FileStatusError::ClosedFd => FileStatusError::ClosedFd, - broker_fs::errors::FileStatusError::Io => FileStatusError::Io, - broker_fs::errors::FileStatusError::PathError(error) => { - FileStatusError::PathError(guest_path_error(error)) - } - } -} - -crate::fd::enable_fds_for_subsystem! { - @ Platform: { sync::RawSyncPrimitivesProvider }, Backend: { broker_fs::backend::Backend + 'static }; - Resolver; - @ Backend: { broker_fs::backend::Backend + 'static }; - ResolverEntry; - -> ResolverFd; -} diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs deleted file mode 100644 index d3e473027..000000000 --- a/litebox/src/fs/tests.rs +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Smoke coverage for the compatibility facade. Backend semantics live in broker-core tests. - -use crate::LiteBox; -use crate::fs::errors::{OpenError, ReadError, WriteError}; -use crate::fs::in_mem::{InMem, InitialNode}; -use crate::fs::resolver::{Context, Resolver}; -use crate::fs::{FileType, Mode, OFlags, SeekWhence, UserInfo}; -use crate::platform::mock::MockPlatform; - -fn facade_fs(litebox: &LiteBox) -> Resolver> { - Resolver::new( - litebox, - InMem::new_initialized([ - ( - "/", - InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: UserInfo::ROOT, - }, - ), - ( - "/seed", - InitialNode::File { - mode: Mode::RUSR | Mode::RGRP | Mode::ROTH, - owner: UserInfo { - user: 123, - group: 456, - }, - data: b"seed data".as_slice().into(), - }, - ), - ]), - ) -} - -#[test] -fn context_resolves_relative_paths_and_forwards_credentials() { - let litebox = LiteBox::new(MockPlatform::new()); - let fs = facade_fs(&litebox); - let mut ctx = Context::new(); - fs.mkdir(&ctx, "/work", Mode::RWXU | Mode::XOTH).unwrap(); - ctx.set_cwd(ctx.resolve("/work").unwrap()); - - let fd = fs - .open(&ctx, "./file", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .unwrap(); - fs.write(&fd, b"data", None).unwrap(); - fs.close(&fd).unwrap(); - - let status = fs.file_status(&ctx, "../work/file").unwrap(); - assert_eq!(status.file_type, FileType::RegularFile); - assert_eq!(status.mode, Mode::RWXU); - assert_eq!(status.owner.user, ctx.acting_user().user); - assert_eq!(status.owner.group, ctx.acting_user().group); - assert_eq!(status.size, 4); - - fs.chmod(&ctx, "file", Mode::WUSR).unwrap(); - assert!(matches!( - fs.open(&ctx, "file", OFlags::RDONLY, Mode::empty()), - Err(OpenError::AccessNotAllowed) - )); - ctx.set_acting_user(UserInfo::ROOT); - fs.chown(&ctx, "file", Some(321), Some(654)).unwrap(); - let status = fs.file_status(&ctx, "/work/file").unwrap(); - assert_eq!(status.owner.user, 321); - assert_eq!(status.owner.group, 654); - assert_eq!(status.mode, Mode::WUSR); -} - -#[test] -fn descriptors_share_position_and_report_closed_fd_errors() { - let litebox = LiteBox::new(MockPlatform::new()); - let fs = facade_fs(&litebox); - let ctx = Context::new(); - let fd = fs - .open(&ctx, "/file", OFlags::CREAT | OFlags::RDWR, Mode::RWXU) - .unwrap(); - fs.write(&fd, b"abcdef", None).unwrap(); - fs.seek(&fd, 0, SeekWhence::RelativeToBeginning).unwrap(); - let duplicate = litebox.descriptor_table_mut().duplicate(&fd).unwrap(); - - let mut buffer = [0; 3]; - assert_eq!(fs.read(&fd, &mut buffer, None).unwrap(), 3); - assert_eq!(&buffer, b"abc"); - assert_eq!(fs.read(&duplicate, &mut buffer, None).unwrap(), 3); - assert_eq!(&buffer, b"def"); - fs.close(&fd).unwrap(); - assert!(matches!( - fs.read(&fd, &mut buffer, None), - Err(ReadError::ClosedFd) - )); - assert!(matches!( - fs.write(&fd, b"x", None), - Err(WriteError::ClosedFd) - )); - fs.truncate(&duplicate, 2, true).unwrap(); - assert_eq!(fs.fd_file_status(&duplicate).unwrap().size, 2); - assert_eq!(fs.read(&duplicate, &mut buffer, None).unwrap(), 2); - assert_eq!(&buffer[..2], b"ab"); - fs.close(&duplicate).unwrap(); -} - -#[test] -fn initial_nodes_and_directory_entries_preserve_guest_values() { - let litebox = LiteBox::new(MockPlatform::new()); - let fs = facade_fs(&litebox); - let ctx = Context::new(); - let fd = fs - .open(&ctx, "/seed", OFlags::RDONLY, Mode::empty()) - .unwrap(); - assert_eq!( - fs.get_static_backing_data(&fd), - Some(b"seed data".as_slice()) - ); - let status = fs.fd_file_status(&fd).unwrap(); - assert_eq!(status.file_type, FileType::RegularFile); - assert_eq!(status.mode, Mode::RUSR | Mode::RGRP | Mode::ROTH); - assert_eq!(status.owner.user, 123); - assert_eq!(status.owner.group, 456); - assert_eq!(status.size, 9); - fs.close(&fd).unwrap(); - - let directory = fs - .open(&ctx, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) - .unwrap(); - let entries = fs.read_dir(&directory).unwrap(); - let entry = entries.iter().find(|entry| entry.name == "seed").unwrap(); - assert_eq!(entry.file_type, FileType::RegularFile); - assert_eq!(entry.ino_info, Some(status.node_info)); - fs.close(&directory).unwrap(); -} - -mod stdio { - use crate::LiteBox; - use crate::fs::devices::Devices; - use crate::fs::errors::{ReadError, WriteError}; - use crate::fs::resolver::Resolver; - use crate::fs::{Mode, OFlags}; - use crate::platform::mock::MockPlatform; - use alloc::vec; - extern crate std; - - #[test] - fn stdio_requires_broker() { - let ctx = crate::fs::resolver::Context::new(); - let platform = MockPlatform::new(); - let litebox = LiteBox::new(platform); - let fs = Resolver::new( - &litebox, - crate::fs::composer::Composer::builder() - .mount("/dev", Devices::new) - .build() - .unwrap(), - ); - - let fd_stdout = fs - .open(&ctx, "/dev/stdout", OFlags::WRONLY, Mode::empty()) - .expect("Failed to open /dev/stdout"); - assert!(matches!(fs.write(&fd_stdout, b"", None), Ok(0))); - assert!(matches!( - fs.write(&fd_stdout, b"Hello, stdout!", None), - Err(WriteError::Io) - )); - fs.close(&fd_stdout).expect("Failed to close /dev/stdout"); - - let fd_stderr = fs - .open(&ctx, "/dev/stderr", OFlags::WRONLY, Mode::empty()) - .expect("Failed to open /dev/stderr"); - assert!(matches!(fs.write(&fd_stderr, b"", None), Ok(0))); - assert!(matches!( - fs.write(&fd_stderr, b"Hello, stderr!", None), - Err(WriteError::Io) - )); - fs.close(&fd_stderr).expect("Failed to close /dev/stderr"); - - let fd_stdin = fs - .open(&ctx, "/dev/stdin", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open /dev/stdin"); - assert!(matches!(fs.read(&fd_stdin, &mut [], None), Ok(0))); - let mut buffer = vec![0; 13]; - assert!(matches!( - fs.read(&fd_stdin, &mut buffer, None), - Err(ReadError::Io) - )); - fs.close(&fd_stdin).expect("Failed to close /dev/stdin"); - } - - #[test] - fn non_dev_path_fails() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = Resolver::new( - &litebox, - crate::fs::composer::Composer::builder() - .mount("/dev", Devices::new) - .build() - .unwrap(), - ); - - // Attempt to open a non-/dev/* path - let result = fs.open(&ctx, "foo", OFlags::RDONLY, Mode::empty()); - assert!(matches!( - result, - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )) - )); - } -} - -mod composed_stdio { - use crate::LiteBox; - use crate::fs::composer::Composer; - use crate::fs::devices::Devices; - use crate::fs::errors::{ReadError, WriteError}; - use crate::fs::in_mem::{InMem, InitialNode}; - use crate::fs::resolver::Resolver; - use crate::fs::{Mode, OFlags, UserInfo}; - use crate::platform::mock::MockPlatform; - use alloc::vec; - extern crate std; - - type ComposedFs = Resolver; - - fn composed_fs(litebox: &LiteBox) -> ComposedFs { - Resolver::new( - litebox, - Composer::builder() - .mount("/", |_| { - InMem::::new_initialized([( - "/", - InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: UserInfo::ROOT, - }, - )]) - }) - .mount("/dev", Devices::new) - .build() - .unwrap(), - ) - } - - #[test] - fn stdio_requires_broker() { - let ctx = crate::fs::resolver::Context::new(); - let platform = MockPlatform::new(); - let litebox = LiteBox::new(platform); - let fs = composed_fs(&litebox); - - let fd_stdout = fs - .open(&ctx, "/dev/stdout", OFlags::WRONLY, Mode::empty()) - .expect("Failed to open /dev/stdout"); - assert!(matches!(fs.write(&fd_stdout, b"", None), Ok(0))); - assert!(matches!( - fs.write(&fd_stdout, b"Hello, composed stdout!", None), - Err(WriteError::Io) - )); - fs.close(&fd_stdout).expect("Failed to close /dev/stdout"); - - let fd_stderr = fs - .open(&ctx, "/dev/stderr", OFlags::WRONLY, Mode::empty()) - .expect("Failed to open /dev/stderr"); - assert!(matches!(fs.write(&fd_stderr, b"", None), Ok(0))); - assert!(matches!( - fs.write(&fd_stderr, b"Hello, composed stderr!", None), - Err(WriteError::Io) - )); - fs.close(&fd_stderr).expect("Failed to close /dev/stderr"); - - let fd_stdin = fs - .open(&ctx, "/dev/stdin", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open /dev/stdin"); - assert!(matches!(fs.read(&fd_stdin, &mut [], None), Ok(0))); - let mut buffer = vec![0; 1024]; - assert!(matches!( - fs.read(&fd_stdin, &mut buffer, None), - Err(ReadError::Io) - )); - fs.close(&fd_stdin).expect("Failed to close /dev/stdin"); - } - - #[test] - fn write_to_non_dev() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = composed_fs(&litebox); - - // Test file creation - let path = "/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - - fs.close(&fd).expect("Failed to close file"); - - // Test file deletion - fs.unlink(&ctx, path).expect("Failed to unlink file"); - assert!( - fs.open(&ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), - "File should not exist" - ); - } -} diff --git a/litebox_broker_core/src/fs/nine_p/tests.rs b/litebox_broker_core/src/fs/nine_p/tests.rs index bad06abf5..3c11b82da 100644 --- a/litebox_broker_core/src/fs/nine_p/tests.rs +++ b/litebox_broker_core/src/fs/nine_p/tests.rs @@ -1,7 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! 9P engine tests using a real `diod` server and fault-injected transports. +//! 9P filesystem semantics, exercised against a real `diod` server. +//! +//! These tests drive the broker-core resolver over the [`NineP`] backend, so they cover the +//! client's protocol handling and the resolver semantics layered on it. They need `diod` +//! installed (`apt install diod`). use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::io::{Read as _, Write as _}; @@ -15,11 +19,12 @@ use crate::fs::errors::{ }; use crate::fs::inode_allocator::InodeAllocator; use crate::fs::resolver::Resolver; -use crate::fs::{Mode, OFlags}; +use crate::fs::{FileType, Mode, OFlags, SeekWhence}; use crate::test_platform::TestPlatform; use super::{NineP, transport}; +/// A resolver over a 9P backend reached through `T`. type NinePFs = Resolver>; const USER: crate::fs::UserInfo = crate::fs::UserInfo { @@ -46,7 +51,7 @@ fn attach( .expect("failed to create 9P filesystem") } -/// A wrapper around `TcpStream` that implements the broker-core 9P transport traits. +/// A wrapper around `TcpStream` that implements the 9P transport traits. struct TcpTransport { stream: TcpStream, } @@ -211,15 +216,13 @@ fn connect_9p(server: &DiodServer) -> NinePFs { #[test] fn test_nine_p_create_and_read_file() { - let user = USER; - let server = DiodServer::start(); let fs = connect_9p(&server); // Create a file and write to it let mut fd = fs .open( - user, + USER, "/hello.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -242,7 +245,7 @@ fn test_nine_p_create_and_read_file() { // Read the file back through 9P let mut fd = fs - .open(user, "/hello.txt", OFlags::RDONLY, Mode::empty()) + .open(USER, "/hello.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open file for reading via 9P"); let mut buf = alloc::vec![0u8; 256]; @@ -256,21 +259,19 @@ fn test_nine_p_create_and_read_file() { #[test] fn test_nine_p_mkdir_and_readdir() { - let user = USER; - let server = DiodServer::start(); let fs = connect_9p(&server); // Create directories - fs.mkdir(user, "/subdir", Mode::RWXU) + fs.mkdir(USER, "/subdir", Mode::RWXU) .expect("failed to mkdir via 9P"); - fs.mkdir(user, "/subdir/nested", Mode::RWXU) + fs.mkdir(USER, "/subdir/nested", Mode::RWXU) .expect("failed to mkdir nested via 9P"); // Create a file inside the subdirectory let mut fd = fs .open( - user, + USER, "/subdir/file.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -282,7 +283,7 @@ fn test_nine_p_mkdir_and_readdir() { // Read the root directory let fd = fs - .open(user, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open(USER, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) .expect("failed to open root dir"); let entries = fs.read_dir(&fd).expect("failed to readdir root"); drop(fd); @@ -296,7 +297,7 @@ fn test_nine_p_mkdir_and_readdir() { // Read the subdirectory let fd = fs .open( - user, + USER, "/subdir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), @@ -318,15 +319,13 @@ fn test_nine_p_mkdir_and_readdir() { #[test] fn test_nine_p_unlink_and_rmdir() { - let user = USER; - let server = DiodServer::start(); let fs = connect_9p(&server); // Create a file, then delete it let fd = fs .open( - user, + USER, "/to_delete.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -334,20 +333,20 @@ fn test_nine_p_unlink_and_rmdir() { .expect("failed to create file"); drop(fd); - fs.unlink(user, "/to_delete.txt") + fs.unlink(USER, "/to_delete.txt") .expect("failed to unlink file via 9P"); // Verify the file is gone assert!( - fs.open(user, "/to_delete.txt", OFlags::RDONLY, Mode::empty()) + fs.open(USER, "/to_delete.txt", OFlags::RDONLY, Mode::empty()) .is_err(), "file should no longer exist" ); // Create a directory, then remove it - fs.mkdir(user, "/to_remove", Mode::RWXU) + fs.mkdir(USER, "/to_remove", Mode::RWXU) .expect("failed to mkdir"); - fs.rmdir(user, "/to_remove") + fs.rmdir(USER, "/to_remove") .expect("failed to rmdir via 9P"); // Verify the directory is gone on the host @@ -359,15 +358,13 @@ fn test_nine_p_unlink_and_rmdir() { #[test] fn test_nine_p_file_status() { - let user = USER; - let server = DiodServer::start(); let fs = connect_9p(&server); // Create a file with known content let mut fd = fs .open( - user, + USER, "/status_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -379,38 +376,36 @@ fn test_nine_p_file_status() { // Check file_status via path let status = fs - .file_status(user, "/status_test.txt") + .file_status(USER, "/status_test.txt") .expect("failed to stat file"); assert_eq!( status.file_type, - crate::fs::FileType::RegularFile, + FileType::RegularFile, "should be a regular file" ); assert_eq!(status.size, 10, "file size should be 10 bytes"); // Check directory status - fs.mkdir(user, "/stat_dir", Mode::RWXU).unwrap(); + fs.mkdir(USER, "/stat_dir", Mode::RWXU).unwrap(); let status = fs - .file_status(user, "/stat_dir") + .file_status(USER, "/stat_dir") .expect("failed to stat dir"); assert_eq!( status.file_type, - crate::fs::FileType::Directory, + FileType::Directory, "should be a directory" ); } #[test] fn test_nine_p_seek_and_partial_read() { - let user = USER; - let server = DiodServer::start(); let fs = connect_9p(&server); // Write a file with known content let mut fd = fs .open( - user, + USER, "/seek_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -421,12 +416,12 @@ fn test_nine_p_seek_and_partial_read() { // Open for reading and seek let mut fd = fs - .open(user, "/seek_test.txt", OFlags::RDONLY, Mode::empty()) + .open(USER, "/seek_test.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open file for reading"); // Seek to offset 5 let pos = fs - .seek(&mut fd, 5, crate::fs::SeekWhence::RelativeToBeginning) + .seek(&mut fd, 5, SeekWhence::RelativeToBeginning) .expect("failed to seek"); assert_eq!(pos, 5); @@ -442,15 +437,13 @@ fn test_nine_p_seek_and_partial_read() { #[test] fn test_nine_p_truncate() { - let user = USER; - let server = DiodServer::start(); let fs = connect_9p(&server); // Write a file let mut fd = fs .open( - user, + USER, "/trunc_test.txt", OFlags::CREAT | OFlags::RDWR, Mode::RWXU, @@ -471,8 +464,6 @@ fn test_nine_p_truncate() { #[test] fn test_nine_p_host_files_visible() { - let user = USER; - let server = DiodServer::start(); // Pre-populate some files on the host side @@ -488,7 +479,7 @@ fn test_nine_p_host_files_visible() { // Read file created on the host through 9P let mut fd = fs - .open(user, "/host_file.txt", OFlags::RDONLY, Mode::empty()) + .open(USER, "/host_file.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open host file via 9P"); let mut buf = alloc::vec![0u8; 256]; let n = fs.read(&NoDeviceIo, &mut fd, &mut buf, None).unwrap(); @@ -498,7 +489,7 @@ fn test_nine_p_host_files_visible() { // List host directory through 9P let fd = fs .open( - user, + USER, "/host_dir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), @@ -584,54 +575,35 @@ fn connect_9p_broken(server: &DiodServer, allowed_writes: usize) -> NinePFs) -> Resolver { - Resolver::new(TarRo::new(tar_data, InodeAllocator::standalone())) -} +struct UnservicedStdio; -type InMemFs = Resolver>; +impl DeviceIo for UnservicedStdio { + fn read_stdin(&self, output: &mut [u8]) -> Result { + if output.is_empty() { + return Ok(0); + } + Err(ReadError::Io) + } -fn in_mem_fs() -> InMemFs { - Resolver::new(InMem::new(InodeAllocator::standalone())) -} + fn write_stdio(&self, _stream: StdioOutputStream, input: &[u8]) -> Result { + if input.is_empty() { + return Ok(0); + } + Err(WriteError::Io) + } -/// Run `f` with the acting user set to root. -fn with_root_privileges( - fs: &Resolver, - f: impl FnOnce(&Resolver, UserInfo), -) { - f(fs, UserInfo::ROOT); + fn fill_random(&self, _output: &mut [u8]) -> Result<(), ReadError> { + Err(ReadError::Io) + } } -/// Run `f` with the acting user set to `user`/`group`, so that tests can exercise operations -/// whose outcome depends on the acting user. -fn with_user( - fs: &Resolver, - user: u16, - group: u16, - f: impl FnOnce(&Resolver, UserInfo), -) { - f(fs, UserInfo { user, group }); +fn in_mem_fs() -> Resolver> { + Resolver::new(InMem::::new(InodeAllocator::standalone())) } -type OverlayFs = Resolver>; +fn tar_ro_fs(tar_data: Cow<'static, [u8]>) -> Resolver { + Resolver::new(TarRo::new(tar_data, InodeAllocator::standalone())) +} /// An overlay of `upper` over a tar-backed lower layer. -fn overlay_fs(upper: InMem, tar_data: Cow<'static, [u8]>) -> OverlayFs { - Resolver::new(Overlay::new( +fn overlay_fs( + upper: InMem, + tar_data: Cow<'static, [u8]>, +) -> Resolver> { + Resolver::new(Overlay::::new( upper, TarRo::new(tar_data, InodeAllocator::standalone()), InodeAllocator::standalone(), @@ -60,75 +71,85 @@ fn overlay_fs(upper: InMem, tar_data: Cow<'static, [u8]>) -> Overl } mod in_mem { - use super::USER; - use crate::fs::backend::NoDeviceIo; - use crate::fs::{Mode, OFlags}; + use super::{ + FileType, InMem, Mode, NoDeviceIo, OFlags, ROOT, Resolver, ResolverEntry, SeekWhence, + TestPlatform, USER, UserInfo, in_mem_fs, + }; + use crate::fs::errors::{ + ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, + UnlinkError, + }; use alloc::vec; use alloc::vec::Vec; - extern crate std; - use super::{with_root_privileges, with_user}; + type InMemFs = Resolver>; + type InMemEntry = ResolverEntry>; + + /// Create `/tmp` as root, so that the unprivileged user can create entries in it. + fn world_writable_tmp(fs: &InMemFs) { + fs.mkdir(ROOT, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to create /tmp"); + } + + /// Make the root directory world-writable, so tests can create entries directly in it. + fn world_writable_root(fs: &InMemFs) { + fs.chmod(ROOT, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to chmod /"); + } #[test] fn root_file_creation_and_deletion() { - with_root_privileges(&super::in_mem_fs(), |fs, user| { - // Test file creation - let path = "/testfile"; - let fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); + let fs = in_mem_fs(); - drop(fd); + // Test file creation + let path = "/testfile"; + let fd = fs + .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + drop(fd); - // Test file deletion - fs.unlink(user, path).expect("Failed to unlink file"); - assert!( - fs.open(user, path, OFlags::RDONLY, Mode::RWXU).is_err(), - "File should not exist" - ); - }); + // Test file deletion + fs.unlink(ROOT, path).expect("Failed to unlink file"); + assert!( + fs.open(ROOT, path, OFlags::RDONLY, Mode::RWXU).is_err(), + "File should not exist" + ); } #[test] fn root_file_read_write() { - with_root_privileges(&super::in_mem_fs(), |fs, user| { - // Create and write to a file - let path = "/testfile"; - let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - let data = b"Hello, world!"; - fs.write(&NoDeviceIo, &mut fd, data, None) - .expect("Failed to write to file"); - drop(fd); + let fs = in_mem_fs(); - // Read from the file - let mut fd = fs - .open(user, path, OFlags::RDONLY, Mode::RWXU) - .expect("Failed to open file"); - let mut buffer = vec![0; data.len()]; - let bytes_read = fs - .read(&NoDeviceIo, &mut fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, data.len()); - assert_eq!(&buffer, data); - drop(fd); - }); + // Create and write to a file + let path = "/testfile"; + let mut fd = fs + .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + let data = b"Hello, world!"; + fs.write(&NoDeviceIo, &mut fd, data, None) + .expect("Failed to write to file"); + drop(fd); + + // Read from the file + let mut fd = fs + .open(ROOT, path, OFlags::RDONLY, Mode::RWXU) + .expect("Failed to open file"); + let mut buffer = vec![0; data.len()]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(bytes_read, data.len()); + assert_eq!(&buffer, data); } #[test] fn write_only_open_does_not_require_read_permission() { - let user = USER; - - let fs = super::in_mem_fs(); - with_root_privileges(&fs, |fs, user| { - fs.mkdir(user, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); + let fs = in_mem_fs(); + world_writable_tmp(&fs); let path = "/tmp/write_only"; let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) .expect("Failed to create write-only file"); fs.write(&NoDeviceIo, &mut fd, b"x", None) .expect("Failed to write file"); @@ -136,101 +157,83 @@ mod in_mem { let mut buffer = [0]; assert!(matches!( fs.read(&NoDeviceIo, &mut fd, &mut buffer, None), - Err(crate::fs::errors::ReadError::NotForReading) + Err(ReadError::NotForReading) )); drop(fd); assert!(matches!( - fs.open(user, path, OFlags::RDONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::AccessNotAllowed) + fs.open(USER, path, OFlags::RDONLY, Mode::empty()), + Err(OpenError::AccessNotAllowed) )); } #[test] fn newly_created_file_does_not_require_its_own_permissions() { - let user = USER; - - let fs = super::in_mem_fs(); - with_root_privileges(&fs, |fs, user| { - fs.mkdir(user, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); + let fs = in_mem_fs(); + world_writable_tmp(&fs); let path = "/tmp/zero_mode"; let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) .expect("Failed to create zero-mode file"); fs.write(&NoDeviceIo, &mut fd, b"x", None) .expect("Failed to write file"); drop(fd); - let status = fs.file_status(user, path).expect("Failed to stat file"); + let status = fs.file_status(USER, path).expect("Failed to stat file"); assert_eq!(status.mode, Mode::empty()); assert!(matches!( - fs.open(user, path, OFlags::WRONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::AccessNotAllowed) + fs.open(USER, path, OFlags::WRONLY, Mode::empty()), + Err(OpenError::AccessNotAllowed) )); } #[test] fn root_directory_creation_and_removal() { - with_root_privileges(&super::in_mem_fs(), |fs, user| { - // Test directory creation - let path = "/testdir"; - fs.mkdir(user, path, Mode::RWXU) - .expect("Failed to create directory"); - - // Test directory removal - fs.rmdir(user, path).expect("Failed to remove directory"); - assert!( - fs.open(user, path, OFlags::RDONLY, Mode::RWXU).is_err(), - "Directory should not exist" - ); - }); + let fs = in_mem_fs(); + + // Test directory creation + let path = "/testdir"; + fs.mkdir(ROOT, path, Mode::RWXU) + .expect("Failed to create directory"); + + // Test directory removal + fs.rmdir(ROOT, path).expect("Failed to remove directory"); + assert!( + fs.open(ROOT, path, OFlags::RDONLY, Mode::RWXU).is_err(), + "Directory should not exist" + ); } #[test] fn file_creation_and_deletion() { - let user = USER; - - let fs = super::in_mem_fs(); - with_root_privileges(&fs, |fs, user| { - // Make `/tmp` and set up with reasonable privs so normal users can do things in there. - fs.mkdir(user, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); + let fs = in_mem_fs(); + world_writable_tmp(&fs); // Test file creation let path = "/tmp/testfile"; let fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); - drop(fd); // Test file deletion - fs.unlink(user, path).expect("Failed to unlink file"); + fs.unlink(USER, path).expect("Failed to unlink file"); assert!( - fs.open(user, path, OFlags::RDONLY, Mode::RWXU).is_err(), + fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), "File should not exist" ); } #[test] fn file_read_write() { - let user = USER; - - let fs = super::in_mem_fs(); - with_root_privileges(&fs, |fs, user| { - // Make `/tmp` and set up with reasonable privs so normal users can do things in there. - fs.mkdir(user, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); + let fs = in_mem_fs(); + world_writable_tmp(&fs); // Create and write to a file let path = "/tmp/testfile"; let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let data = b"Hello, world!"; fs.write(&NoDeviceIo, &mut fd, data, None) @@ -241,7 +244,7 @@ mod in_mem { // Read from the file let mut fd = fs - .open(user, path, OFlags::RDONLY, Mode::RWXU) + .open(USER, path, OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; data.len()]; let bytes_read = fs @@ -253,268 +256,242 @@ mod in_mem { assert_eq!(bytes_read, data.len()); assert_eq!(bytes_read2, data.len() - 2); assert_eq!(&buffer, data); - drop(fd); } #[test] fn directory_creation_and_removal() { - let user = USER; - - let fs = super::in_mem_fs(); - with_root_privileges(&fs, |fs, user| { - // Make `/tmp` and set up with reasonable privs so normal users can do things in there. - fs.mkdir(user, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); + let fs = in_mem_fs(); + world_writable_tmp(&fs); // Test directory creation let path = "/tmp/testdir"; - fs.mkdir(user, path, Mode::RWXU) + fs.mkdir(USER, path, Mode::RWXU) .expect("Failed to create directory"); // Test directory removal - fs.rmdir(user, path).expect("Failed to remove directory"); + fs.rmdir(USER, path).expect("Failed to remove directory"); assert!( - fs.open(user, path, OFlags::RDONLY, Mode::RWXU).is_err(), + fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), "Directory should not exist" ); } #[test] fn read_dir_empty() { - with_root_privileges(&super::in_mem_fs(), |fs, user| { - let fd = fs - .open(user, "/", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open root directory"); - let entries = fs - .read_dir(&fd) - .expect("Failed to read directory") - .iter() - .map(|e| e.name.clone()) - .collect::>(); - assert_eq!( - entries, - vec![".", ".."], - "Root directory should contain . and .." - ); - drop(fd); - }); + let fs = in_mem_fs(); + + let fd = fs + .open(ROOT, "/", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open root directory"); + let entries = fs + .read_dir(&fd) + .expect("Failed to read directory") + .iter() + .map(|e| e.name.clone()) + .collect::>(); + assert_eq!( + entries, + vec![".", ".."], + "Root directory should contain . and .." + ); } #[test] fn read_dir_with_files_and_dirs() { - with_root_privileges(&super::in_mem_fs(), |fs, user| { - // Create a directory structure - fs.mkdir(user, "/testdir", Mode::RWXU) - .expect("Failed to create directory"); - let fd1 = fs - .open( - user, - "/testfile1", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file1"); - drop(fd1); - let fd2 = fs - .open( - user, - "/testfile2", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file2"); - drop(fd2); - - // Read root directory - let fd = fs - .open(user, "/", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open root directory"); - let entries = fs.read_dir(&fd).expect("Failed to read directory"); - drop(fd); + let fs = in_mem_fs(); + + // Create a directory structure + fs.mkdir(ROOT, "/testdir", Mode::RWXU) + .expect("Failed to create directory"); + let fd1 = fs + .open( + ROOT, + "/testfile1", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file1"); + drop(fd1); + let fd2 = fs + .open( + ROOT, + "/testfile2", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file2"); + drop(fd2); + + // Read root directory + let fd = fs + .open(ROOT, "/", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open root directory"); + let entries = fs.read_dir(&fd).expect("Failed to read directory"); + drop(fd); + + // Should have 5 entries: ., .., testdir, testfile1, testfile2 + assert_eq!(entries.len(), 5); + + let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); + names.sort_unstable(); + assert_eq!(names, vec![".", "..", "testdir", "testfile1", "testfile2"]); - // Should have 5 entries: ., .., testdir, testfile1, testfile2 - assert_eq!(entries.len(), 5); - - let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); - names.sort_unstable(); - assert_eq!(names, vec![".", "..", "testdir", "testfile1", "testfile2"]); - - // Check file types - for entry in &entries { - match entry.name.as_str() { - "testdir" | "." | ".." => { - assert_eq!(entry.file_type, crate::fs::FileType::Directory); - } - "testfile1" | "testfile2" => { - assert_eq!(entry.file_type, crate::fs::FileType::RegularFile); - } - _ => panic!("Unexpected entry: {}", entry.name), + // Check file types + for entry in &entries { + match entry.name.as_str() { + "testdir" | "." | ".." => { + assert_eq!(entry.file_type, FileType::Directory); } - if entry.name != "." && entry.name != ".." { - assert!(entry.ino_info.is_some(), "Inode info should be present"); - } else { - // TODO(jayb): Re-enable this assertion once the resolver fills in - // inode information for the synthesized `.` and `..` entries. + "testfile1" | "testfile2" => { + assert_eq!(entry.file_type, FileType::RegularFile); } + _ => panic!("Unexpected entry: {}", entry.name), + } + if entry.name != "." && entry.name != ".." { + assert!(entry.ino_info.is_some(), "Inode info should be present"); + } else { + // TODO(jayb): Re-enable this assertion once the resolver fills in + // inode information for the synthesized `.` and `..` entries. } + } - // Read the subdirectory (should be empty) - let fd = fs - .open(user, "/testdir", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open subdirectory"); - let entries = fs - .read_dir(&fd) - .expect("Failed to read subdirectory") - .iter() - .map(|e| e.name.clone()) - .collect::>(); - assert!(entries.len() == 2, "Subdirectory should contain . and .."); - drop(fd); - }); + // Read the subdirectory (should be empty) + let fd = fs + .open(ROOT, "/testdir", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open subdirectory"); + let entries = fs + .read_dir(&fd) + .expect("Failed to read subdirectory") + .iter() + .map(|e| e.name.clone()) + .collect::>(); + assert!(entries.len() == 2, "Subdirectory should contain . and .."); } #[test] fn read_dir_file_not_directory() { - with_root_privileges(&super::in_mem_fs(), |fs, user| { - // Create a file - let fd = fs - .open( - user, - "/testfile", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file"); - drop(fd); + let fs = in_mem_fs(); - // Try to read_dir on the file (should fail) - let fd = fs - .open(user, "/testfile", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file"); - let result = fs.read_dir(&fd); - drop(fd); + // Create a file + let fd = fs + .open( + ROOT, + "/testfile", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file"); + drop(fd); - assert!(matches!( - result, - Err(crate::fs::errors::ReadDirError::NotADirectory) - )); - }); + // Try to read_dir on the file (should fail) + let fd = fs + .open(ROOT, "/testfile", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file"); + assert!(matches!(fs.read_dir(&fd), Err(ReadDirError::NotADirectory))); } #[test] fn parent_dir_write_permissions_are_enforced() { - let fs = super::in_mem_fs(); - - with_root_privileges(&fs, |fs, user| { - // A root-owned 0755 directory, holding a file and a directory to try to remove. - fs.mkdir( - user, - "/rootdir", - Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, + let fs = in_mem_fs(); + + // A root-owned 0755 directory, holding a file and a directory to try to remove. + fs.mkdir( + ROOT, + "/rootdir", + Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, + ) + .expect("Failed to create directory"); + let fd = fs + .open( + ROOT, + "/rootdir/file", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, ) - .expect("Failed to create directory"); - let fd = fs - .open( - user, - "/rootdir/file", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file"); - drop(fd); - fs.mkdir(user, "/rootdir/sub", Mode::RWXU) - .expect("Failed to create subdirectory"); + .expect("Failed to create file"); + drop(fd); + fs.mkdir(ROOT, "/rootdir/sub", Mode::RWXU) + .expect("Failed to create subdirectory"); - // A world-writable directory, for the positive case. - fs.mkdir(user, "/opendir", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create directory"); - }); + // A world-writable directory, for the positive case. + fs.mkdir(ROOT, "/opendir", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to create directory"); - with_user(&fs, 1000, 1000, |fs, user| { - assert!(matches!( - fs.open( - user, - "/rootdir/new", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU - ), - Err(crate::fs::errors::OpenError::NoWritePerms) - )); - assert!(matches!( - fs.mkdir(user, "/rootdir/newdir", Mode::RWXU), - Err(crate::fs::errors::MkdirError::NoWritePerms) - )); - assert!(matches!( - fs.unlink(user, "/rootdir/file"), - Err(crate::fs::errors::UnlinkError::NoWritePerms) - )); - assert!(matches!( - fs.rmdir(user, "/rootdir/sub"), - Err(crate::fs::errors::RmdirError::NoWritePerms) - )); + assert!(matches!( + fs.open( + USER, + "/rootdir/new", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU + ), + Err(OpenError::NoWritePerms) + )); + assert!(matches!( + fs.mkdir(USER, "/rootdir/newdir", Mode::RWXU), + Err(MkdirError::NoWritePerms) + )); + assert!(matches!( + fs.unlink(USER, "/rootdir/file"), + Err(UnlinkError::NoWritePerms) + )); + assert!(matches!( + fs.rmdir(USER, "/rootdir/sub"), + Err(RmdirError::NoWritePerms) + )); - // The same operations succeed in a directory the user may write. - let fd = fs - .open( - user, - "/opendir/new", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file"); - drop(fd); - fs.mkdir(user, "/opendir/newdir", Mode::RWXU) - .expect("Failed to create directory"); - fs.unlink(user, "/opendir/new") - .expect("Failed to unlink file"); - fs.rmdir(user, "/opendir/newdir") - .expect("Failed to remove directory"); - }); + // The same operations succeed in a directory the user may write. + let fd = fs + .open( + USER, + "/opendir/new", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file"); + drop(fd); + fs.mkdir(USER, "/opendir/newdir", Mode::RWXU) + .expect("Failed to create directory"); + fs.unlink(USER, "/opendir/new") + .expect("Failed to unlink file"); + fs.rmdir(USER, "/opendir/newdir") + .expect("Failed to remove directory"); } #[test] fn chown_test() { - let user = USER; - - let fs = super::in_mem_fs(); + let fs = in_mem_fs(); // Create a test file as root - with_root_privileges(&fs, |fs, user| { - let path = "/testfile"; - let fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - drop(fd); - - // First chown to 1000:1000 as root (should succeed) - fs.chown(user, path, Some(1000), Some(1000)) - .expect("Failed to chown as root"); - }); - - // Switch to user 1000 and test that owner can chown (should succeed) let path = "/testfile"; - with_user(&fs, 1000, 1000, |fs, user| { - fs.chown(user, path, Some(123), Some(456)) - .expect("Failed to chown as owner"); - }); + let fd = fs + .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + drop(fd); - // Switch to a different user and test that non-owner cannot chown (should fail) - with_user(&fs, 500, 500, |fs, user| { - match fs.chown(user, path, Some(789), Some(101)) { - Err(crate::fs::errors::ChownError::NotTheOwner) => { - // Expected behavior - } - Ok(()) => panic!("Non-owner should not be able to chown"), - Err(e) => panic!("Unexpected error: {e:?}"), + // First chown to 1000:1000 as root (should succeed) + fs.chown(ROOT, path, Some(1000), Some(1000)) + .expect("Failed to chown as root"); + + // The owner may chown (should succeed) + fs.chown(USER, path, Some(123), Some(456)) + .expect("Failed to chown as owner"); + + // A different user may not chown (should fail) + let other = UserInfo { + user: 500, + group: 500, + }; + match fs.chown(other, path, Some(789), Some(101)) { + Err(ChownError::NotTheOwner) => { + // Expected behavior } - }); + Ok(()) => panic!("Non-owner should not be able to chown"), + Err(e) => panic!("Unexpected error: {e:?}"), + } // Test chown on non-existent file (should fail) - match fs.chown(user, "/nonexistent", Some(123), Some(456)) { - Err(crate::fs::errors::ChownError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory, - )) => { + match fs.chown(USER, "/nonexistent", Some(123), Some(456)) { + Err(ChownError::PathError(PathError::NoSuchFileOrDirectory)) => { // Expected behavior } Ok(()) => panic!("Should not be able to chown non-existent file"), @@ -522,35 +499,26 @@ mod in_mem { } // Test partial chown (change only user, leave group unchanged) - with_root_privileges(&fs, |fs, user| { - fs.chown(user, path, Some(999), None) - .expect("Failed to chown user only"); - }); + fs.chown(ROOT, path, Some(999), None) + .expect("Failed to chown user only"); // Test partial chown (change only group, leave user unchanged) - with_root_privileges(&fs, |fs, user| { - fs.chown(user, path, None, Some(888)) - .expect("Failed to chown group only"); - }); + fs.chown(ROOT, path, None, Some(888)) + .expect("Failed to chown group only"); } #[test] fn o_directory_flag_tests() { - let user = USER; - - let fs = super::in_mem_fs(); + let fs = in_mem_fs(); + world_writable_root(&fs); - with_root_privileges(&fs, |fs, user| { - fs.chmod(user, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); // Create test directory and file - fs.mkdir(user, "/testdir", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(USER, "/testdir", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create directory"); let fd = fs .open( - user, + USER, "/testfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -561,7 +529,7 @@ mod in_mem { // Test O_DIRECTORY on a directory (should succeed) let fd = fs .open( - user, + USER, "/testdir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), @@ -572,34 +540,30 @@ mod in_mem { // Test O_DIRECTORY on a regular file (should fail) assert!(matches!( fs.open( - user, + USER, "/testfile", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) + Err(OpenError::PathError(PathError::ComponentNotADirectory)) )); // Test O_DIRECTORY on non-existent path (should fail) assert!(matches!( fs.open( - user, + USER, "/nonexistent", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )) + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) )); // Test O_DIRECTORY with O_CREAT on non-existent path // According to the implementation, O_DIRECTORY should be ignored when O_CREAT is specified let fd = fs .open( - user, + USER, "/newfile", OFlags::CREAT | OFlags::WRONLY | OFlags::DIRECTORY, Mode::RWXU, @@ -609,9 +573,9 @@ mod in_mem { // Verify it created a regular file, not a directory let stat = fs - .file_status(user, "/newfile") + .file_status(USER, "/newfile") .expect("Failed to get file status"); - assert_eq!(stat.file_type, crate::fs::FileType::RegularFile); + assert_eq!(stat.file_type, FileType::RegularFile); // TODO(jayb): Restore coverage of `O_RDWR | O_DIRECTORY` once `OpenError` can report // `EISDIR`; see the matching TODO in `InMem::owned_dir_at`. The legacy in-memory file @@ -620,19 +584,13 @@ mod in_mem { #[test] fn o_excl_flag_tests() { - let user = USER; - - let fs = super::in_mem_fs(); - - with_root_privileges(&fs, |fs, user| { - fs.chmod(user, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); + let fs = in_mem_fs(); + world_writable_root(&fs); // Test O_CREAT | O_EXCL on non-existent file (should succeed) let mut fd = fs .open( - user, + USER, "/newfile", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -647,18 +605,18 @@ mod in_mem { // Test O_CREAT | O_EXCL on existing file (should fail) assert!(matches!( fs.open( - user, + USER, "/newfile", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), - Err(crate::fs::errors::OpenError::AlreadyExists) + Err(OpenError::AlreadyExists) )); // Test O_EXCL without O_CREAT (should be ignored and succeed) let mut fd = fs .open( - user, + USER, "/newfile", OFlags::EXCL | OFlags::RDONLY, Mode::empty(), @@ -675,39 +633,33 @@ mod in_mem { // Test O_CREAT without O_EXCL on existing file (should succeed) let fd = fs - .open(user, "/newfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(USER, "/newfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to open existing file with O_CREAT (without O_EXCL)"); drop(fd); // Test O_CREAT | O_EXCL on directory (should fail) - fs.mkdir(user, "/testdir", Mode::RWXU) + fs.mkdir(USER, "/testdir", Mode::RWXU) .expect("Failed to create directory"); assert!(matches!( fs.open( - user, + USER, "/testdir", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), - Err(crate::fs::errors::OpenError::AlreadyExists) + Err(OpenError::AlreadyExists) )); } #[test] fn open_with_trunc() { - let user = USER; - - let fs = super::in_mem_fs(); - - with_root_privileges(&fs, |fs, user| { - fs.chmod(user, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); + let fs = in_mem_fs(); + world_writable_root(&fs); // Create a file and write some initial content let path = "/testfile"; let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let initial_data = b"Hello, world! This is initial content."; fs.write(&NoDeviceIo, &mut fd, initial_data, None) @@ -716,7 +668,7 @@ mod in_mem { // Verify initial content was written let mut fd = fs - .open(user, path, OFlags::RDONLY, Mode::empty()) + .open(USER, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); let mut buffer = vec![0; initial_data.len()]; let bytes_read = fs @@ -728,7 +680,7 @@ mod in_mem { // Test O_TRUNC with O_WRONLY - should truncate file let mut fd = fs - .open(user, path, OFlags::WRONLY | OFlags::TRUNC, Mode::empty()) + .open(USER, path, OFlags::WRONLY | OFlags::TRUNC, Mode::empty()) .expect("Failed to open file with O_TRUNC | O_WRONLY"); // Write new content to the truncated file @@ -739,7 +691,7 @@ mod in_mem { // Verify the file was truncated and contains only new content let mut fd = fs - .open(user, path, OFlags::RDONLY, Mode::empty()) + .open(USER, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for verification"); let mut buffer = vec![0; initial_data.len()]; let bytes_read = fs @@ -750,15 +702,15 @@ mod in_mem { drop(fd); // Test O_TRUNC with O_RDWR - should also truncate - fs.write( - &NoDeviceIo, - &mut fs.open(user, path, OFlags::WRONLY, Mode::empty()).unwrap(), - b"More content to truncate", - None, - ) - .unwrap(); let mut fd = fs - .open(user, path, OFlags::RDWR | OFlags::TRUNC, Mode::empty()) + .open(USER, path, OFlags::WRONLY, Mode::empty()) + .expect("Failed to open file for writing"); + fs.write(&NoDeviceIo, &mut fd, b"More content to truncate", None) + .expect("Failed to write more content"); + drop(fd); + + let mut fd = fs + .open(USER, path, OFlags::RDWR | OFlags::TRUNC, Mode::empty()) .expect("Failed to open file with O_TRUNC | O_RDWR"); // File should be empty after truncation @@ -773,32 +725,24 @@ mod in_mem { fs.write(&NoDeviceIo, &mut fd, test_data, None) .expect("Failed to write after RDWR truncation"); - fs.seek(&mut fd, 0, crate::fs::SeekWhence::RelativeToBeginning) + fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) .expect("Failed to seek to beginning"); let bytes_read = fs .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read after write"); assert_eq!(bytes_read, test_data.len()); assert_eq!(&buffer[..bytes_read], test_data); - drop(fd); } #[test] fn write_position_after_seek() { - use crate::fs::SeekWhence; - - let user = USER; - - let fs = super::in_mem_fs(); - with_root_privileges(&fs, |fs, user| { - // Allow regular user to create in root for this focused test - fs.chmod(user, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("chmod / failed"); - }); + let fs = in_mem_fs(); + // Allow regular user to create in root for this focused test + world_writable_root(&fs); let mut fd = fs .open( - user, + USER, "/posfile", OFlags::CREAT | OFlags::RDWR, Mode::RWXU | Mode::RWXG | Mode::RWXO, @@ -817,7 +761,7 @@ mod in_mem { fs.write(&NoDeviceIo, &mut fd, b"X", None) .expect("overwrite failed"); - // The file offset should now be at 2. + // The file offset should now be at 1. assert_eq!( fs.seek(&mut fd, 0, SeekWhence::RelativeToCurrentOffset) .expect("seek failed"), @@ -845,78 +789,63 @@ mod in_mem { .expect("read 2 failed"); assert_eq!(n2, 8); assert_eq!(&buf2[..n2], b"Xbcdef12"); + } - drop(fd); + /// Create `path` holding `data`, as the unprivileged user. + fn create_with_content(fs: &InMemFs, path: &str, data: &[u8]) { + let mut fd = fs + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + fs.write(&NoDeviceIo, &mut fd, data, None) + .expect("Failed to write initial content"); + } + + /// Read the whole of `fd` from its current position. + fn read_all(fs: &InMemFs, fd: &mut InMemEntry) -> Vec { + let mut buffer = vec![0; 64]; + let bytes_read = fs + .read(&NoDeviceIo, fd, &mut buffer, None) + .expect("Failed to read from file"); + buffer.truncate(bytes_read); + buffer } #[test] fn o_append_flag_basic() { - let user = USER; - - let fs = super::in_mem_fs(); - - with_root_privileges(&fs, |fs, user| { - fs.chmod(user, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); + let fs = in_mem_fs(); + world_writable_root(&fs); // Create a file and write some initial content let path = "/testfile"; - let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - let initial_data = b"Hello"; - fs.write(&NoDeviceIo, &mut fd, initial_data, None) - .expect("Failed to write initial content"); - drop(fd); + create_with_content(&fs, path, b"Hello"); // Re-open with O_APPEND and write more data let mut fd = fs - .open(user, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_APPEND"); - let append_data = b" World"; - fs.write(&NoDeviceIo, &mut fd, append_data, None) + fs.write(&NoDeviceIo, &mut fd, b" World", None) .expect("Failed to append data"); drop(fd); // Verify the file contains both pieces of data concatenated let mut fd = fs - .open(user, path, OFlags::RDONLY, Mode::empty()) + .open(USER, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); - let mut buffer = vec![0; 11]; - let bytes_read = fs - .read(&NoDeviceIo, &mut fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 11); - assert_eq!(&buffer[..bytes_read], b"Hello World"); - drop(fd); + assert_eq!(read_all(&fs, &mut fd), b"Hello World"); } #[test] fn o_append_flag_seek_ignored_for_write() { - use crate::fs::SeekWhence; - - let user = USER; - - let fs = super::in_mem_fs(); - - with_root_privileges(&fs, |fs, user| { - fs.chmod(user, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); + let fs = in_mem_fs(); + world_writable_root(&fs); // Create a file and write some initial content let path = "/testfile"; - let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.write(&NoDeviceIo, &mut fd, b"ABCDEF", None) - .expect("Failed to write initial content"); - drop(fd); + create_with_content(&fs, path, b"ABCDEF"); // Re-open with O_APPEND let mut fd = fs - .open(user, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_APPEND"); // Seek to beginning - this should succeed but writes should still append @@ -930,51 +859,27 @@ mod in_mem { // Verify the file content: original data followed by appended data let mut fd = fs - .open(user, path, OFlags::RDONLY, Mode::empty()) + .open(USER, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); - let mut buffer = vec![0; 20]; - let bytes_read = fs - .read(&NoDeviceIo, &mut fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 9); - assert_eq!(&buffer[..bytes_read], b"ABCDEF123"); - drop(fd); + assert_eq!(read_all(&fs, &mut fd), b"ABCDEF123"); } #[test] fn o_append_flag_with_rdwr() { - use crate::fs::SeekWhence; - - let user = USER; - - let fs = super::in_mem_fs(); - - with_root_privileges(&fs, |fs, user| { - fs.chmod(user, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); + let fs = in_mem_fs(); + world_writable_root(&fs); // Create a file with initial content let path = "/testfile"; - let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.write(&NoDeviceIo, &mut fd, b"Hello", None) - .expect("Failed to write initial content"); - drop(fd); + create_with_content(&fs, path, b"Hello"); // Re-open with O_RDWR | O_APPEND let mut fd = fs - .open(user, path, OFlags::RDWR | OFlags::APPEND, Mode::empty()) + .open(USER, path, OFlags::RDWR | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_RDWR | O_APPEND"); // Read should work normally from the beginning - let mut buffer = vec![0; 10]; - let bytes_read = fs - .read(&NoDeviceIo, &mut fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 5); - assert_eq!(&buffer[..bytes_read], b"Hello"); + assert_eq!(read_all(&fs, &mut fd), b"Hello"); // Seek to beginning - write should still append despite position being at 0 fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) @@ -987,38 +892,21 @@ mod in_mem { // Seek to beginning and read the whole file fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) .expect("Seek failed"); - let mut buffer = vec![0; 20]; - let bytes_read = fs - .read(&NoDeviceIo, &mut fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 11); - assert_eq!(&buffer[..bytes_read], b"Hello World"); - drop(fd); + assert_eq!(read_all(&fs, &mut fd), b"Hello World"); } #[test] fn o_append_pwrite_ignores_append_mode() { - let user = USER; - - let fs = super::in_mem_fs(); - - with_root_privileges(&fs, |fs, user| { - fs.chmod(user, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); + let fs = in_mem_fs(); + world_writable_root(&fs); // Create a file with initial content let path = "/testfile"; - let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.write(&NoDeviceIo, &mut fd, b"ABCDEF", None) - .expect("Failed to write initial content"); - drop(fd); + create_with_content(&fs, path, b"ABCDEF"); // Re-open with O_APPEND let mut fd = fs - .open(user, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_APPEND"); // pwrite (write with explicit offset) should ignore O_APPEND per POSIX @@ -1028,41 +916,24 @@ mod in_mem { // Verify the file content: XX should be at position 2, not appended let mut fd = fs - .open(user, path, OFlags::RDONLY, Mode::empty()) + .open(USER, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); - let mut buffer = vec![0; 10]; - let bytes_read = fs - .read(&NoDeviceIo, &mut fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 6); - assert_eq!(&buffer[..bytes_read], b"ABXXEF"); - drop(fd); + assert_eq!(read_all(&fs, &mut fd), b"ABXXEF"); } #[test] fn o_append_with_trunc() { - let user = USER; - - let fs = super::in_mem_fs(); - - with_root_privileges(&fs, |fs, user| { - fs.chmod(user, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); + let fs = in_mem_fs(); + world_writable_root(&fs); // Create a file with initial content let path = "/testfile"; - let mut fd = fs - .open(user, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.write(&NoDeviceIo, &mut fd, b"Original content", None) - .expect("Failed to write initial content"); - drop(fd); + create_with_content(&fs, path, b"Original content"); // Re-open with O_TRUNC | O_APPEND let mut fd = fs .open( - user, + USER, path, OFlags::WRONLY | OFlags::TRUNC | OFlags::APPEND, Mode::empty(), @@ -1078,35 +949,23 @@ mod in_mem { // Verify the file content let mut fd = fs - .open(user, path, OFlags::RDONLY, Mode::empty()) + .open(USER, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); - let mut buffer = vec![0; 20]; - let bytes_read = fs - .read(&NoDeviceIo, &mut fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 10); - assert_eq!(&buffer[..bytes_read], b"NewContent"); - drop(fd); + assert_eq!(read_all(&fs, &mut fd), b"NewContent"); } } mod tar_ro { - use super::USER; - use crate::fs::backend::NoDeviceIo; - use crate::fs::{Mode, OFlags}; + use super::{FileType, Mode, NoDeviceIo, OFlags, TEST_TAR_FILE, USER, tar_ro_fs}; + use crate::fs::errors::{OpenError, PathError, ReadDirError}; use alloc::vec; use alloc::vec::Vec; - extern crate std; - - const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); #[test] fn file_read() { - let user = USER; - - let fs = super::tar_ro_fs(TEST_TAR_FILE.into()); + let fs = tar_ro_fs(TEST_TAR_FILE.into()); let mut fd = fs - .open(user, "foo", OFlags::RDONLY, Mode::RWXU) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1114,114 +973,94 @@ mod tar_ro { .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"testfoo\n"); drop(fd); + let mut fd = fs - .open(user, "bar/baz", OFlags::RDONLY, Mode::empty()) + .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"test bar baz\n"); - drop(fd); } #[test] fn dir_and_nonexist_checks() { - let user = USER; - - let fs = super::tar_ro_fs(TEST_TAR_FILE.into()); + let fs = tar_ro_fs(TEST_TAR_FILE.into()); assert!(matches!( - fs.open(user, "bar/ba", OFlags::RDONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )), + fs.open(USER, "bar/ba", OFlags::RDONLY, Mode::empty()), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), )); - let fd = fs - .open(user, "bar", OFlags::RDONLY, Mode::empty()) + fs.open(USER, "bar", OFlags::RDONLY, Mode::empty()) .expect("Failed to open dir"); - drop(fd); } #[test] fn o_directory_flag_tests() { - let user = USER; - - let fs = super::tar_ro_fs(TEST_TAR_FILE.into()); + let fs = tar_ro_fs(TEST_TAR_FILE.into()); // Test O_DIRECTORY on a directory (should succeed) - let fd = fs - .open( - user, - "bar", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .expect("Failed to open directory with O_DIRECTORY"); - drop(fd); + fs.open( + USER, + "bar", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) + .expect("Failed to open directory with O_DIRECTORY"); // Test O_DIRECTORY on a regular file (should fail) assert!(matches!( fs.open( - user, + USER, "foo", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) + Err(OpenError::PathError(PathError::ComponentNotADirectory)) )); // Test O_DIRECTORY on non-existent path (should fail) assert!(matches!( fs.open( - user, + USER, "nonexistent", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )) + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) )); // Test O_DIRECTORY on nested file (should fail) assert!(matches!( fs.open( - user, + USER, "bar/baz", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) + Err(OpenError::PathError(PathError::ComponentNotADirectory)) )); } #[test] fn write_or_truncate_open_of_directory_fails() { - let user = USER; - - let fs = super::tar_ro_fs(TEST_TAR_FILE.into()); + let fs = tar_ro_fs(TEST_TAR_FILE.into()); for flags in [OFlags::WRONLY, OFlags::RDWR, OFlags::TRUNC] { assert!(matches!( - fs.open(user, "bar", flags, Mode::empty()), - Err(crate::fs::errors::OpenError::ReadOnlyFileSystem) + fs.open(USER, "bar", flags, Mode::empty()), + Err(OpenError::ReadOnlyFileSystem) )); } } #[test] fn read_dir_subdirectory() { - let user = USER; - - let fs = super::tar_ro_fs(TEST_TAR_FILE.into()); + let fs = tar_ro_fs(TEST_TAR_FILE.into()); // Read root directory let fd = fs - .open(user, "/", OFlags::RDONLY, Mode::empty()) + .open(USER, "/", OFlags::RDONLY, Mode::empty()) .expect("Failed to open root directory"); let entries = fs.read_dir(&fd).expect("Failed to read root directory"); drop(fd); @@ -1237,9 +1076,9 @@ mod tar_ro { for entry in &entries { match entry.name.as_str() { "foo" => { - assert_eq!(entry.file_type, crate::fs::FileType::RegularFile); + assert_eq!(entry.file_type, FileType::RegularFile); } - "bar" | "." | ".." => assert_eq!(entry.file_type, crate::fs::FileType::Directory), + "bar" | "." | ".." => assert_eq!(entry.file_type, FileType::Directory), _ => panic!("Unexpected entry: {}", entry.name), } if entry.name != "." && entry.name != ".." { @@ -1252,53 +1091,40 @@ mod tar_ro { // Read `bar` directory let fd = fs - .open(user, "bar", OFlags::RDONLY, Mode::empty()) + .open(USER, "bar", OFlags::RDONLY, Mode::empty()) .expect("Failed to open bar directory"); let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); - drop(fd); - // Should have 3 entry: ., .., baz (file) + // Should have 3 entries: ., .., baz (file) assert_eq!(entries.len(), 3); assert_eq!(entries[2].name, "baz"); - assert_eq!(entries[2].file_type, crate::fs::FileType::RegularFile); + assert_eq!(entries[2].file_type, FileType::RegularFile); } #[test] fn read_dir_file_not_directory() { - let user = USER; - - let fs = super::tar_ro_fs(TEST_TAR_FILE.into()); + let fs = tar_ro_fs(TEST_TAR_FILE.into()); let fd = fs - .open(user, "foo", OFlags::RDONLY, Mode::empty()) + .open(USER, "foo", OFlags::RDONLY, Mode::empty()) .expect("Failed to open foo file"); - let result = fs.read_dir(&fd); - drop(fd); - - assert!(matches!( - result, - Err(crate::fs::errors::ReadDirError::NotADirectory) - )); + assert!(matches!(fs.read_dir(&fd), Err(ReadDirError::NotADirectory))); } } mod overlay { - use super::USER; - use crate::fs::backend::NoDeviceIo; + use super::{ + FileType, Mode, NoDeviceIo, OFlags, Overlay, Resolver, SeekWhence, TEST_TAR_FILE, + TestPlatform, USER, UserInfo, + }; + use crate::fs::errors::{FileStatusError, OpenError, PathError, RmdirError}; use crate::fs::in_mem::{InMem, InitialNode}; - use crate::fs::{FileType, Mode, OFlags, UserInfo}; - use crate::test_platform::TestPlatform; use alloc::vec; use alloc::vec::Vec; extern crate std; - const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); - /// The user these tests act as, and so the owner of anything they are set up as having created. - const ACTING_USER: UserInfo = UserInfo { - user: 1000, - group: 1000, - }; + const ACTING_USER: UserInfo = USER; const ALL_PERMS: Mode = Mode::RWXU.union(Mode::RWXG).union(Mode::RWXO); /// An upper backend whose root is writable by the acting user, holding `entries`. @@ -1321,75 +1147,64 @@ mod overlay { ) } - fn overlay_fs(upper: InMem) -> super::OverlayFs { + fn overlay_fs(upper: InMem) -> Resolver> { super::overlay_fs(upper, TEST_TAR_FILE.into()) } #[test] fn file_read_from_lower() { - let user = USER; - let fs = overlay_fs(upper([])); let mut fd = fs - .open(user, "foo", OFlags::RDONLY, Mode::RWXU) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"testfoo\n"); - let stat = fs.handle_status(&fd).expect("Failed to fd file stat"); + let stat = fs.handle_status(&fd).expect("Failed to handle stat"); assert_eq!(stat.file_type, FileType::RegularFile); assert_eq!(stat.mode, Mode::from_bits(0o644).unwrap()); drop(fd); - let stat = fs.file_status(user, "bar").expect("Failed to file stat"); + let stat = fs.file_status(USER, "bar").expect("Failed to file stat"); assert_eq!(stat.file_type, FileType::Directory); assert_eq!(stat.mode, Mode::from_bits(0o777).unwrap()); let mut fd = fs - .open(user, "bar/baz", OFlags::RDONLY, Mode::empty()) + .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"test bar baz\n"); - let stat = fs.handle_status(&fd).expect("Failed to fd file stat"); + let stat = fs.handle_status(&fd).expect("Failed to handle stat"); assert_eq!(stat.file_type, FileType::RegularFile); assert_eq!(stat.mode, Mode::from_bits(0o644).unwrap()); - drop(fd); } #[test] fn dir_and_nonexist_checks() { - let user = USER; - let fs = overlay_fs(upper([])); assert!(matches!( - fs.open(user, "bar/ba", OFlags::RDONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )), + fs.open(USER, "bar/ba", OFlags::RDONLY, Mode::empty()), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), )); - let fd = fs - .open(user, "bar", OFlags::RDONLY, Mode::empty()) + fs.open(USER, "bar", OFlags::RDONLY, Mode::empty()) .expect("Failed to open dir"); - drop(fd); } /// Check that for the same file, even though it started as a lower file, writing to it copies /// it up and redirects handles already open on it, so every descriptor sees the update. #[test] fn file_read_write_copy_up() { - let user = USER; - let fs = overlay_fs(upper([])); let mut fd1 = fs - .open(user, "foo", OFlags::RDONLY, Mode::RWXU) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut fd2 = fs - .open(user, "foo", OFlags::WRONLY, Mode::RWXU) + .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; @@ -1402,29 +1217,24 @@ mod overlay { fs.write(&NoDeviceIo, &mut fd2, b"share", None) .expect("Failed to write to file"); - fs.seek(&mut fd1, 0, crate::fs::SeekWhence::RelativeToBeginning) + fs.seek(&mut fd1, 0, SeekWhence::RelativeToBeginning) .expect("Failed to seek to start"); let bytes_read = fs .read(&NoDeviceIo, &mut fd1, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"shareoo\n"); - - drop(fd1); - drop(fd2); } /// Similar to [`file_read_write_copy_up`] but also confirm that file positions have been /// maintained. #[test] fn file_read_write_copy_up_keeps_position() { - let user = USER; - let fs = overlay_fs(upper([])); let mut fd1 = fs - .open(user, "foo", OFlags::RDONLY, Mode::RWXU) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut fd2 = fs - .open(user, "foo", OFlags::WRONLY, Mode::RWXU) + .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1441,18 +1251,13 @@ mod overlay { .read(&NoDeviceIo, &mut fd1, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"eoo\n"); - - drop(fd1); - drop(fd2); } #[test] fn file_deletion() { - let user = USER; - let fs = overlay_fs(upper([])); let mut fd = fs - .open(user, "foo", OFlags::RDONLY, Mode::RWXU) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1464,7 +1269,7 @@ mod overlay { assert_eq!(&buffer[..bytes_read], b"test"); // Then we delete it - fs.unlink(user, "foo").unwrap(); + fs.unlink(USER, "foo").unwrap(); // This should not really impact the readability; file is fine. let bytes_read = fs @@ -1475,17 +1280,13 @@ mod overlay { // But if we close and attempt to re-open, it should not exist drop(fd); assert!(matches!( - fs.open(user, "foo", OFlags::RDONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )), + fs.open(USER, "foo", OFlags::RDONLY, Mode::empty()), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), )); } #[test] fn o_directory_flag_tests() { - let user = USER; - let fs = overlay_fs(upper([ ( "/upperdir", @@ -1505,77 +1306,65 @@ mod overlay { ])); // Test O_DIRECTORY on directory from lower layer (tar) - let fd = fs - .open( - user, - "bar", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .expect("Failed to open lower layer directory with O_DIRECTORY"); - drop(fd); + fs.open( + USER, + "bar", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) + .expect("Failed to open lower layer directory with O_DIRECTORY"); // Test O_DIRECTORY on directory from upper layer (in_mem) - let fd = fs - .open( - user, - "/upperdir", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .expect("Failed to open upper layer directory with O_DIRECTORY"); - drop(fd); + fs.open( + USER, + "/upperdir", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) + .expect("Failed to open upper layer directory with O_DIRECTORY"); // Test O_DIRECTORY on file from lower layer (should fail) assert!(matches!( fs.open( - user, + USER, "foo", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) + Err(OpenError::PathError(PathError::ComponentNotADirectory)) )); // Test O_DIRECTORY on file from upper layer (should fail) assert!(matches!( fs.open( - user, + USER, "/upperfile", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) + Err(OpenError::PathError(PathError::ComponentNotADirectory)) )); // Test O_DIRECTORY on nested file from lower layer (should fail) assert!(matches!( fs.open( - user, + USER, "bar/baz", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) + Err(OpenError::PathError(PathError::ComponentNotADirectory)) )); // Test O_DIRECTORY on non-existent path (should fail) assert!(matches!( fs.open( - user, + USER, "nonexistent", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )) + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) )); } @@ -1583,11 +1372,9 @@ mod overlay { // Regression test for #250: a file that already exists in the lower layer should not be // shadowed by an attempt to create a file. fn file_create_exist_in_lower() { - let user = USER; - let fs = overlay_fs(upper([])); let mut fd = fs - .open(user, "foo", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) + .open(USER, "foo", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1600,21 +1387,18 @@ mod overlay { #[test] fn read_dir_from_lower_layer() { - let user = USER; - let fs = overlay_fs(upper([])); // Read bar subdirectory let fd = fs - .open(user, "bar", OFlags::RDONLY, Mode::empty()) + .open(USER, "bar", OFlags::RDONLY, Mode::empty()) .expect("Failed to open bar directory"); let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); - drop(fd); // Should have 3 entries: ., .., baz (file) assert_eq!(entries.len(), 3); assert_eq!(entries[2].name, "baz"); - assert_eq!(entries[2].file_type, crate::fs::FileType::RegularFile); + assert_eq!(entries[2].file_type, FileType::RegularFile); assert!( entries[2].ino_info.is_some(), "Inode info should be present" @@ -1623,8 +1407,6 @@ mod overlay { #[test] fn read_dir_from_upper_layer() { - let user = USER; - let fs = overlay_fs(upper([ ( "/upperdir", @@ -1645,7 +1427,7 @@ mod overlay { // Read root directory (should contain entries from both layers) let fd = fs - .open(user, "/", OFlags::RDONLY, Mode::empty()) + .open(USER, "/", OFlags::RDONLY, Mode::empty()) .expect("Failed to open root directory"); let entries = fs.read_dir(&fd).expect("Failed to read root directory"); drop(fd); @@ -1664,10 +1446,10 @@ mod overlay { for entry in &entries { match entry.name.as_str() { "foo" | "upperfile" => { - assert_eq!(entry.file_type, crate::fs::FileType::RegularFile); + assert_eq!(entry.file_type, FileType::RegularFile); } "bar" | "upperdir" | "." | ".." => { - assert_eq!(entry.file_type, crate::fs::FileType::Directory); + assert_eq!(entry.file_type, FileType::Directory); } _ => panic!("Unexpected entry: {}", entry.name), } @@ -1681,10 +1463,9 @@ mod overlay { // Read upperdir directory (should be from upper layer) let fd = fs - .open(user, "/upperdir", OFlags::RDONLY, Mode::empty()) + .open(USER, "/upperdir", OFlags::RDONLY, Mode::empty()) .expect("Failed to open upperdir"); let entries = fs.read_dir(&fd).expect("Failed to read upperdir"); - drop(fd); // only . and .. assert_eq!(entries.len(), 2); @@ -1692,26 +1473,24 @@ mod overlay { #[test] fn o_excl_tests() { - let user = USER; - let fs = overlay_fs(upper([])); // Test O_CREAT | O_EXCL on file that exists in lower layer (should fail) // "foo" exists in the tar file assert!(matches!( fs.open( - user, + USER, "foo", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), - Err(crate::fs::errors::OpenError::AlreadyExists) + Err(OpenError::AlreadyExists) )); // Test O_CREAT | O_EXCL on file that doesn't exist anywhere (should succeed) let mut fd = fs .open( - user, + USER, "/newfile", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -1725,35 +1504,35 @@ mod overlay { // Test O_CREAT | O_EXCL on file that now exists in upper layer (should fail) assert!(matches!( fs.open( - user, + USER, "/newfile", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), - Err(crate::fs::errors::OpenError::AlreadyExists) + Err(OpenError::AlreadyExists) )); // Test O_CREAT | O_EXCL on directory that exists in lower layer (should fail) // "bar" is a directory in the tar file assert!(matches!( fs.open( - user, + USER, "bar", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), - Err(crate::fs::errors::OpenError::AlreadyExists) + Err(OpenError::AlreadyExists) )); // Test O_CREAT | O_EXCL on file that was deleted (tombstoned) should succeed // First delete a file from lower layer - fs.unlink(user, "foo") + fs.unlink(USER, "foo") .expect("Failed to unlink lower layer file"); // Now try to create it with O_EXCL (should succeed since it's tombstoned) let mut fd = fs .open( - user, + USER, "foo", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -1766,7 +1545,7 @@ mod overlay { // Verify the new content let mut fd = fs - .open(user, "foo", OFlags::RDONLY, Mode::empty()) + .open(USER, "foo", OFlags::RDONLY, Mode::empty()) .expect("Failed to open recreated file"); let mut buffer = vec![0; 15]; let bytes_read = fs @@ -1779,7 +1558,7 @@ mod overlay { // Create a file in upper layer first let mut fd = fs .open( - user, + USER, "/upper_only_file", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -1792,39 +1571,36 @@ mod overlay { // Now try O_CREAT | O_EXCL on the same file (should fail) assert!(matches!( fs.open( - user, + USER, "/upper_only_file", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), - Err(crate::fs::errors::OpenError::AlreadyExists) + Err(OpenError::AlreadyExists) )); } #[test] fn dir_creation_inside_lower_existing_dir() { - let user = USER; - let fs = overlay_fs(upper([])); // Create the directory /bar/test (where /bar already exists inside the tar file) - fs.mkdir(user, "/bar/test", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(USER, "/bar/test", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /bar/test directory"); // Verify the directory was created let stat = fs - .file_status(user, "/bar/test") + .file_status(USER, "/bar/test") .expect("Failed to get status of /bar/test"); assert_eq!(stat.file_type, FileType::Directory); // Verify we can open the directory let fd = fs - .open(user, "/bar/test", OFlags::RDONLY, Mode::empty()) + .open(USER, "/bar/test", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /bar/test directory"); let entries = fs .read_dir(&fd) .expect("Failed to read /bar/test directory"); - drop(fd); // Should contain only . and .. entries assert_eq!(entries.len(), 2); @@ -1835,14 +1611,12 @@ mod overlay { #[test] fn file_creation_materializes_ancestor_dirs() { - let user = USER; - let fs = overlay_fs(upper([])); // Open bar/test for writing (where bar exists in lower layer but test doesn't exist) // This should create ancestor directories and allow file creation let mut fd = fs - .open(user, "bar/test", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(USER, "bar/test", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to open bar/test for writing"); // Write data to the file @@ -1853,7 +1627,7 @@ mod overlay { // Read the file back let mut fd = fs - .open(user, "bar/test", OFlags::RDONLY, Mode::empty()) + .open(USER, "bar/test", OFlags::RDONLY, Mode::empty()) .expect("Failed to open bar/test for reading"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1864,21 +1638,19 @@ mod overlay { // Verify the file exists and has correct type let stat = fs - .file_status(user, "bar/test") + .file_status(USER, "bar/test") .expect("Failed to get status of bar/test"); assert_eq!(stat.file_type, FileType::RegularFile); } #[test] fn file_modification_materializes_ancestor_dirs() { - let user = USER; - let fs = overlay_fs(upper([])); // Open bar/baz for writing (both bar and baz exist in lower layer) // This copies up the ancestor directories and allows the file to be modified let mut fd = fs - .open(user, "bar/baz", OFlags::WRONLY, Mode::RWXU) + .open(USER, "bar/baz", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open bar/baz for writing"); // Write new data to the file (overwriting existing content) @@ -1889,7 +1661,7 @@ mod overlay { // Read the file back to verify it was modified let mut fd = fs - .open(user, "bar/baz", OFlags::RDONLY, Mode::empty()) + .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) .expect("Failed to open bar/baz for reading"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1901,20 +1673,18 @@ mod overlay { // Verify the file still exists and has correct type let stat = fs - .file_status(user, "bar/baz") + .file_status(USER, "bar/baz") .expect("Failed to get status of bar/baz"); assert_eq!(stat.file_type, FileType::RegularFile); } #[test] fn open_with_trunc() { - let user = USER; - let fs = overlay_fs(upper([])); // Open with O_TRUNC should copy the file up into the upper backend, empty let mut fd = fs - .open(user, "foo", OFlags::RDWR | OFlags::TRUNC, Mode::empty()) + .open(USER, "foo", OFlags::RDWR | OFlags::TRUNC, Mode::empty()) .expect("Failed to open file with O_TRUNC"); // File should be truncated (empty) @@ -1931,62 +1701,51 @@ mod overlay { // Verify the content persists let mut fd = fs - .open(user, "foo", OFlags::RDONLY, Mode::empty()) + .open(USER, "foo", OFlags::RDONLY, Mode::empty()) .expect("Failed to reopen file"); let mut buffer = vec![0; 1024]; let bytes_read = fs .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read file"); assert_eq!(&buffer[..bytes_read], b"new content"); - drop(fd); } #[test] fn rmdir_upper_only_directory() { - use crate::fs::errors::{PathError, RmdirError}; - - let user = USER; - let fs = overlay_fs(upper([])); // Create an empty directory only in upper layer - fs.mkdir(user, "/upper_empty", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(USER, "/upper_empty", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("mkdir upper_empty failed"); // Remove it - fs.rmdir(user, "/upper_empty") + fs.rmdir(USER, "/upper_empty") .expect("rmdir upper_empty should succeed"); // Verify it no longer exists assert!(matches!( - fs.file_status(user, "/upper_empty"), - Err(crate::fs::errors::FileStatusError::PathError( - PathError::NoSuchFileOrDirectory - )) + fs.file_status(USER, "/upper_empty"), + Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) )); // Second removal should yield NoSuchFileOrDirectory (path error) assert!(matches!( - fs.rmdir(user, "/upper_empty"), + fs.rmdir(USER, "/upper_empty"), Err(RmdirError::PathError(PathError::NoSuchFileOrDirectory)) )); } #[test] fn rmdir_upper_directory_not_empty_then_empty() { - use crate::fs::errors::{PathError, RmdirError}; - - let user = USER; - let fs = overlay_fs(upper([])); - fs.mkdir(user, "/upper_dir", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(USER, "/upper_dir", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("mkdir upper_dir failed"); // Create a file inside making directory non-empty let fd = fs .open( - user, + USER, "/upper_dir/file", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU | Mode::RWXG, @@ -1996,51 +1755,41 @@ mod overlay { // Attempt to remove while non-empty assert!(matches!( - fs.rmdir(user, "/upper_dir"), + fs.rmdir(USER, "/upper_dir"), Err(RmdirError::NotEmpty) )); // Remove inner file - fs.unlink(user, "/upper_dir/file") + fs.unlink(USER, "/upper_dir/file") .expect("unlink inner failed"); // Now should succeed - fs.rmdir(user, "/upper_dir") + fs.rmdir(USER, "/upper_dir") .expect("rmdir upper_dir should succeed"); // Confirm gone assert!(matches!( - fs.file_status(user, "/upper_dir"), - Err(crate::fs::errors::FileStatusError::PathError( - PathError::NoSuchFileOrDirectory - )) + fs.file_status(USER, "/upper_dir"), + Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) )); } #[test] fn rmdir_lower_directory_non_empty() { - use crate::fs::errors::RmdirError; - - let user = USER; - let fs = overlay_fs(upper([])); // "bar" exists in lower layer and contains "baz" (non-empty) - assert!(matches!(fs.rmdir(user, "bar"), Err(RmdirError::NotEmpty))); + assert!(matches!(fs.rmdir(USER, "bar"), Err(RmdirError::NotEmpty))); } #[test] fn rmdir_not_a_directory() { - use crate::fs::errors::RmdirError; - - let user = USER; - let fs = overlay_fs(upper([])); // Create a regular file (upper only) let fd = fs .open( - user, + USER, "/regular_file", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU | Mode::RWXG, @@ -2050,7 +1799,7 @@ mod overlay { // rmdir should fail with NotADirectory assert!(matches!( - fs.rmdir(user, "/regular_file"), + fs.rmdir(USER, "/regular_file"), Err(RmdirError::NotADirectory) )); } @@ -2061,17 +1810,15 @@ mod overlay { use std::thread; use std::time::Duration; - let user = USER; - let fs = overlay_fs(upper([])); - fs.file_status(user, "foo").expect("Failed to stat foo"); + fs.file_status(USER, "foo").expect("Failed to stat foo"); // Writing to the lower-layer file triggers copy-up. Run it on a worker thread. let (tx, rx) = mpsc::channel(); thread::spawn(move || { let mut fd = fs - .open(user, "foo", OFlags::WRONLY, Mode::RWXU) + .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file for writing"); fs.write(&NoDeviceIo, &mut fd, b"x", None) .expect("Failed to write to file"); @@ -2083,3 +1830,157 @@ mod overlay { .expect("copy-up deadlocked"); } } + +mod devices { + use super::{Mode, OFlags, Resolver, TestPlatform, USER, UnservicedStdio}; + use crate::fs::composer::Composer; + use crate::fs::devices::Devices; + use crate::fs::errors::{OpenError, PathError, ReadError, WriteError}; + use alloc::vec; + + fn devices_fs() -> Resolver { + Resolver::new( + Composer::builder() + .mount("/dev", Devices::new) + .build() + .unwrap(), + ) + } + + /// Stdio devices hold no data of their own: every non-empty transfer needs the session's + /// device I/O, and fails when the session cannot service it. + #[test] + fn stdio_requires_broker() { + let fs = devices_fs(); + let stdio = UnservicedStdio; + + let mut fd_stdout = fs + .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stdout"); + assert!(matches!(fs.write(&stdio, &mut fd_stdout, b"", None), Ok(0))); + assert!(matches!( + fs.write(&stdio, &mut fd_stdout, b"Hello, stdout!", None), + Err(WriteError::Io) + )); + drop(fd_stdout); + + let mut fd_stderr = fs + .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stderr"); + assert!(matches!(fs.write(&stdio, &mut fd_stderr, b"", None), Ok(0))); + assert!(matches!( + fs.write(&stdio, &mut fd_stderr, b"Hello, stderr!", None), + Err(WriteError::Io) + )); + drop(fd_stderr); + + let mut fd_stdin = fs + .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open /dev/stdin"); + assert!(matches!( + fs.read(&stdio, &mut fd_stdin, &mut [], None), + Ok(0) + )); + let mut buffer = vec![0; 13]; + assert!(matches!( + fs.read(&stdio, &mut fd_stdin, &mut buffer, None), + Err(ReadError::Io) + )); + } + + #[test] + fn non_dev_path_fails() { + let fs = devices_fs(); + + // Attempt to open a non-/dev/* path + assert!(matches!( + fs.open(USER, "foo", OFlags::RDONLY, Mode::empty()), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) + )); + } +} + +mod composed { + use super::{InMem, Mode, OFlags, Resolver, TestPlatform, USER, UnservicedStdio, UserInfo}; + use crate::fs::composer::Composer; + use crate::fs::devices::Devices; + use crate::fs::errors::{ReadError, WriteError}; + use crate::fs::in_mem::InitialNode; + use alloc::vec; + + fn composed_fs() -> Resolver { + Resolver::new( + Composer::builder() + .mount("/", |_| { + InMem::::new_initialized([( + "/", + InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner: UserInfo::ROOT, + }, + )]) + }) + .mount("/dev", Devices::new) + .build() + .unwrap(), + ) + } + + #[test] + fn stdio_requires_broker() { + let fs = composed_fs(); + let stdio = UnservicedStdio; + + let mut fd_stdout = fs + .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stdout"); + assert!(matches!(fs.write(&stdio, &mut fd_stdout, b"", None), Ok(0))); + assert!(matches!( + fs.write(&stdio, &mut fd_stdout, b"Hello, composed stdout!", None), + Err(WriteError::Io) + )); + drop(fd_stdout); + + let mut fd_stderr = fs + .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stderr"); + assert!(matches!(fs.write(&stdio, &mut fd_stderr, b"", None), Ok(0))); + assert!(matches!( + fs.write(&stdio, &mut fd_stderr, b"Hello, composed stderr!", None), + Err(WriteError::Io) + )); + drop(fd_stderr); + + let mut fd_stdin = fs + .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open /dev/stdin"); + assert!(matches!( + fs.read(&stdio, &mut fd_stdin, &mut [], None), + Ok(0) + )); + let mut buffer = vec![0; 1024]; + assert!(matches!( + fs.read(&stdio, &mut fd_stdin, &mut buffer, None), + Err(ReadError::Io) + )); + } + + #[test] + fn write_to_non_dev() { + let fs = composed_fs(); + + // Test file creation + let path = "/testfile"; + let fd = fs + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + drop(fd); + + // Test file deletion + fs.unlink(USER, path).expect("Failed to unlink file"); + assert!( + fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), + "File should not exist" + ); + } +} diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index cd962a754..4a246af41 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -618,10 +618,10 @@ impl Drop for BrokerSession { #[cfg(test)] mod tests { - use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; - use core::time::Duration; + use core::sync::atomic::{AtomicUsize, Ordering}; use super::{SessionReferences, release_pending_reference}; + use crate::test_platform::TestPlatform; use crate::test_support::TestBrokerCoreBuilder; use crate::{ BrokerCore, BrokerCoreLimits, BrokerError, CallerCredential, ObjectRights, PolicyEngine, @@ -633,13 +633,7 @@ mod tests { FileAccessMode, FileError, FileMode, FileOpenFlags, FileSeekWhence, FileType, FileUser, }; use litebox_broker_protocol::readiness::ReadinessFlags; - use litebox_platform::sync::{ - ImmediatelyWokenUp, RawMutex, RawMutexProvider, UnblockedOrTimedOut, - }; - use std::{ - sync::{Arc, Condvar, Mutex}, - vec::Vec, - }; + use std::{sync::Arc, vec::Vec}; const TEST_MAX_REFERENCES: usize = 4; const TEST_MAX_PIPE_CAPACITY: usize = 8; @@ -647,70 +641,6 @@ mod tests { const TEST_MAX_PIPE_CAPACITY_PER_SESSION: usize = 4; const ROOT: FileUser = FileUser { user: 0, group: 0 }; - struct TestRawMutex { - state: AtomicU32, - waiters: Mutex<()>, - wake: Condvar, - } - - impl RawMutex for TestRawMutex { - const INIT: Self = Self { - state: AtomicU32::new(0), - waiters: Mutex::new(()), - wake: Condvar::new(), - }; - - fn underlying_atomic(&self) -> &AtomicU32 { - &self.state - } - - fn wake_many(&self, count: usize) -> usize { - let _waiters = self.waiters.lock().unwrap(); - self.wake.notify_all(); - count - } - - fn block(&self, expected: u32) -> Result<(), ImmediatelyWokenUp> { - let waiters = self.waiters.lock().unwrap(); - if self.state.load(Ordering::Acquire) != expected { - return Err(ImmediatelyWokenUp); - } - let _waiters = self - .wake - .wait_while(waiters, |()| self.state.load(Ordering::Acquire) == expected) - .unwrap(); - Ok(()) - } - - fn block_or_timeout( - &self, - expected: u32, - timeout: Duration, - ) -> Result { - let waiters = self.waiters.lock().unwrap(); - if self.state.load(Ordering::Acquire) != expected { - return Err(ImmediatelyWokenUp); - } - let (_waiters, result) = self - .wake - .wait_timeout_while(waiters, timeout, |()| { - self.state.load(Ordering::Acquire) == expected - }) - .unwrap(); - Ok(if result.timed_out() { - UnblockedOrTimedOut::TimedOut - } else { - UnblockedOrTimedOut::Unblocked - }) - } - } - - struct TestSync; - - impl RawMutexProvider for TestSync { - type RawMutex = TestRawMutex; - } - #[test] fn pending_reference_release_checks_both_counters() { let core_pending_references = AtomicUsize::new(1); @@ -953,7 +883,7 @@ mod tests { fn object_reference_lifecycle_uses_public_core_constructor_once() { let socket_provider = Arc::new(crate::socket::tests::TestSocketProvider::default()); let fs = crate::fs::composer::Composer::builder() - .mount("/", crate::fs::in_mem::InMem::::new) + .mount("/", crate::fs::in_mem::InMem::::new) .mount("/dev", crate::fs::devices::Devices::new) .build() .unwrap(); @@ -975,9 +905,9 @@ mod tests { ) .with_socket_provider(socket_provider.clone()) .with_random_provider(Arc::new(crate::random::TestRandomProvider)) - .with_file_service(Arc::new(crate::fs::resolver::Resolver::::new( - fs, - ))) + .with_file_service(Arc::new( + crate::fs::resolver::Resolver::::new(fs), + )) .build() .unwrap(); diff --git a/litebox_broker_core/src/socket/tests.rs b/litebox_broker_core/src/socket/tests.rs index 3d8e9ea7a..9a1bc32fe 100644 --- a/litebox_broker_core/src/socket/tests.rs +++ b/litebox_broker_core/src/socket/tests.rs @@ -3,7 +3,6 @@ use super::*; use crate::readiness::tests::TestReadinessSink; -use crate::test_support::TestBrokerCoreBuilder; use crate::{BrokerCore, CallerCredential}; use litebox_broker_protocol::socket::{AddressFamily, IpProtocol, SocketType}; use std::net::Ipv4Addr; @@ -1316,14 +1315,24 @@ fn test_broker_with_policy( socket_provider: Arc, socket_policy: &crate::SocketPolicy, ) -> BrokerCore { - TestBrokerCoreBuilder::new( - crate::PolicyEngine::with_unauthenticated_rights(crate::ObjectRights::all()) - .with_socket_policy(*socket_policy), - ) - .with_limits(crate::BrokerCoreLimits::new_with_all_limits(16, 4, 8, 8)) - .with_socket_provider(socket_provider) - .build() - .unwrap() + BrokerCore { + policy: Arc::new( + crate::PolicyEngine::with_unauthenticated_rights(crate::ObjectRights::all()) + .with_socket_policy(*socket_policy), + ), + limits: crate::BrokerCoreLimits::new_with_all_limits(16, 4, 8, 8), + next_session_id: Arc::new(spin::RwLock::new(1)), + next_reference_handle: Arc::new(spin::RwLock::new(1)), + references: Arc::new(spin::RwLock::new(hashbrown::HashMap::new())), + pending_references: Arc::new(AtomicUsize::new(0)), + reserved_pipe_capacity: Arc::new(AtomicUsize::new(0)), + reserved_sockets: Arc::new(AtomicUsize::new(0)), + random_provider: Arc::new(crate::random::TestRandomProvider), + stdio_provider: Arc::new(crate::stdio::UnsupportedStdioProvider), + socket_provider, + fs: Arc::new(crate::fs::UnsupportedFileService), + socket_ports: BrokerSocketPorts::default(), + } } pub(crate) fn check_socket_lifecycle(broker: &BrokerCore, provider: &TestSocketProvider) { diff --git a/litebox_broker_core/src/test_platform.rs b/litebox_broker_core/src/test_platform.rs index 07fc5e776..b954e9571 100644 --- a/litebox_broker_core/src/test_platform.rs +++ b/litebox_broker_core/src/test_platform.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Host synchronization for broker-core unit tests. +//! Synchronization platform shared by broker-core unit tests. use core::sync::atomic::{AtomicU32, Ordering}; use core::time::Duration; @@ -9,6 +9,7 @@ use core::time::Duration; use litebox_platform::sync::{ImmediatelyWokenUp, RawMutex, RawMutexProvider, UnblockedOrTimedOut}; use std::sync::{Condvar, Mutex}; +/// A [`RawMutex`] built on the host's condition variables. pub(crate) struct TestRawMutex { state: AtomicU32, waiters: Mutex<()>, @@ -26,16 +27,9 @@ impl RawMutex for TestRawMutex { &self.state } - fn wake_many(&self, count: usize) -> usize { + fn wake_many(&self, _count: usize) -> usize { let _waiters = self.waiters.lock().unwrap(); - if count == i32::MAX as usize { - self.wake.notify_all(); - } else { - for _ in 0..count { - self.wake.notify_one(); - } - } - // The host condition variable does not report how many waiters were woken. + self.wake.notify_all(); 0 } @@ -44,7 +38,10 @@ impl RawMutex for TestRawMutex { if self.state.load(Ordering::Acquire) != expected { return Err(ImmediatelyWokenUp); } - let _waiters = self.wake.wait(waiters).unwrap(); + let _waiters = self + .wake + .wait_while(waiters, |()| self.state.load(Ordering::Acquire) == expected) + .unwrap(); Ok(()) } @@ -57,7 +54,12 @@ impl RawMutex for TestRawMutex { if self.state.load(Ordering::Acquire) != expected { return Err(ImmediatelyWokenUp); } - let (_waiters, result) = self.wake.wait_timeout(waiters, timeout).unwrap(); + let (_waiters, result) = self + .wake + .wait_timeout_while(waiters, timeout, |()| { + self.state.load(Ordering::Acquire) == expected + }) + .unwrap(); Ok(if result.timed_out() { UnblockedOrTimedOut::TimedOut } else { @@ -66,6 +68,7 @@ impl RawMutex for TestRawMutex { } } +/// The platform broker-core tests instantiate platform-generic types with. pub(crate) struct TestPlatform; impl RawMutexProvider for TestPlatform { diff --git a/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index e4cdb73e5..6cee9e234 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -796,28 +796,6 @@ mod tests { ); } - #[test] - fn directory_payload_rejects_zero_rdev() { - let mut payload = encode_directory_entries(&[FileDirectoryEntry { - name: "x".into(), - file_type: FileType::CharacterDevice, - ino_info: Some(FileNodeInfo { - dev: 2, - ino: 3, - rdev: NonZeroU64::new(5), - }), - }]) - .unwrap(); - - // `rdev` is the trailing value of the payload's only entry. - let rdev = payload.len() - size_of::(); - payload[rdev..].copy_from_slice(&0u64.to_le_bytes()); - assert_eq!( - decode_directory_entries(&payload), - Err(DirectoryPayloadError::Malformed) - ); - } - #[test] fn directory_payload_chunks_use_entry_indexes() { let entries = [ diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 9bdb6dd7e..77fc786d3 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -1321,26 +1321,6 @@ mod tests { decode_response(&unsupported_mode), Err(WireError::InvalidTag) ); - let mut zero_rdev = encode_response(BrokerResponse { - request_id: TEST_REQUEST_ID, - result: BrokerResult::File(FileResponse::PathStatus(FileStatus { - file_type: FileType::CharacterDevice, - mode: FileMode::from_bits(0o640).unwrap(), - size: 0, - owner: FileUser { user: 2, group: 3 }, - node_info: FileNodeInfo { - dev: 5, - ino: 7, - rdev: NonZeroU64::new(9), - }, - blksize: 4096, - })), - }); - // `rdev` is encoded immediately before the trailing block size. - let rdev = zero_rdev.len() - size_of::() * 2; - zero_rdev[rdev..rdev + size_of::()].copy_from_slice(&0u64.to_le_bytes()); - assert_eq!(decode_response(&zero_rdev), Err(WireError::InvalidTag)); - let mut invalid_next_index = encode_response(BrokerResponse { request_id: TEST_REQUEST_ID, result: BrokerResult::File(FileResponse::ReadDirectory(ReadDirectoryResponse { diff --git a/litebox_shim_linux/src/transport.rs b/litebox_shim_linux/src/transport.rs deleted file mode 100644 index 45c7fe351..000000000 --- a/litebox_shim_linux/src/transport.rs +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Spin-polling TCP transport over the shim's internal network stack. - -use alloc::boxed::Box; -use alloc::sync::Arc; - -use litebox::fs::nine_p::transport; -use litebox::net::socket_channel::{ChannelReadError, ChannelWriteError, NetworkProxy}; -use litebox::net::{ReceiveFlags, SendFlags}; -use litebox_common_linux::{SockFlags, SockType, errno::Errno}; - -use crate::syscalls::net::SocketFd; -use crate::{GlobalState, ShimPlatform}; - -/// Handles socket cleanup on drop without exposing the concrete socket/global-state types. -/// -/// This is stored as `Box` inside [`ShimTransport`] so that the -/// transport itself does not need to name them. -// XXX: this erasure only existed to hide the old `FS` generic. Now that `SocketDropGuard`'s fields -// are nameable from `Platform` alone, we could inline them into [`ShimTransport`] and drop this -// trait. However, this `DropGuard` _may_ be worth keeping if a future non-socket backing (shared -// memory, ...) needs to share `ShimTransport`. -trait DropGuard: Send + Sync { - fn close(&mut self); -} - -/// Concrete, generic implementation of [`DropGuard`]. -struct SocketDropGuard { - global: Arc>, - sockfd: SocketFd, -} - -impl DropGuard for SocketDropGuard { - fn close(&mut self) { - let _ = self - .global - .net - .lock() - .close(&self.sockfd, litebox::net::CloseBehavior::Immediate); - } -} - -/// A spin-polling TCP transport backed by a raw `SocketFd` and its [`NetworkProxy`]. -/// -/// The socket lives in the litebox descriptor table (for metadata / proxy) but is -/// **not** registered in the guest's file-descriptor table, keeping it invisible -/// to the guest program. -/// -/// All I/O goes through the non-blocking [`NetworkProxy`] methods directly -/// (`try_read` / `try_write`), with spin-polling when data is not yet available. -/// This avoids the need for a `WaitState` or any association with a particular -/// guest `Task`. -pub struct ShimTransport { - drop_guard: Box, - proxy: Arc>, -} - -impl ShimTransport { - /// Create a TCP socket, connect it to `addr`, and return a transport. - /// - /// The socket is created via [`litebox::net::Network::socket`] and initialised - /// with [`GlobalState::initialize_socket`] so that the channel-based proxy is - /// set up, but the socket is **not** assigned a guest fd number. - /// - /// Connection and all subsequent I/O use the [`NetworkProxy`] directly, - /// spin-polling when the operation cannot complete immediately. - pub(crate) fn connect( - global: Arc>, - addr: core::net::SocketAddr, - ) -> Result { - // 1. Create the raw socket. - let sockfd = global - .net - .lock() - .socket(litebox::net::Protocol::Tcp) - .map_err(Errno::from)?; - - // 2. Initialise metadata / proxy in the litebox descriptor table. - let proxy = global.initialize_socket(&sockfd, SockType::Stream, SockFlags::empty()); - - // 3. Initiate the TCP connection. - let mut check_progress = false; - loop { - match global.net.lock().connect(&sockfd, &addr, check_progress) { - Ok(()) => break, - Err(litebox::net::errors::ConnectError::InProgress) => { - core::hint::spin_loop(); - check_progress = true; - } - Err(e) => return Err(Errno::from(e)), - } - } - - let drop_guard = Box::new(SocketDropGuard { global, sockfd }); - - Ok(Self { drop_guard, proxy }) - } -} - -impl Drop for ShimTransport { - fn drop(&mut self) { - self.drop_guard.close(); - } -} - -impl transport::Read for ShimTransport { - fn read(&mut self, buf: &mut [u8]) -> Result { - loop { - match self.proxy.try_read(buf, ReceiveFlags::empty(), None) { - Err(ChannelReadError::WouldBlock) => { - // No data yet — spin until something arrives. - core::hint::spin_loop(); - } - Ok(n) => return Ok(n), - Err(_) => return Err(transport::ReadError), - } - } - } -} - -impl transport::Write for ShimTransport { - fn write(&mut self, buf: &[u8]) -> Result { - loop { - match self.proxy.try_write(buf, SendFlags::empty(), None) { - Ok(n) => return Ok(n), - Err(ChannelWriteError::BufferFull) => { - // TX ring full — spin until space opens up. - core::hint::spin_loop(); - } - Err(_) => return Err(transport::WriteError), - } - } - } -} - -// require network support -#[cfg(target_os = "linux")] -#[cfg(test)] -mod tests { - extern crate std; - - use core::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; - use std::net::TcpListener; - use std::path::Path; - - use litebox::fs::nine_p::NineP; - use litebox::fs::resolver::Resolver; - use litebox::fs::{Mode, OFlags}; - - use crate::syscalls::tests::init_platform; - - use super::*; - - fn find_free_port() -> u16 { - let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind to port 0"); - listener.local_addr().unwrap().port() - } - - struct DiodServer { - child: std::process::Child, - port: u16, - _export_dir: tempfile::TempDir, - export_path: std::path::PathBuf, - } - - impl DiodServer { - const MAX_START_ATTEMPTS: usize = 5; - - fn start() -> Self { - let export_dir = tempfile::tempdir().expect("failed to create temp dir"); - let export_path = export_dir.path().to_path_buf(); - - for attempt in 0..Self::MAX_START_ATTEMPTS { - let port = find_free_port(); - - let mut child = std::process::Command::new("diod") - .args([ - "--foreground", - "--no-auth", - "--export", - export_dir.path().to_str().unwrap(), - "--listen", - &std::format!("0.0.0.0:{port}"), - "--nwthreads", - "1", - ]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::piped()) - .spawn() - .expect("failed to start diod – is it installed? (`apt install diod`)"); - - if Self::wait_until_ready(&mut child, port) { - return Self { - child, - port, - _export_dir: export_dir, - export_path, - }; - } - - let _ = child.kill(); - let _ = child.wait(); - if attempt + 1 < Self::MAX_START_ATTEMPTS { - std::eprintln!( - "diod failed to bind to port {port}, retrying ({}/{})…", - attempt + 1, - Self::MAX_START_ATTEMPTS, - ); - } - } - - panic!( - "failed to start diod after {} attempts", - Self::MAX_START_ATTEMPTS, - ); - } - - fn wait_until_ready(child: &mut std::process::Child, port: u16) -> bool { - use std::net::TcpStream; - let addr = std::format!("127.0.0.1:{port}"); - for _ in 0..50 { - if let Some(_status) = child.try_wait().ok().flatten() { - return false; - } - if TcpStream::connect(&addr).is_ok() { - return true; - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - false - } - - fn export_path(&self) -> &Path { - &self.export_path - } - } - - impl Drop for DiodServer { - fn drop(&mut self) { - let _ = self.child.kill(); - let _ = self.child.wait(); - if let Some(mut stderr) = self.child.stderr.take() { - use std::io::Read as _; - let mut output = std::string::String::new(); - let _ = stderr.read_to_string(&mut output); - if !output.is_empty() { - std::eprintln!("--- diod stderr ---\n{output}\n--- end diod stderr ---"); - } - } - } - } - - /// Helper to create a `SocketAddr` for connection. - fn socket_addr(ip: [u8; 4], port: u16) -> SocketAddr { - SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]), - port, - )) - } - - fn connect_9p( - task: &crate::Task, - server: &DiodServer, - ) -> Resolver { - let addr = socket_addr([127, 0, 0, 1], server.port); - let transport = ShimTransport::connect(task.global.clone(), addr) - .expect("failed to connect to 9P server via shim network"); - - let aname = server.export_path().to_str().unwrap(); - let username = std::env::var("USER") - .or_else(|_| std::env::var("LOGNAME")) - .unwrap_or_else(|_| std::string::String::from("nobody")); - - let composer = litebox::fs::composer::Composer::builder() - .mount("/", |allocator| { - NineP::::new( - transport, 65536, &username, aname, allocator, - ) - .expect("failed to create 9P filesystem") - }) - .build() - .expect("a single mount at `/`"); - Resolver::new(&task.global.litebox, composer) - } - - // ----------------------------------------------------------------------- - // Tests (require broker-backed socket setup + diod) - // ----------------------------------------------------------------------- - - #[test] - #[ignore = "requires broker-backed socket test setup"] - fn test_nine_p_create_and_read_file() { - let ctx = litebox::fs::resolver::Context::new(); - let task = init_platform(); - - let server = DiodServer::start(); - let fs = connect_9p(&task, &server); - - // Create a file and write to it. - let fd = fs - .open( - &ctx, - "/hello.txt", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("failed to create file via 9P"); - - let data = b"Hello from litebox shim 9P!"; - let written = fs.write(&fd, data, None).expect("failed to write via 9P"); - assert_eq!(written, data.len()); - fs.close(&fd).expect("failed to close file"); - - // Verify on host. - let host_path = server.export_path().join("hello.txt"); - assert!(host_path.exists(), "file should exist on host"); - let host_content = std::fs::read_to_string(&host_path).unwrap(); - assert_eq!(host_content, "Hello from litebox shim 9P!"); - - // Read back through 9P. - let fd = fs - .open(&ctx, "/hello.txt", OFlags::RDONLY, Mode::empty()) - .expect("failed to open file for reading"); - - let mut buf = alloc::vec![0u8; 256]; - let n = fs.read(&fd, &mut buf, None).expect("failed to read via 9P"); - assert_eq!(&buf[..n], data); - fs.close(&fd).expect("failed to close file"); - } - - #[test] - #[ignore = "requires broker-backed socket test setup"] - fn test_nine_p_host_files_visible() { - let ctx = litebox::fs::resolver::Context::new(); - let task = init_platform(); - - let server = DiodServer::start(); - - // Pre-populate files on the host side. - std::fs::write(server.export_path().join("host_file.txt"), "from host").unwrap(); - std::fs::create_dir(server.export_path().join("host_dir")).unwrap(); - std::fs::write( - server.export_path().join("host_dir/inner.txt"), - "inner content", - ) - .unwrap(); - - let fs = connect_9p(&task, &server); - - // Read file created on the host through 9P. - let fd = fs - .open(&ctx, "/host_file.txt", OFlags::RDONLY, Mode::empty()) - .expect("failed to open host file via 9P"); - let mut buf = alloc::vec![0u8; 256]; - let n = fs.read(&fd, &mut buf, None).unwrap(); - assert_eq!(&buf[..n], b"from host"); - fs.close(&fd).unwrap(); - - // List host directory through 9P. - let fd = fs - .open( - &ctx, - "/host_dir", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .expect("failed to open host dir via 9P"); - let entries = fs.read_dir(&fd).unwrap(); - fs.close(&fd).unwrap(); - - let names: alloc::vec::Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); - assert!( - names.contains(&"inner.txt"), - "host_dir should contain 'inner.txt', got: {names:?}" - ); - } -}