diff --git a/litebox/src/fs/inode_allocator.rs b/litebox/src/fs/inode_allocator.rs index 118596073e..5f7df5eca4 100644 --- a/litebox/src/fs/inode_allocator.rs +++ b/litebox/src/fs/inode_allocator.rs @@ -38,9 +38,15 @@ impl InodeAllocator { pub fn next(&self) -> NodeInfo { let ino = self.counter.fetch_add(1, Ordering::Relaxed); NodeInfo { - dev: self.device_id.try_into().unwrap(), + dev: self.device_id(), ino: ino.try_into().unwrap(), rdev: None, } } + + /// The device id this allocator hands out. + #[must_use] + pub fn device_id(&self) -> usize { + self.device_id.try_into().unwrap() + } } diff --git a/litebox/src/fs/nine_p/client.rs b/litebox/src/fs/nine_p/client.rs index c5d2c453a3..30fac1bc1c 100644 --- a/litebox/src/fs/nine_p/client.rs +++ b/litebox/src/fs/nine_p/client.rs @@ -83,6 +83,24 @@ impl Drop for FidInner { } } +/// The outcome of a [`Client::walk`]. +pub(super) struct WalkResult { + /// The qids of the components that were walked, in order. + pub(super) wqids: Vec, + /// The fid for the final location. + /// + /// `Some` iff `wqids.len() == wnames.len()`: per 9P2000.L, a short walk does not establish a + /// new fid, so a partial walk yields qids but nothing to address them with. + pub(super) fid: Option>, +} + +impl WalkResult { + /// The fid for a walk that reached the requested path, treating a short walk as `ENOENT`. + pub(super) fn into_complete_fid(self) -> Result, Error> { + self.fid.ok_or(Error::Remote(super::ENOENT)) + } +} + /// 9P client state for writing to the connection struct ClientWriteState { /// The underlying transport @@ -307,14 +325,19 @@ impl Client { /// Walks the path from the given fid, handling paths longer than fcall::MAXWELEM by walking in chunks. /// - /// Returns the qids for each path component and a new fid for the final location on success. + /// Returns the qids for each walked path component, along with a new fid for the final + /// location if the whole path was walked. fn walk_chunked( &self, fid: &Fid, wnames: &[FcallStr], - ) -> Result<(Vec, Fid), Error> { + ) -> Result, Error> { if wnames.is_empty() { - return self.walk_once(fid, wnames); + let (wqids, fid) = self.walk_once(fid, wnames)?; + return Ok(WalkResult { + wqids, + fid: Some(fid), + }); } let mut wqids = Vec::with_capacity(fcall::MAXWELEM); let mut prev: Option> = None; @@ -336,22 +359,26 @@ impl Client { } // It means that the walk failed at the nwqid-th element if new_len < chunk.len() { + // XXX: Per 9P2000.L the server does not establish `new_f` on a short walk, so not + // sure why we have a clunk here; it does lead to a round-trip cost (and a + // swallowed `Rlerror`) on every short walk, so might be good to clean up? self.clunk(new_f); - return Err(Error::Remote(super::ENOENT)); + return Ok(WalkResult { wqids, fid: None }); } prev = Some(new_f); } - Ok((wqids, prev.unwrap())) + Ok(WalkResult { + wqids, + fid: Some(prev.unwrap()), + }) } /// Walk to a path from a given fid. - /// - /// Returns the qids for each path component and a new fid for the final location. pub(super) fn walk>( &self, fid: &Fid, wnames: &[S], - ) -> Result<(Vec, Fid), Error> { + ) -> Result, Error> { let wnames: Vec> = wnames .iter() .map(|s| fcall::FcallStr::Borrowed(s.as_ref())) @@ -687,7 +714,6 @@ impl Client { /// Clone a fid (walk with empty path) pub(super) fn clone_fid(&self, fid: &Fid) -> Result, Error> { let empty: [&str; 0] = []; - let (_, new_fid) = self.walk(fid, &empty)?; - Ok(new_fid) + self.walk(fid, &empty)?.into_complete_fid() } } diff --git a/litebox/src/fs/nine_p/mod.rs b/litebox/src/fs/nine_p/mod.rs index 3d58c5c0df..96e026854e 100644 --- a/litebox/src/fs/nine_p/mod.rs +++ b/litebox/src/fs/nine_p/mod.rs @@ -3,9 +3,9 @@ //! A network file system, using the 9P2000.L protocol //! -//! This module provides a [`FileSystem`] implementation that accesses files over a 9P2000.L -//! network connection. The 9P protocol is a simple, message-based protocol originally designed -//! for Plan 9 from Bell Labs. 9P2000.L is a Linux-specific variant that provides better +//! This module provides a [`NineP`] [`Backend`](super::backend::Backend) that accesses files over +//! a 9P2000.L network connection. The 9P protocol is a simple, message-based protocol originally +//! designed for Plan 9 from Bell Labs. 9P2000.L is a Linux-specific variant that provides better //! compatibility with POSIX semantics. use alloc::string::String; @@ -17,13 +17,16 @@ use core::sync::atomic::{AtomicBool, Ordering}; use thiserror::Error; use crate::fs::OFlags; +use crate::fs::backend::{ + DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, + WalkStopReason, WalkedComponent, WalkingDirHandle, +}; use crate::fs::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, - ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, + ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; use crate::fs::nine_p::fcall::Rlerror; -use crate::path::Arg; -use crate::{LiteBox, sync}; +use crate::sync; mod client; mod fcall; @@ -33,740 +36,542 @@ pub mod transport; #[cfg(test)] mod tests; -const DEVICE_ID: usize = u32::from_le_bytes(*b"NINE") as usize; - -// Common POSIX error codes used when converting remote errors to specific FS error types. -const EPERM: u32 = 1; -const ENOENT: u32 = 2; -const EACCES: u32 = 13; -const EEXIST: u32 = 17; -const ENOTDIR: u32 = 20; -const EISDIR: u32 = 21; -const EINVAL: u32 = 22; -const ESPIPE: u32 = 29; -const ENAMETOOLONG: u32 = 36; -const ENOSYS: u32 = 38; -const ENOTEMPTY: u32 = 39; -const EOPNOTSUPP: u32 = 95; - -/// Error type for 9P operations -#[derive(Debug, Error)] -pub enum Error { - #[error("I/O error")] - Io, - - #[error("Invalid response from server")] - InvalidResponse, - - #[error("Invalid pathname")] - InvalidPathname, - - /// Error reported by the 9P server, carrying the raw errno - #[error("Remote error (errno={0})")] - Remote(u32), -} - -impl From for OpenError { - fn from(e: Error) -> Self { - match e { - Error::InvalidPathname => OpenError::PathError(PathError::InvalidPathname), - Error::Remote(errno) => match errno { - ENOENT => OpenError::PathError(PathError::NoSuchFileOrDirectory), - EEXIST => OpenError::AlreadyExists, - EPERM | EACCES => OpenError::AccessNotAllowed, - ENOTDIR => OpenError::PathError(PathError::ComponentNotADirectory), - ENAMETOOLONG => OpenError::PathError(PathError::InvalidPathname), - _ => OpenError::Io, - }, - Error::Io | Error::InvalidResponse => OpenError::Io, - } - } -} - -impl From for ReadError { - fn from(e: Error) -> Self { - match e { - Error::Remote(errno) => match errno { - ENOENT | EISDIR => ReadError::NotAFile, - EPERM | EACCES => ReadError::NotForReading, - _ => ReadError::Io, - }, - Error::Io | Error::InvalidResponse | Error::InvalidPathname => ReadError::Io, - } - } -} - -impl From for WriteError { - fn from(e: Error) -> Self { - match e { - Error::Remote(errno) => match errno { - ENOENT | EISDIR => WriteError::NotAFile, - EPERM | EACCES => WriteError::NotForWriting, - _ => WriteError::Io, - }, - Error::Io | Error::InvalidResponse | Error::InvalidPathname => WriteError::Io, - } - } -} - -impl From for MkdirError { - fn from(e: Error) -> Self { - match e { - Error::InvalidPathname => MkdirError::PathError(PathError::InvalidPathname), - Error::Remote(errno) => match errno { - ENOENT => MkdirError::PathError(PathError::NoSuchFileOrDirectory), - EEXIST => MkdirError::AlreadyExists, - EPERM | EACCES => MkdirError::NoWritePerms, - ENOTDIR => MkdirError::PathError(PathError::ComponentNotADirectory), - ENAMETOOLONG => MkdirError::PathError(PathError::InvalidPathname), - _ => MkdirError::Io, - }, - Error::Io | Error::InvalidResponse => MkdirError::Io, - } - } -} - -impl From for ReadDirError { - fn from(e: Error) -> Self { - match e { - Error::Remote(errno) => match errno { - ENOENT | ENOTDIR => ReadDirError::NotADirectory, - _ => ReadDirError::Io, - }, - Error::Io | Error::InvalidResponse | Error::InvalidPathname => ReadDirError::Io, - } - } -} - -impl From for UnlinkError { - fn from(e: Error) -> Self { - match e { - Error::InvalidPathname => UnlinkError::PathError(PathError::InvalidPathname), - Error::Remote(errno) => match errno { - ENOENT => UnlinkError::PathError(PathError::NoSuchFileOrDirectory), - EISDIR => UnlinkError::IsADirectory, - EPERM | EACCES => UnlinkError::NoWritePerms, - ENOTDIR => UnlinkError::PathError(PathError::ComponentNotADirectory), - ENAMETOOLONG => UnlinkError::PathError(PathError::InvalidPathname), - _ => UnlinkError::Io, - }, - Error::Io | Error::InvalidResponse => UnlinkError::Io, - } - } -} - -impl From for RmdirError { - fn from(e: Error) -> Self { - match e { - Error::InvalidPathname => RmdirError::PathError(PathError::InvalidPathname), - Error::Remote(errno) => match errno { - ENOENT => RmdirError::PathError(PathError::NoSuchFileOrDirectory), - ENOTDIR => RmdirError::NotADirectory, - EPERM | EACCES => RmdirError::NoWritePerms, - ENAMETOOLONG => RmdirError::PathError(PathError::InvalidPathname), - ENOTEMPTY => RmdirError::NotEmpty, - _ => RmdirError::Io, - }, - Error::Io | Error::InvalidResponse => RmdirError::Io, - } - } -} - -impl From for FileStatusError { - fn from(e: Error) -> Self { - match e { - Error::InvalidPathname => FileStatusError::PathError(PathError::InvalidPathname), - Error::Remote(errno) => match errno { - ENOENT => FileStatusError::PathError(PathError::NoSuchFileOrDirectory), - ENAMETOOLONG => FileStatusError::PathError(PathError::InvalidPathname), - ENOTDIR => FileStatusError::PathError(PathError::ComponentNotADirectory), - EPERM | EACCES => FileStatusError::PathError(PathError::NoSearchPerms { - #[cfg(debug_assertions)] - dir: String::new(), - #[cfg(debug_assertions)] - perms: super::Mode::empty(), - }), - _ => FileStatusError::Io, - }, - Error::Io | Error::InvalidResponse => FileStatusError::Io, - } - } -} - -impl From for SeekError { - fn from(e: Error) -> Self { - match e { - Error::Remote(e) => match e { - ENOENT => SeekError::ClosedFd, - EINVAL => SeekError::InvalidOffset, - ESPIPE => SeekError::NonSeekable, - _ => SeekError::Io, - }, - _ => SeekError::Io, - } - } -} - -impl From for TruncateError { - fn from(e: Error) -> Self { - match e { - Error::Remote(errno) => match errno { - ENOENT => TruncateError::ClosedFd, - EISDIR => TruncateError::IsDirectory, - EPERM | EACCES => TruncateError::NotForWriting, - _ => TruncateError::Io, - }, - Error::Io | Error::InvalidResponse | Error::InvalidPathname => TruncateError::Io, - } - } -} - -impl From for ChmodError { - fn from(e: Error) -> Self { - match e { - Error::InvalidPathname => ChmodError::PathError(PathError::InvalidPathname), - Error::Remote(errno) => match errno { - ENOENT => ChmodError::PathError(PathError::NoSuchFileOrDirectory), - ENOTDIR => ChmodError::PathError(PathError::ComponentNotADirectory), - EPERM | EACCES => ChmodError::NotTheOwner, - _ => ChmodError::Io, - }, - Error::Io | Error::InvalidResponse => ChmodError::Io, - } - } -} - -impl From for ChownError { - fn from(e: Error) -> Self { - match e { - Error::InvalidPathname => ChownError::PathError(PathError::InvalidPathname), - Error::Remote(errno) => match errno { - ENOENT => ChownError::PathError(PathError::NoSuchFileOrDirectory), - ENOTDIR => ChownError::PathError(PathError::ComponentNotADirectory), - EPERM | EACCES => ChownError::NotTheOwner, - _ => ChownError::Io, - }, - Error::Io | Error::InvalidResponse => ChownError::Io, - } - } -} - -impl From for Error { - fn from(err: Rlerror) -> Self { - Error::Remote(err.ecode) - } -} - -/// A backing implementation for [`FileSystem`](super::FileSystem) using a 9P2000.L-based network -/// file system. +/// A [`Backend`](super::backend::Backend) backed by a 9P2000.L server. /// /// This filesystem implementation communicates with a 9P server to provide access to remote files. /// All file operations are translated into 9P protocol messages that are sent to the server. /// /// # Type Parameters /// -/// - `Platform`: The platform provider that supplies synchronization primitives and other -/// platform-specific functionality. +/// - `Platform`: The platform provider that supplies synchronization primitives. /// - `T`: The transport type that implements both `Read` and `Write` traits. -pub struct FileSystem< - Platform: sync::RawSyncPrimitivesProvider, - T: transport::Read + transport::Write, -> { - /// Reference to the LiteBox instance - litebox: LiteBox, +pub struct NineP { /// 9P client for protocol operations - client: client::Client, - /// Root (attached to the root of the remote filesystem) - root: (fcall::Qid, client::Fid, String), - // cwd invariant: always ends with a `/` - current_working_dir: String, + client: Arc>, + /// The fid attached to the root of the remote filesystem. + /// + /// Handed out (shared) by [`Backend::root`](super::backend::Backend::root), so it must never + /// be `Tlopen`ed or `Tlcreate`d; see the `is_backend_root` flag on the walking dir handle. + root: Arc>, + /// Device id reported in every [`NodeInfo`](super::NodeInfo) from this backend; inode numbers + /// come from the server's qids instead. + device_id: usize, /// Whether `unlinkat` is supported by the server unlinkat_supported: AtomicBool, } impl - FileSystem + NineP { - /// Construct a new `FileSystem` instance - /// - /// This function is expected to only be invoked once per platform, as an initialization step, - /// and the created `FileSystem` handle is expected to be shared across all usage over the - /// system. + /// Construct a new `NineP` backend, negotiating the protocol version and attaching to `path`. /// /// # Arguments /// - /// * `litebox` - Reference to the LiteBox instance for platform access /// * `transport` - The transport for 9P communication /// * `msize` - Maximum message size to negotiate /// * `username` - Username for authentication /// * `path` - Attach path (typically the root directory path) + /// * `inode_allocator` - Supplies the device id reported for this backend's files /// /// # Errors /// /// Returns an error if version negotiation or attach fails. pub fn new( - litebox: &LiteBox, transport: T, msize: u32, username: &str, path: &str, + inode_allocator: super::inode_allocator::InodeAllocator, ) -> Result { - let client = client::Client::new(transport, msize)?; - let (qid, fid) = client.attach(username, path)?; - + let client = Arc::new(client::Client::new(transport, msize)?); + let (_qid, fid) = client.attach(username, path)?; Ok(Self { - litebox: litebox.clone(), + root: Arc::new(OwnedFid { + fid, + client: Arc::clone(&client), + }), client, - root: (qid, fid, String::from(path)), - current_working_dir: String::from("/"), + device_id: inode_allocator.device_id(), unlinkat_supported: AtomicBool::new(true), }) } - /// Gives the absolute path for `path`, resolving any `.` or `..`s, and making sure to account - /// for any relative paths from current working directory. - /// - /// Note: does NOT account for symlinks. - fn absolute_path(&self, path: impl crate::path::Arg) -> Result { - assert!(self.current_working_dir.ends_with('/')); - let path = path.as_rust_str()?; - if path.starts_with('/') { - // Absolute path - Ok(path.normalized()?) - } else { - // Relative path - Ok((self.current_working_dir.clone() + path.as_rust_str()?).normalized()?) - } + /// Tie a freshly obtained `fid` to this backend's client, so that it is clunked once the last + /// handle referring to it goes away. + fn own(&self, fid: client::Fid) -> Arc> { + Arc::new(OwnedFid { + fid, + client: Arc::clone(&self.client), + }) } - /// Walk to a path and return the fid - fn walk_to(&self, path: &str) -> Result, Error> { - let components: Vec<&str> = path - .normalized_components() - .map_err(|_| Error::InvalidPathname)? - .collect(); - if components.is_empty() { - // Clone the root fid - self.client.clone_fid(&self.root.1) - } else { - let (_, fid) = self.client.walk(&self.root.1, &components)?; - Ok(fid) - } - } + /// Remove `name` from `dir`, via `Tunlinkat` where the server supports it. + fn remove_at( + &self, + dir: &NinePDirHandle, + name: &str, + is_file: bool, + ) -> Result<(), Error> { + const AT_REMOVEDIR: u32 = 0x200; - /// Walk to the parent of a path and return the parent fid and the name of the final component - fn walk_to_parent<'a>(&self, path: &'a str) -> Result<(client::Fid, &'a str), Error> { - let components: Vec<&str> = path - .normalized_components() - .map_err(|_| Error::InvalidPathname)? - .collect(); - if components.is_empty() { - return Err(Error::InvalidPathname); + if self.unlinkat_supported.load(Ordering::SeqCst) { + let result = + self.client + .unlinkat(&dir.fid.fid, name, if is_file { 0 } else { AT_REMOVEDIR }); + if let Err(Error::Remote(ENOSYS | EOPNOTSUPP)) = &result { + self.unlinkat_supported.store(false, Ordering::SeqCst); + // fall back to `remove` + } else { + return result; + } } - let name = components.last().unwrap(); - let parent_components = &components[..components.len() - 1]; - - if parent_components.is_empty() { - let parent_fid = self.client.clone_fid(&self.root.1)?; - Ok((parent_fid, name)) - } else { - let (_, parent_fid) = self.client.walk(&self.root.1, parent_components)?; - Ok((parent_fid, name)) - } + // `Tremove` removes whatever a fid names (and clunks it), so it needs a fid of its own. + let fid = self + .client + .walk(&dir.fid.fid, &[name])? + .into_complete_fid()?; + self.client.remove(fid) } +} - /// Convert FileSystem OFlags to 9P LOpenFlags - fn oflags_to_lopen(flags: super::OFlags) -> fcall::LOpenFlags { - let mut lflags = fcall::LOpenFlags::empty(); - - // Access mode (RDONLY is 0, so we only check for WRONLY and RDWR) - if flags.contains(super::OFlags::RDWR) { - lflags |= fcall::LOpenFlags::O_RDWR; - } else if flags.contains(super::OFlags::WRONLY) { - lflags |= fcall::LOpenFlags::O_WRONLY; - } - // RDONLY is implicit if neither WRONLY nor RDWR - - if flags.contains(super::OFlags::CREAT) { - lflags |= fcall::LOpenFlags::O_CREAT; - } - if flags.contains(super::OFlags::EXCL) { - lflags |= fcall::LOpenFlags::O_EXCL; - } - if flags.contains(super::OFlags::TRUNC) { - lflags |= fcall::LOpenFlags::O_TRUNC; - } - if flags.contains(super::OFlags::APPEND) { - lflags |= fcall::LOpenFlags::O_APPEND; - } - if flags.contains(super::OFlags::DIRECTORY) { - lflags |= fcall::LOpenFlags::O_DIRECTORY; - } - if flags.contains(super::OFlags::NOFOLLOW) { - lflags |= fcall::LOpenFlags::O_NOFOLLOW; - } - if flags.contains(super::OFlags::NONBLOCK) { - lflags |= fcall::LOpenFlags::O_NONBLOCK; - } - if flags.contains(super::OFlags::SYNC) { - lflags |= fcall::LOpenFlags::O_SYNC; - } - if flags.contains(super::OFlags::DSYNC) { - lflags |= fcall::LOpenFlags::O_DSYNC; - } - if flags.contains(super::OFlags::DIRECT) { - lflags |= fcall::LOpenFlags::O_DIRECT; - } - if flags.contains(super::OFlags::NOATIME) { - lflags |= fcall::LOpenFlags::O_NOATIME; - } - - lflags +/// A fid whose server-side state is released when the last handle referring to it goes away. +/// +/// [`Backend`](super::backend::Backend) has no close hook, so the `Tclunk` has to ride on `Drop`. +/// Handles hold this behind an [`Arc`], so incidental handle clones (the resolver passing a clone +/// into a single call) do not clunk; only the last reference does. +struct OwnedFid { + fid: client::Fid, + client: Arc>, +} +impl Drop + for OwnedFid +{ + fn drop(&mut self) { + // `clunk` takes the (refcounted) fid by value; the local id is recycled once this + // `OwnedFid`'s own reference goes away, immediately after this call. + self.client.clunk(self.fid.clone()); } +} - /// Convert a Qid type to our FileType - fn qid_type_to_file_type(qid_type: fcall::QidType) -> super::FileType { - if qid_type.contains(fcall::QidType::DIR) { - super::FileType::Directory - } else { - super::FileType::RegularFile - } - } +/// Walking directory handle +pub struct NinePWalkingDirHandle< + Platform: sync::RawSyncPrimitivesProvider, + T: transport::Read + transport::Write, +> { + inner: NinePWalkingDirHandleInner, +} - /// Convert getattr response to FileStatus - fn rgetattr_to_file_status(attr: &fcall::Rgetattr) -> Result { - let file_type = Self::qid_type_to_file_type(attr.qid.typ); +enum NinePWalkingDirHandleInner< + Platform: sync::RawSyncPrimitivesProvider, + T: transport::Read + transport::Write, +> { + /// A fid on the directory itself. + Dir { + fid: Arc>, + /// Whether `fid` is the backend's own attach fid, handed out by + /// [`Backend::root`](super::backend::Backend::root). + /// + /// Such a fid is shared with the backend itself, so any operation that mutates it + /// server-side (`Tlopen`, `Tlcreate`) must be performed on a private clone instead. + is_backend_root: bool, + }, + /// The walk stopped because `name` is not a directory. + /// + /// No fid on the parent directory is held: 9P walks into files just fine, so the walk already + /// ended up with a fid on `name` itself, which is the only thing the resolver asks for here + /// (see [`Backend::open_file_at`](super::backend::Backend::open_file_at)). + /// + /// `child` is `None` when the path continued *through* the non-directory, as a short walk + /// establishes no fid; the resolver turns that into `ComponentNotADirectory` without ever + /// using this handle. + // XXX: anything this handle is asked for other than `name` itself (a different child via + // `open_file_at`, or the directory via `into_dir`) needs a walk to the parent first; both paths + // are `unimplemented!()` today. + StoppedAtNonDir { + name: String, + child: Option>>, + }, +} - if attr.valid.contains(fcall::GetattrMask::BASIC) { - Ok(super::FileStatus { - file_type, - mode: super::Mode::from_bits_truncate(attr.stat.mode), - size: usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)?, - owner: super::UserInfo { - user: u16::try_from(attr.stat.uid).map_err(|_| Error::InvalidResponse)?, - group: u16::try_from(attr.stat.gid).map_err(|_| Error::InvalidResponse)?, - }, - node_info: super::NodeInfo { - dev: DEVICE_ID, - ino: usize::try_from(attr.qid.path).map_err(|_| Error::InvalidResponse)?, - rdev: NonZeroUsize::new( - usize::try_from(attr.stat.rdev).map_err(|_| Error::InvalidResponse)?, - ), - }, - blksize: usize::try_from(attr.stat.blksize).map_err(|_| Error::InvalidResponse)?, - }) - } else { - Ok(super::FileStatus { - file_type, - mode: if attr.valid.contains(fcall::GetattrMask::MODE) { - super::Mode::from_bits_truncate(attr.stat.mode) - } else { - super::Mode::empty() - }, - size: if attr.valid.contains(fcall::GetattrMask::SIZE) { - usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)? - } else { - 0 - }, - owner: super::UserInfo { - user: if attr.valid.contains(fcall::GetattrMask::UID) { - u16::try_from(attr.stat.uid).map_err(|_| Error::InvalidResponse)? - } else { - 0 - }, - group: if attr.valid.contains(fcall::GetattrMask::GID) { - u16::try_from(attr.stat.gid).map_err(|_| Error::InvalidResponse)? - } else { - 0 - }, - }, - node_info: super::NodeInfo { - dev: DEVICE_ID, - ino: usize::try_from(attr.qid.path).map_err(|_| Error::InvalidResponse)?, - rdev: if attr.valid.contains(fcall::GetattrMask::RDEV) { - NonZeroUsize::new( - usize::try_from(attr.stat.rdev).map_err(|_| Error::InvalidResponse)?, - ) - } else { - None - }, - }, - blksize: if attr.valid.contains(fcall::GetattrMask::BLOCKS) { - usize::try_from(attr.stat.blksize).map_err(|_| Error::InvalidResponse)? - } else { - 0 - }, - }) - } +impl + From> for NinePWalkingDirHandle +{ + fn from(inner: NinePWalkingDirHandleInner) -> Self { + Self { inner } } +} - fn remove_file_or_dir(&self, path: impl crate::path::Arg, is_file: bool) -> Result<(), Error> { - const AT_REMOVEDIR: u32 = 0x200; - - let path = self - .absolute_path(path) - .map_err(|_| Error::InvalidPathname)?; - if self.unlinkat_supported.load(Ordering::SeqCst) { - let (parent_fid, name) = self.walk_to_parent(&path)?; - - let result = - self.client - .unlinkat(&parent_fid, name, if is_file { 0 } else { AT_REMOVEDIR }); - self.client.clunk(parent_fid); - if let Err(Error::Remote(ENOSYS | EOPNOTSUPP)) = &result { - self.unlinkat_supported.store(false, Ordering::SeqCst); - // fall back to `remove` - } else { - return result; +impl + NinePWalkingDirHandle +{ + /// The fid of the directory this handle names, and whether it is the backend's shared root. + fn into_dir(self) -> (Arc>, bool) { + match self.inner { + NinePWalkingDirHandleInner::Dir { + fid, + is_backend_root, + } => (fid, is_backend_root), + // XXX: reaching the parent directory of a walk that stopped at a non-directory would + // need a second walk (from the fid the walk started at, back down the prefix); nothing + // currently needs it, as the resolver only ever opens the child. + NinePWalkingDirHandleInner::StoppedAtNonDir { .. } => { + unimplemented!() } } + } +} - let fid = self.walk_to(&path)?; - self.client.remove(fid) +/// Directory handle +pub struct NinePDirHandle< + Platform: sync::RawSyncPrimitivesProvider, + T: transport::Read + transport::Write, +> { + fid: Arc>, +} +impl Clone + for NinePDirHandle +{ + fn clone(&self) -> Self { + Self { + fid: Arc::clone(&self.fid), + } } } -impl Drop - for FileSystem +/// File handle +pub struct NinePFileHandle< + Platform: sync::RawSyncPrimitivesProvider, + T: transport::Read + transport::Write, +> { + fid: Arc>, +} +impl Clone + for NinePFileHandle { - fn drop(&mut self) { - self.client.clunk(self.root.1.clone()); + fn clone(&self) -> Self { + Self { + fid: Arc::clone(&self.fid), + } } } impl - super::private::Sealed for FileSystem + super::backend::private::Sealed for NineP { } -impl - super::FileSystem for FileSystem +impl super::backend::BackendHandles for NineP +where + Platform: sync::RawSyncPrimitivesProvider + 'static, + T: transport::Read + transport::Write + Send + 'static, { - #[allow(clippy::similar_names)] - fn open( - &self, - path: impl crate::path::Arg, - flags: super::OFlags, - mode: super::Mode, - ) -> Result, super::errors::OpenError> { - // TODO: we don't support non-blocking, so ignore that flag instead of returning an error - let flags = flags - OFlags::NONBLOCK; - let currently_supported_oflags: OFlags = OFlags::RDONLY - | OFlags::WRONLY - | OFlags::RDWR - | OFlags::CREAT - | OFlags::NOCTTY - | OFlags::EXCL - | OFlags::DIRECTORY - | OFlags::LARGEFILE; - if flags.intersects(currently_supported_oflags.complement()) { - unimplemented!("{flags:?}") - } + type WalkingDirHandle<'a> = NinePWalkingDirHandle; + type FileHandle = NinePFileHandle; + type DirHandle = NinePDirHandle; +} - let path = self.absolute_path(path)?; - let components: Vec<&str> = path - .normalized_components() - .map_err(|_| OpenError::PathError(PathError::InvalidPathname))? - .collect(); - let lflags = Self::oflags_to_lopen(flags); - let needs_create = flags.contains(super::OFlags::CREAT); - - let (new_qid, new_fid) = if needs_create { - let (_, dfid) = self - .client - .walk(&self.root.1, &components[..components.len() - 1])?; - self.client - .create(dfid, components.last().unwrap(), lflags, mode.bits(), 0)? - } else { - let (_, new_fid) = self.client.walk(&self.root.1, &components)?; - let qid = match self.client.open(&new_fid, lflags) { - Ok(qid) => qid, - Err(err) => { - self.client.clunk(new_fid); - return Err(err.into()); - } +impl super::backend::Backend for NineP +where + Platform: sync::RawSyncPrimitivesProvider + 'static, + T: transport::Read + transport::Write + Send + 'static, +{ + fn root(&self) -> WalkingDirHandle<'_> { + WalkingDirHandle::from_typed::( + NinePWalkingDirHandleInner::Dir { + fid: Arc::clone(&self.root), + is_backend_root: true, + } + .into(), + ) + } + + fn walk_directories<'a>( + &'a self, + from: WalkingDirHandle<'a>, + components: &[&str], + ) -> Result>, WalkError> { + assert!(!components.is_empty()); + let (from, _) = from.into_typed::().into_dir(); + // 9P walks happily into files, so the qids have to be inspected to find where this walk + // must stop for the resolver's purposes. + let result = self.client.walk(&from.fid, components)?; + let first_non_dir = result + .wqids + .iter() + .position(|qid| !qid.typ.contains(fcall::QidType::DIR)); + + let Some(stopped_at) = first_non_dir else { + let Some(fid) = result.fid else { + // A short walk whose walked components are all directories means the next + // component simply does not exist. + return Err(WalkError::PathError(PathError::NoSuchFileOrDirectory)); }; - (qid, new_fid) - }; - - let descriptor = Descriptor { - fid: new_fid, - offset: Arc::new(sync::Mutex::new(0)), - qid: new_qid, + debug_assert_eq!(result.wqids.len(), components.len()); + return Ok(WalkOutcome { + components: backend_checked_components(components.len()), + last: WalkingDirHandle::from_typed::( + NinePWalkingDirHandleInner::Dir { + fid: self.own(fid), + is_backend_root: false, + } + .into(), + ), + stop_reason: WalkStopReason::CompleteDirectory, + }); }; - let fd = self.litebox.descriptor_table_mut().insert(descriptor); - Ok(fd) - } - - fn close(&self, fd: &FileFd) -> Result<(), super::errors::CloseError> { - let entry = self.litebox.descriptor_table_mut().remove(fd); - if let Some(entry) = entry { - self.client.clunk(entry.entry.fid); - } - Ok(()) + let child = result.fid.map(|fid| { + // A completed walk lands on the last component, so its fid names the non-directory the + // walk stopped at. Anything else would mean the server walked *through* a + // non-directory, which 9P2000.L does not permit. + assert_eq!( + stopped_at + 1, + components.len(), + "server completed a walk through a non-directory" + ); + // Holding on to the fid saves `open_file_at` a walk of its own. + self.own(fid) + }); + Ok(WalkOutcome { + components: backend_checked_components(stopped_at), + last: WalkingDirHandle::from_typed::( + NinePWalkingDirHandleInner::StoppedAtNonDir { + name: String::from(components[stopped_at]), + child, + } + .into(), + ), + stop_reason: WalkStopReason::StoppedAtNonDirectory, + }) } - fn read( + fn owned_dir_at( &self, - fd: &FileFd, - buf: &mut [u8], - offset: Option, - ) -> Result { - // Clone the fid and offset lock out of the descriptor and release the - // table lock before issuing the potentially blocking 9P call. The fid - // keeps the pool slot reserved while the offset lock serializes - // implicit-offset I/O on this descriptor. - let (fid, descriptor_offset) = self - .litebox - .descriptor_table() - .with_entry(fd, |desc| { - (desc.entry.fid.clone(), Arc::clone(&desc.entry.offset)) - }) - .ok_or(super::errors::ReadError::ClosedFd)?; - - if let Some(read_offset) = offset { - return Ok(self.client.read(&fid, read_offset as u64, buf)?); - } - - let mut current_offset = descriptor_offset.lock(); - let bytes_read = self.client.read(&fid, *current_offset as u64, buf)?; - *current_offset = current_offset - .checked_add(bytes_read) - .ok_or(super::errors::ReadError::Io)?; - Ok(bytes_read) + dir: WalkingDirHandle<'_>, + flags: OFlags, + ) -> Result { + assert_supported_oflags(flags); + if flags.intersects(OFlags::WRONLY | OFlags::RDWR) { + // TODO(jayb): POSIX requires `EISDIR` when write access is requested on a directory, + // but `OpenError` has no such variant yet. + unimplemented!() + } + let (fid, is_backend_root) = dir.into_typed::().into_dir(); + if flags.contains(OFlags::PATH) { + // An `O_PATH` handle is never opened server-side, so the walked fid can be handed over + // as-is, even when it is the shared root fid. + return Ok(DirHandle::from_typed::(NinePDirHandle { fid })); + } + // `Tlopen` mutates the fid server-side, so it must never be issued on the shared root fid. + let fid = if is_backend_root { + self.own(self.client.clone_fid(&fid.fid)?) + } else { + fid + }; + self.client.open(&fid.fid, fcall::LOpenFlags::O_DIRECTORY)?; + Ok(DirHandle::from_typed::(NinePDirHandle { fid })) } - fn write( - &self, - fd: &FileFd, - buf: &[u8], - offset: Option, - ) -> Result { - let (fid, descriptor_offset) = self - .litebox - .descriptor_table() - .with_entry(fd, |desc| { - (desc.entry.fid.clone(), Arc::clone(&desc.entry.offset)) - }) - .ok_or(super::errors::WriteError::ClosedFd)?; - - if let Some(write_offset) = offset { - return Ok(self.client.write(&fid, write_offset as u64, buf)?); - } - - let mut current_offset = descriptor_offset.lock(); - let bytes_written = self.client.write(&fid, *current_offset as u64, buf)?; - *current_offset = current_offset - .checked_add(bytes_written) - .ok_or(super::errors::WriteError::Io)?; - Ok(bytes_written) + fn walking_dir_at<'a>(&'a self, dir: &DirHandle) -> Option> { + // The walking handle can end up being opened (via `owned_dir_at`), which must not affect + // the directory handle it came from, so this hands out a private clone of the fid. + let fid = self + .client + .clone_fid(&dir.get_typed::().fid.fid) + .ok()?; + Some(WalkingDirHandle::from_typed::( + NinePWalkingDirHandleInner::Dir { + fid: self.own(fid), + is_backend_root: false, + } + .into(), + )) } - fn seek( + fn open_file_at( &self, - fd: &FileFd, - offset: isize, - whence: super::SeekWhence, - ) -> Result { - let (fid, descriptor_offset) = self - .litebox - .descriptor_table() - .with_entry(fd, |desc| { - (desc.entry.fid.clone(), Arc::clone(&desc.entry.offset)) - }) - .ok_or(SeekError::ClosedFd)?; - - let new_offset = match whence { - super::SeekWhence::RelativeToBeginning => 0, - super::SeekWhence::RelativeToCurrentOffset => { - let mut current_offset = descriptor_offset.lock(); - let new_offset = current_offset - .checked_add_signed(offset) - .ok_or(SeekError::InvalidOffset)?; - *current_offset = new_offset; - return Ok(new_offset); - } - super::SeekWhence::RelativeToEnd => { - let attr = self.client.getattr(&fid, fcall::GetattrMask::SIZE)?; - usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)? + dir: WalkingDirHandle<'_>, + name: &str, + flags: OFlags, + ) -> Result, OpenError> { + assert_supported_oflags(flags); + // TODO: we do not support non-blocking, so ignore that flag instead of returning an error. + let flags = flags - OFlags::NONBLOCK; + if flags.contains(OFlags::DIRECTORY) { + return Err(OpenError::PathError(PathError::ComponentNotADirectory)); + } + + let fid = match dir.into_typed::().inner { + // The walk already ended up holding a fid on this very file. + NinePWalkingDirHandleInner::StoppedAtNonDir { + name: walked, + child: Some(child), + } if walked == name => child, + NinePWalkingDirHandleInner::StoppedAtNonDir { .. } => unimplemented!("{name}"), + NinePWalkingDirHandleInner::Dir { fid, .. } => { + self.own(self.client.walk(&fid.fid, &[name])?.into_complete_fid()?) } - } - .checked_add_signed(offset) - .ok_or(SeekError::InvalidOffset)?; + }; - *descriptor_offset.lock() = new_offset; - Ok(new_offset) + if !flags.contains(OFlags::PATH) { + // An `O_PATH` handle addresses the file without opening it server-side. + // + // The file exists (it is what stopped the walk), so the creation flags say nothing + // about how to open it; the resolver enforces `O_CREAT | O_EXCL` itself. + self.client.open( + &fid.fid, + oflags_to_lopen(flags - OFlags::CREAT - OFlags::EXCL), + )?; + } + Ok(Permissioned { + item: FileHandle::from_typed::(NinePFileHandle { fid }), + permissions: PermissionCheck::ByBackend, + }) } - fn truncate( - &self, - fd: &FileFd, - length: usize, - reset_offset: bool, - ) -> Result<(), super::errors::TruncateError> { - let (fid, qid, descriptor_offset) = self - .litebox - .descriptor_table() - .with_entry(fd, |desc| { - ( - desc.entry.fid.clone(), - desc.entry.qid, - Arc::clone(&desc.entry.offset), - ) + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + let handle = handle.into_typed::(); + let entries = self.client.readdir_all(&handle.fid.fid)?; + Ok(entries + .into_iter() + .filter(|entry| { + // The resolver synthesizes `.` and `..` itself. + // + // XXX(jayb): would it be better to allow `list_dir_at` to handle `.` and `..` and + // have the resolver handle only cases where it is not handled by the backend? + !matches!(&*entry.name, b"." | b"..") + }) + .map(|entry| { + Ok(super::DirEntry { + name: String::from_utf8_lossy(&entry.name).into_owned(), + file_type: qid_type_to_file_type(entry.qid.typ), + ino_info: Some(super::NodeInfo { + dev: self.device_id, + ino: usize::try_from(entry.qid.path).map_err(|_| Error::InvalidResponse)?, + rdev: None, + }), + }) }) - .ok_or(super::errors::TruncateError::ClosedFd)?; + .collect::>()?) + } - if qid.typ.contains(fcall::QidType::DIR) { - return Err(super::errors::TruncateError::IsDirectory); - } + fn read(&self, h: &FileHandle, buf: &mut [u8], offset: usize) -> Result { + let offset = u64::try_from(offset).map_err(|_| ReadError::Io)?; + Ok(self + .client + .read(&h.get_typed::().fid.fid, offset, buf)?) + } + fn write(&self, h: &FileHandle, buf: &[u8], offset: usize) -> Result { + let offset = u64::try_from(offset).map_err(|_| WriteError::Io)?; + Ok(self + .client + .write(&h.get_typed::().fid.fid, offset, buf)?) + } + + fn truncate(&self, h: &FileHandle, length: usize) -> Result<(), TruncateError> { let stat = fcall::SetAttr { - mode: 0, - uid: 0, - gid: 0, - size: length as u64, + size: u64::try_from(length).map_err(|_| TruncateError::Io)?, ..Default::default() }; + self.client.setattr( + &h.get_typed::().fid.fid, + fcall::SetattrMask::SIZE, + stat, + )?; + Ok(()) + } - self.client.setattr(&fid, fcall::SetattrMask::SIZE, stat)?; - - if reset_offset { - *descriptor_offset.lock() = 0; - } + fn seek_behavior(&self, _h: &FileHandle) -> SeekBehavior { + // 9P has no server-side file position; the resolver owns positions and passes offsets in. + SeekBehavior::PositionBased + } - Ok(()) + fn status(&self, h: HandleRef<'_>) -> Result { + let fid = match h { + HandleRef::File(h) => &h.get_typed::().fid, + HandleRef::Dir(h) => &h.get_typed::().fid, + }; + let attr = self.client.getattr(&fid.fid, fcall::GetattrMask::ALL)?; + Ok(rgetattr_to_file_status(&attr, self.device_id)?) } - fn chmod( + fn create_file_at( &self, - path: impl crate::path::Arg, + dir: DirHandle, + name: &str, mode: super::Mode, - ) -> Result<(), super::errors::ChmodError> { - let path = self.absolute_path(path)?; - let fid = self.walk_to(&path)?; + ) -> Result { + // `Tlcreate` turns the directory fid into the new file's fid server-side, so it must be + // handed a private clone rather than the caller's directory handle. + let fid = self.client.clone_fid(&dir.get_typed::().fid.fid)?; + // NOTE: 9P needs to commit to an access mode at creation time. The resolver still enforces + // the caller's read/write intent via its own `read_allowed`/`write_allowed`. + let (_, fid) = self + .client + .create(fid, name, fcall::LOpenFlags::O_RDWR, mode.bits(), 0)?; + Ok(FileHandle::from_typed::(NinePFileHandle { + fid: self.own(fid), + })) + } + + fn mkdir_at( + &self, + dir: DirHandle, + name: &str, + mode: super::Mode, + ) -> Result { + let dir = dir.into_typed::(); + self.client.mkdir(&dir.fid.fid, name, mode.bits(), 0)?; + // `Tmkdir` only reports the new directory's qid, so a walk is needed to address it. + // + // TODO(jayb): the resolver discards this handle, so the walk is pure overhead, and worse, a + // walk that fails (connection loss, or a concurrent removal) reports a `Tmkdir` that + // already succeeded as a failure. I should decide if having `mkdir_at` return a dir is the + // right move, or I want to remove that behavior. + let fid = self + .client + .walk(&dir.fid.fid, &[name])? + .into_complete_fid()?; + Ok(DirHandle::from_typed::(NinePDirHandle { + fid: self.own(fid), + })) + } + + fn unlink_at(&self, dir: DirHandle, name: &str) -> Result<(), UnlinkError> { + Ok(self.remove_at(&dir.into_typed::(), name, true)?) + } + + fn rmdir_at(&self, dir: DirHandle, name: &str) -> Result<(), RmdirError> { + Ok(self.remove_at(&dir.into_typed::(), name, false)?) + } + fn chmod(&self, h: HandleRef<'_>, mode: super::Mode) -> Result<(), ChmodError> { + let fid = match h { + HandleRef::File(h) => &h.get_typed::().fid, + HandleRef::Dir(h) => &h.get_typed::().fid, + }; let stat = fcall::SetAttr { mode: mode.bits(), ..Default::default() }; - - let result = self.client.setattr(&fid, fcall::SetattrMask::MODE, stat); - self.client.clunk(fid); - - result.map_err(ChmodError::from) + Ok(self + .client + .setattr(&fid.fid, fcall::SetattrMask::MODE, stat)?) } fn chown( &self, - path: impl crate::path::Arg, + h: HandleRef<'_>, user: Option, group: Option, - ) -> Result<(), super::errors::ChownError> { - let path = self.absolute_path(path)?; - let fid = self.walk_to(&path)?; - + ) -> Result<(), ChownError> { + let fid = match h { + HandleRef::File(h) => &h.get_typed::().fid, + HandleRef::Dir(h) => &h.get_typed::().fid, + }; + // Only the fields actually supplied are marked valid, so the rest are left alone. let mut valid = fcall::SetattrMask::empty(); let uid = match user { Some(u) => { @@ -787,120 +592,412 @@ impl Vec { + alloc::vec![ + WalkedComponent { + permissions: PermissionCheck::ByBackend + }; + count + ] +} - result.map_err(ChownError::from) +/// Flags this backend knows how to honor when opening files/directories. +const SUPPORTED_OFLAGS: OFlags = OFlags::CREAT + .union(OFlags::RDONLY) + .union(OFlags::WRONLY) + .union(OFlags::RDWR) + .union(OFlags::TRUNC) + .union(OFlags::NOCTTY) + .union(OFlags::EXCL) + .union(OFlags::DIRECTORY) + .union(OFlags::NONBLOCK) + .union(OFlags::LARGEFILE) + .union(OFlags::NOFOLLOW) + .union(OFlags::APPEND) + .union(OFlags::PATH); + +fn assert_supported_oflags(flags: OFlags) { + if flags.intersects(SUPPORTED_OFLAGS.complement()) { + unimplemented!("{flags:?}") } +} + +/// Convert [`OFlags`] to 9P `LOpenFlags` +fn oflags_to_lopen(flags: OFlags) -> fcall::LOpenFlags { + let mut lflags = fcall::LOpenFlags::empty(); - fn unlink(&self, path: impl crate::path::Arg) -> Result<(), super::errors::UnlinkError> { - self.remove_file_or_dir(path, true) - .map_err(UnlinkError::from) + // Access mode (RDONLY is 0, so we only check for WRONLY and RDWR) + if flags.contains(OFlags::RDWR) { + lflags |= fcall::LOpenFlags::O_RDWR; + } else if flags.contains(OFlags::WRONLY) { + lflags |= fcall::LOpenFlags::O_WRONLY; } + // RDONLY is implicit if neither WRONLY nor RDWR - fn mkdir(&self, path: impl crate::path::Arg, mode: super::Mode) -> Result<(), MkdirError> { - let path = self.absolute_path(path)?; + if flags.contains(OFlags::CREAT) { + lflags |= fcall::LOpenFlags::O_CREAT; + } + if flags.contains(OFlags::EXCL) { + lflags |= fcall::LOpenFlags::O_EXCL; + } + if flags.contains(OFlags::TRUNC) { + lflags |= fcall::LOpenFlags::O_TRUNC; + } + if flags.contains(OFlags::APPEND) { + lflags |= fcall::LOpenFlags::O_APPEND; + } + if flags.contains(OFlags::DIRECTORY) { + lflags |= fcall::LOpenFlags::O_DIRECTORY; + } + if flags.contains(OFlags::NOFOLLOW) { + lflags |= fcall::LOpenFlags::O_NOFOLLOW; + } + if flags.contains(OFlags::NONBLOCK) { + lflags |= fcall::LOpenFlags::O_NONBLOCK; + } + if flags.contains(OFlags::SYNC) { + lflags |= fcall::LOpenFlags::O_SYNC; + } + if flags.contains(OFlags::DSYNC) { + lflags |= fcall::LOpenFlags::O_DSYNC; + } + if flags.contains(OFlags::DIRECT) { + lflags |= fcall::LOpenFlags::O_DIRECT; + } + if flags.contains(OFlags::NOATIME) { + lflags |= fcall::LOpenFlags::O_NOATIME; + } - let (parent_fid, name) = self.walk_to_parent(&path)?; + lflags +} - let result = self.client.mkdir(&parent_fid, name, mode.bits(), 0); - self.client.clunk(parent_fid); +/// Convert a Qid type to our FileType +fn qid_type_to_file_type(qid_type: fcall::QidType) -> super::FileType { + if qid_type.contains(fcall::QidType::DIR) { + super::FileType::Directory + } else { + super::FileType::RegularFile + } +} - result.map(|_| ()).map_err(MkdirError::from) +/// Convert getattr response to FileStatus +/// +/// Inode numbers come from the server's qids; `device_id` is the device the caller reports this +/// filesystem as. +fn rgetattr_to_file_status( + attr: &fcall::Rgetattr, + device_id: usize, +) -> Result { + let file_type = qid_type_to_file_type(attr.qid.typ); + + if attr.valid.contains(fcall::GetattrMask::BASIC) { + Ok(super::FileStatus { + file_type, + mode: super::Mode::from_bits_truncate(attr.stat.mode), + size: usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)?, + owner: super::UserInfo { + user: u16::try_from(attr.stat.uid).map_err(|_| Error::InvalidResponse)?, + group: u16::try_from(attr.stat.gid).map_err(|_| Error::InvalidResponse)?, + }, + node_info: super::NodeInfo { + dev: device_id, + ino: usize::try_from(attr.qid.path).map_err(|_| Error::InvalidResponse)?, + rdev: NonZeroUsize::new( + usize::try_from(attr.stat.rdev).map_err(|_| Error::InvalidResponse)?, + ), + }, + blksize: usize::try_from(attr.stat.blksize).map_err(|_| Error::InvalidResponse)?, + }) + } else { + Ok(super::FileStatus { + file_type, + mode: if attr.valid.contains(fcall::GetattrMask::MODE) { + super::Mode::from_bits_truncate(attr.stat.mode) + } else { + super::Mode::empty() + }, + size: if attr.valid.contains(fcall::GetattrMask::SIZE) { + usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)? + } else { + 0 + }, + owner: super::UserInfo { + user: if attr.valid.contains(fcall::GetattrMask::UID) { + u16::try_from(attr.stat.uid).map_err(|_| Error::InvalidResponse)? + } else { + 0 + }, + group: if attr.valid.contains(fcall::GetattrMask::GID) { + u16::try_from(attr.stat.gid).map_err(|_| Error::InvalidResponse)? + } else { + 0 + }, + }, + node_info: super::NodeInfo { + dev: device_id, + ino: usize::try_from(attr.qid.path).map_err(|_| Error::InvalidResponse)?, + rdev: if attr.valid.contains(fcall::GetattrMask::RDEV) { + NonZeroUsize::new( + usize::try_from(attr.stat.rdev).map_err(|_| Error::InvalidResponse)?, + ) + } else { + None + }, + }, + blksize: if attr.valid.contains(fcall::GetattrMask::BLOCKS) { + usize::try_from(attr.stat.blksize).map_err(|_| Error::InvalidResponse)? + } else { + 0 + }, + }) } +} + +// Common POSIX error codes used when converting remote errors to specific FS error types. +const EPERM: u32 = 1; +const ENOENT: u32 = 2; +const EACCES: u32 = 13; +const EEXIST: u32 = 17; +const ENOTDIR: u32 = 20; +const EISDIR: u32 = 21; +const EINVAL: u32 = 22; +const ESPIPE: u32 = 29; +const ENAMETOOLONG: u32 = 36; +const ENOSYS: u32 = 38; +const ENOTEMPTY: u32 = 39; +const EOPNOTSUPP: u32 = 95; - fn rmdir(&self, path: impl crate::path::Arg) -> Result<(), RmdirError> { - self.remove_file_or_dir(path, false) - .map_err(RmdirError::from) +/// Error type for 9P operations +#[derive(Debug, Error)] +pub enum Error { + #[error("I/O error")] + Io, + + #[error("Invalid response from server")] + InvalidResponse, + + #[error("Invalid pathname")] + InvalidPathname, + + /// Error reported by the 9P server, carrying the raw errno + #[error("Remote error (errno={0})")] + Remote(u32), +} + +impl From for OpenError { + fn from(e: Error) -> Self { + match e { + Error::InvalidPathname => OpenError::PathError(PathError::InvalidPathname), + Error::Remote(errno) => match errno { + ENOENT => OpenError::PathError(PathError::NoSuchFileOrDirectory), + EEXIST => OpenError::AlreadyExists, + EPERM | EACCES => OpenError::AccessNotAllowed, + ENOTDIR => OpenError::PathError(PathError::ComponentNotADirectory), + ENAMETOOLONG => OpenError::PathError(PathError::InvalidPathname), + _ => OpenError::Io, + }, + Error::Io | Error::InvalidResponse => OpenError::Io, + } } +} - fn read_dir( - &self, - fd: &FileFd, - ) -> Result, super::errors::ReadDirError> { - let (fid, qid) = self - .litebox - .descriptor_table() - .with_entry(fd, |desc| (desc.entry.fid.clone(), desc.entry.qid)) - .ok_or(super::errors::ReadDirError::ClosedFd)?; - - if !qid.typ.contains(fcall::QidType::DIR) { - return Err(super::errors::ReadDirError::NotADirectory); +impl From for ReadError { + fn from(e: Error) -> Self { + match e { + Error::Remote(errno) => match errno { + ENOENT | EISDIR => ReadError::NotAFile, + EPERM | EACCES => ReadError::NotForReading, + _ => ReadError::Io, + }, + Error::Io | Error::InvalidResponse | Error::InvalidPathname => ReadError::Io, } + } +} - let entries = self.client.readdir_all(&fid)?; +impl From for WriteError { + fn from(e: Error) -> Self { + match e { + Error::Remote(errno) => match errno { + ENOENT | EISDIR => WriteError::NotAFile, + EPERM | EACCES => WriteError::NotForWriting, + _ => WriteError::Io, + }, + Error::Io | Error::InvalidResponse | Error::InvalidPathname => WriteError::Io, + } + } +} - let dir_entries: Vec = entries - .into_iter() - .map(|e| { - let file_type = if e.typ == fcall::QidType::DIR.bits() { - super::FileType::Directory - } else { - super::FileType::RegularFile - }; +impl From for MkdirError { + fn from(e: Error) -> Self { + match e { + Error::InvalidPathname => MkdirError::PathError(PathError::InvalidPathname), + Error::Remote(errno) => match errno { + ENOENT => MkdirError::PathError(PathError::NoSuchFileOrDirectory), + EEXIST => MkdirError::AlreadyExists, + EPERM | EACCES => MkdirError::NoWritePerms, + ENOTDIR => MkdirError::PathError(PathError::ComponentNotADirectory), + ENAMETOOLONG => MkdirError::PathError(PathError::InvalidPathname), + _ => MkdirError::Io, + }, + Error::Io | Error::InvalidResponse => MkdirError::Io, + } + } +} - Ok(super::DirEntry { - name: String::from_utf8_lossy(&e.name).into_owned(), - file_type, - ino_info: Some(super::NodeInfo { - dev: DEVICE_ID, - ino: usize::try_from(e.qid.path).map_err(|_| Error::InvalidResponse)?, - rdev: None, - }), - }) - }) - .collect::>()?; +impl From for ReadDirError { + fn from(e: Error) -> Self { + match e { + Error::Remote(errno) => match errno { + ENOENT | ENOTDIR => ReadDirError::NotADirectory, + _ => ReadDirError::Io, + }, + Error::Io | Error::InvalidResponse | Error::InvalidPathname => ReadDirError::Io, + } + } +} - Ok(dir_entries) +impl From for UnlinkError { + fn from(e: Error) -> Self { + match e { + Error::InvalidPathname => UnlinkError::PathError(PathError::InvalidPathname), + Error::Remote(errno) => match errno { + ENOENT => UnlinkError::PathError(PathError::NoSuchFileOrDirectory), + EISDIR => UnlinkError::IsADirectory, + EPERM | EACCES => UnlinkError::NoWritePerms, + ENOTDIR => UnlinkError::PathError(PathError::ComponentNotADirectory), + ENAMETOOLONG => UnlinkError::PathError(PathError::InvalidPathname), + _ => UnlinkError::Io, + }, + Error::Io | Error::InvalidResponse => UnlinkError::Io, + } } +} - fn file_status( - &self, - path: impl crate::path::Arg, - ) -> Result { - let path = self.absolute_path(path)?; - let fid = self.walk_to(&path)?; +impl From for RmdirError { + fn from(e: Error) -> Self { + match e { + Error::InvalidPathname => RmdirError::PathError(PathError::InvalidPathname), + Error::Remote(errno) => match errno { + ENOENT => RmdirError::PathError(PathError::NoSuchFileOrDirectory), + ENOTDIR => RmdirError::NotADirectory, + EPERM | EACCES => RmdirError::NoWritePerms, + ENAMETOOLONG => RmdirError::PathError(PathError::InvalidPathname), + ENOTEMPTY => RmdirError::NotEmpty, + _ => RmdirError::Io, + }, + Error::Io | Error::InvalidResponse => RmdirError::Io, + } + } +} - let result = self.client.getattr(&fid, fcall::GetattrMask::ALL); - self.client.clunk(fid); +impl From for FileStatusError { + fn from(e: Error) -> Self { + match e { + Error::InvalidPathname => FileStatusError::PathError(PathError::InvalidPathname), + Error::Remote(errno) => match errno { + ENOENT => FileStatusError::PathError(PathError::NoSuchFileOrDirectory), + ENAMETOOLONG => FileStatusError::PathError(PathError::InvalidPathname), + ENOTDIR => FileStatusError::PathError(PathError::ComponentNotADirectory), + EPERM | EACCES => FileStatusError::PathError(PathError::NoSearchPerms { + #[cfg(debug_assertions)] + dir: String::new(), + #[cfg(debug_assertions)] + perms: super::Mode::empty(), + }), + _ => FileStatusError::Io, + }, + Error::Io | Error::InvalidResponse => FileStatusError::Io, + } + } +} - result - .and_then(|attr| Self::rgetattr_to_file_status(&attr)) - .map_err(FileStatusError::from) +impl From for SeekError { + fn from(e: Error) -> Self { + match e { + Error::Remote(e) => match e { + ENOENT => SeekError::ClosedFd, + EINVAL => SeekError::InvalidOffset, + ESPIPE => SeekError::NonSeekable, + _ => SeekError::Io, + }, + _ => SeekError::Io, + } } +} - fn fd_file_status( - &self, - fd: &FileFd, - ) -> Result { - let fid = self - .litebox - .descriptor_table() - .with_entry(fd, |desc| desc.entry.fid.clone()) - .ok_or(super::errors::FileStatusError::ClosedFd)?; +impl From for TruncateError { + fn from(e: Error) -> Self { + match e { + Error::Remote(errno) => match errno { + ENOENT => TruncateError::ClosedFd, + EISDIR => TruncateError::IsDirectory, + EPERM | EACCES => TruncateError::NotForWriting, + _ => TruncateError::Io, + }, + Error::Io | Error::InvalidResponse | Error::InvalidPathname => TruncateError::Io, + } + } +} - let attr = self.client.getattr(&fid, fcall::GetattrMask::ALL)?; +impl From for ChmodError { + fn from(e: Error) -> Self { + match e { + Error::InvalidPathname => ChmodError::PathError(PathError::InvalidPathname), + Error::Remote(errno) => match errno { + ENOENT => ChmodError::PathError(PathError::NoSuchFileOrDirectory), + ENOTDIR => ChmodError::PathError(PathError::ComponentNotADirectory), + EPERM | EACCES => ChmodError::NotTheOwner, + _ => ChmodError::Io, + }, + Error::Io | Error::InvalidResponse => ChmodError::Io, + } + } +} - Ok(Self::rgetattr_to_file_status(&attr)?) +impl From for ChownError { + fn from(e: Error) -> Self { + match e { + Error::InvalidPathname => ChownError::PathError(PathError::InvalidPathname), + Error::Remote(errno) => match errno { + ENOENT => ChownError::PathError(PathError::NoSuchFileOrDirectory), + ENOTDIR => ChownError::PathError(PathError::ComponentNotADirectory), + EPERM | EACCES => ChownError::NotTheOwner, + _ => ChownError::Io, + }, + Error::Io | Error::InvalidResponse => ChownError::Io, + } } } -/// Internal descriptor state for a 9P file descriptor -struct Descriptor { - /// The 9P fid for this file. Refcounted so concurrent in-flight - /// operations keep the pool slot reserved across `close`. - fid: client::Fid, - /// Current file offset (9P doesn't track this server-side) - offset: Arc>, - /// The qid of the file (contains type and unique ID) - qid: fcall::Qid, -} - -crate::fd::enable_fds_for_subsystem! { - @Platform: { sync::RawSyncPrimitivesProvider }, T: { transport::Read + transport::Write }; - FileSystem; - @Platform: { sync::RawSyncPrimitivesProvider }; - Descriptor; - -> FileFd; +impl From for WalkError { + fn from(e: Error) -> Self { + match e { + Error::InvalidPathname => WalkError::PathError(PathError::InvalidPathname), + Error::Remote(errno) => match errno { + ENOENT => WalkError::PathError(PathError::NoSuchFileOrDirectory), + ENAMETOOLONG => WalkError::PathError(PathError::InvalidPathname), + ENOTDIR => WalkError::PathError(PathError::ComponentNotADirectory), + EPERM | EACCES => WalkError::PathError(PathError::NoSearchPerms { + #[cfg(debug_assertions)] + dir: String::new(), + #[cfg(debug_assertions)] + perms: super::Mode::empty(), + }), + _ => WalkError::Io, + }, + Error::Io | Error::InvalidResponse => WalkError::Io, + } + } +} + +impl From for Error { + fn from(err: Rlerror) -> Self { + Error::Remote(err.ecode) + } } diff --git a/litebox/src/fs/nine_p/tests.rs b/litebox/src/fs/nine_p/tests.rs index 383b30a318..191b126e9b 100644 --- a/litebox/src/fs/nine_p/tests.rs +++ b/litebox/src/fs/nine_p/tests.rs @@ -12,10 +12,33 @@ use crate::fs::errors::{ FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; +use crate::fs::inode_allocator::InodeAllocator; +use crate::fs::resolver::Resolver; use crate::fs::{FileSystem as _, Mode, OFlags}; use crate::platform::mock::MockPlatform; -use super::transport; +use super::{NineP, transport}; + +type NinePFs = Resolver>; + +/// Attach to `server` over `transport`, building the backend the tests resolve paths through. +fn attach( + transport: T, + server: &DiodServer, +) -> NineP { + 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")); + NineP::new( + transport, + 65536, + &username, + aname, + InodeAllocator::standalone(), + ) + .expect("failed to create 9P filesystem") +} /// A wrapper around `TcpStream` that implements the litebox 9P transport traits. struct TcpTransport { @@ -174,14 +197,9 @@ impl Drop for DiodServer { fn connect_9p( litebox: &crate::LiteBox, server: &DiodServer, -) -> super::FileSystem { +) -> NinePFs { let transport = TcpTransport::connect(&server.addr()); - 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")); - super::FileSystem::new(litebox, transport, 65536, &username, aname) - .expect("failed to create 9P filesystem") + Resolver::new(litebox, attach(transport, server)) } // --------------------------------------------------------------------------- @@ -507,15 +525,12 @@ fn connect_9p_broken( litebox: &crate::LiteBox, server: &DiodServer, allowed_writes: usize, -) -> super::FileSystem { +) -> NinePFs { let tcp = TcpTransport::connect(&server.addr()); - let transport = BrokenTransport::new(tcp, allowed_writes); - 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")); - super::FileSystem::new(litebox, transport, 65536, &username, aname) - .expect("failed to create 9P filesystem (broken transport)") + Resolver::new( + litebox, + attach(BrokenTransport::new(tcp, allowed_writes), server), + ) } // --------------------------------------------------------------------------- @@ -579,8 +594,9 @@ fn test_nine_p_broken_write() { let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - // 4 writes: version + attach + walk + lopen. Then write will fail. - let fs = connect_9p_broken(&litebox, &server, 4); + // 5 writes: version + attach + walk (which reports the file as missing) + the clone of the + // parent directory's fid + create. Then write will fail. + let fs = connect_9p_broken(&litebox, &server, 5); let fd = fs .open("/write_me.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("create should succeed before break"); diff --git a/litebox/src/fs/resolver.rs b/litebox/src/fs/resolver.rs index 4b96db6a78..9b6802e889 100644 --- a/litebox/src/fs/resolver.rs +++ b/litebox/src/fs/resolver.rs @@ -582,7 +582,12 @@ impl) -> Result<(), CloseError> { - self.litebox.descriptor_table_mut().remove(fd); + let mut dt = self.litebox.descriptor_table_mut(); + let removed = dt.remove(fd); + drop(dt); + // some backends might block while closing an fd, so we've released the descriptor table + // lock _before_ we let the backend handle the close. + drop(removed); Ok(()) } diff --git a/litebox_runner_snp/src/main.rs b/litebox_runner_snp/src/main.rs index b579d4ed5f..edd12f2708 100644 --- a/litebox_runner_snp/src/main.rs +++ b/litebox_runner_snp/src/main.rs @@ -40,10 +40,7 @@ type DefaultFS = litebox::fs::layered::FileSystem< litebox::fs::layered::FileSystem< Platform, litebox::fs::resolver::Resolver, - litebox::fs::nine_p::FileSystem< - Platform, - litebox_shim_linux::transport::ShimTransport, - >, + litebox::fs::resolver::Resolver, >, >; @@ -232,15 +229,26 @@ pub extern "C" fn sandbox_process_init( globals::SM_TERM_GENERAL, ); }; - let Ok(nine_p) = - litebox::fs::nine_p::FileSystem::new(litebox, transport, 65536, "root", "/tmp") - else { - ghcb_prints("failed to create 9P filesystem"); - litebox_platform_linux_kernel::host::snp::snp_impl::HostSnpInterface::terminate( - globals::SM_SEV_TERM_SET, - globals::SM_TERM_GENERAL, + let nine_p_composer = litebox::fs::composer::Composer::builder() + .mount("/", |allocator| { + let Ok(backend) = litebox::fs::nine_p::NineP::::new( + transport, 65536, "root", "/tmp", allocator, + ) else { + ghcb_prints("failed to create 9P filesystem"); + litebox_platform_linux_kernel::host::snp::snp_impl::HostSnpInterface::terminate( + globals::SM_SEV_TERM_SET, + globals::SM_TERM_GENERAL, + ); + }; + backend + }) + .build() + .unwrap_or_else( + |(litebox::fs::composer::BuildError::NoMounts + | litebox::fs::composer::BuildError::InvalidMountPath + | litebox::fs::composer::BuildError::DuplicateMountPath)| unreachable!(), ); - }; + let nine_p = litebox::fs::resolver::Resolver::new(litebox, nine_p_composer); let dev_stdio_composer = litebox::fs::composer::Composer::builder() .mount("/dev", |allocator| { litebox::fs::devices::Devices::new(litebox, allocator) diff --git a/litebox_shim_linux/src/transport.rs b/litebox_shim_linux/src/transport.rs index 7713f4dc05..7e48eda283 100644 --- a/litebox_shim_linux/src/transport.rs +++ b/litebox_shim_linux/src/transport.rs @@ -141,7 +141,8 @@ mod tests { use std::net::TcpListener; use std::path::Path; - use litebox::fs::nine_p; + use litebox::fs::nine_p::NineP; + use litebox::fs::resolver::Resolver; use litebox::fs::{FileSystem as _, Mode, OFlags}; use crate::syscalls::tests::init_platform; @@ -263,10 +264,7 @@ mod tests { crate::DefaultFS, >, server: &DiodServer, - ) -> nine_p::FileSystem< - crate::syscalls::tests::TestPlatform, - ShimTransport, - > { + ) -> Resolver { let addr = socket_addr([10, 0, 0, 1], server.port); let transport = ShimTransport::connect(task.global.clone(), addr) .expect("failed to connect to 9P server via shim network"); @@ -276,8 +274,16 @@ mod tests { .or_else(|_| std::env::var("LOGNAME")) .unwrap_or_else(|_| std::string::String::from("nobody")); - nine_p::FileSystem::new(&task.global.litebox, transport, 65536, &username, aname) - .expect("failed to create 9P filesystem") + 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) } // -----------------------------------------------------------------------