diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60cea36aa..892071d19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,9 @@ jobs: - name: Install iperf3 run: | sudo apt install -y iperf3 + - name: Install diod + run: | + sudo apt install -y diod - uses: Swatinem/rust-cache@v2 - name: Cache custom out directories uses: actions/cache@v5 @@ -90,6 +93,9 @@ jobs: uses: taiki-e/install-action@v2 with: tool: nextest@${{ env.NEXTEST_VERSION }} + - name: Install diod + run: | + sudo apt install -y diod - name: Set up tun run: | sudo ./litebox_platform_linux_userland/scripts/tun-setup.sh diff --git a/Cargo.lock b/Cargo.lock index 43a59d912..c1476e34d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,6 +853,7 @@ dependencies = [ "smoltcp", "spin 0.9.8", "tar-no-std", + "tempfile", "thiserror", "windows-sys 0.60.2", "zerocopy", diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index c115a7110..9a8b2e401 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -36,3 +36,6 @@ enforce_singleton_litebox_instance = [] [lints] workspace = true + +[dev-dependencies] +tempfile = "3" diff --git a/litebox/src/fs/errors.rs b/litebox/src/fs/errors.rs index 38089c65f..3e5846596 100644 --- a/litebox/src/fs/errors.rs +++ b/litebox/src/fs/errors.rs @@ -25,6 +25,8 @@ pub enum OpenError { AlreadyExists, #[error("error when truncating: {0}")] TruncateError(#[from] TruncateError), + #[error("I/O error")] + Io, #[error(transparent)] PathError(#[from] PathError), } @@ -44,6 +46,8 @@ pub enum ReadError { NotAFile, #[error("file not open for reading")] NotForReading, + #[error("I/O error")] + Io, } /// Possible errors from [`FileSystem::write`] @@ -56,6 +60,8 @@ pub enum WriteError { NotAFile, #[error("file not open for writing")] NotForWriting, + #[error("I/O error")] + Io, } /// Possible errors from [`FileSystem::seek`] @@ -70,6 +76,8 @@ pub enum SeekError { InvalidOffset, #[error("non-seekable file")] NonSeekable, + #[error("I/O error")] + Io, } /// Possible errors from [`FileSystem::truncate`] @@ -83,6 +91,8 @@ pub enum TruncateError { NotForWriting, #[error("file descriptor points to a terminal device")] IsTerminalDevice, + #[error("I/O error")] + Io, } /// Possible errors from [`FileSystem::chmod`] @@ -96,6 +106,8 @@ pub enum ChmodError { NotTheOwner, #[error("the named file resides on a read-only filesystem")] ReadOnlyFileSystem, + #[error("I/O error")] + Io, #[error(transparent)] PathError(#[from] PathError), } @@ -111,6 +123,8 @@ pub enum ChownError { NotTheOwner, #[error("the named file resides on a read-only filesystem")] ReadOnlyFileSystem, + #[error("I/O error")] + Io, #[error(transparent)] PathError(#[from] PathError), } @@ -125,6 +139,8 @@ pub enum UnlinkError { IsADirectory, #[error("the named file resides on a read-only filesystem")] ReadOnlyFileSystem, + #[error("I/O error")] + Io, #[error(transparent)] PathError(#[from] PathError), } @@ -139,6 +155,8 @@ pub enum MkdirError { AlreadyExists, #[error("the named file resides on a read-only filesystem")] ReadOnlyFileSystem, + #[error("I/O error")] + Io, #[error(transparent)] PathError(#[from] PathError), } @@ -159,6 +177,8 @@ pub enum RmdirError { NotADirectory, #[error("the named file resides on a read-only filesystem")] ReadOnlyFileSystem, + #[error("I/O error")] + Io, #[error(transparent)] PathError(#[from] PathError), } @@ -171,6 +191,8 @@ pub enum ReadDirError { ClosedFd, #[error("fd does not point to a directory")] NotADirectory, + #[error("I/O error")] + Io, } /// Possible errors from [`FileSystem::file_status`] @@ -179,6 +201,8 @@ pub enum ReadDirError { pub enum FileStatusError { #[error("fd has been closed already")] ClosedFd, + #[error("I/O error")] + Io, #[error(transparent)] PathError(#[from] PathError), } diff --git a/litebox/src/fs/layered.rs b/litebox/src/fs/layered.rs index 674f6b4c3..9868bb50b 100644 --- a/litebox/src/fs/layered.rs +++ b/litebox/src/fs/layered.rs @@ -100,6 +100,7 @@ impl Ok(stat.file_type), Err(FileStatusError::ClosedFd) => unreachable!(), + Err(FileStatusError::Io) => Err(PathError::NoSuchFileOrDirectory), Err(FileStatusError::PathError(e)) => Err(e), } } @@ -132,6 +133,7 @@ impl fd, Err(e) => match e { OpenError::AccessNotAllowed => return Err(MigrationError::NoReadPerms), + OpenError::Io => return Err(MigrationError::Io), OpenError::NoWritePerms | OpenError::ReadOnlyFileSystem | OpenError::AlreadyExists @@ -255,6 +258,7 @@ impl unreachable!(), + ReadError::Io => return Err(MigrationError::Io), }, } } @@ -419,6 +423,8 @@ pub enum MigrationError { NotAFile, #[error("no read access permissions")] NoReadPerms, + #[error("I/O error")] + Io, #[error(transparent)] PathError(#[from] PathError), } @@ -525,6 +531,7 @@ impl< } Err(e) => match &e { OpenError::AccessNotAllowed + | OpenError::Io | OpenError::NoWritePerms | OpenError::ReadOnlyFileSystem | OpenError::AlreadyExists @@ -532,7 +539,8 @@ impl< TruncateError::IsDirectory | TruncateError::NotForWriting | TruncateError::IsTerminalDevice - | TruncateError::ClosedFd, + | TruncateError::ClosedFd + | TruncateError::Io, ) | OpenError::PathError( PathError::ComponentNotADirectory @@ -816,6 +824,7 @@ impl< Ok(()) => {} Err(MigrationError::NoReadPerms) => unimplemented!(), Err(MigrationError::NotAFile) => return Err(WriteError::NotAFile), + Err(MigrationError::Io) => return Err(WriteError::Io), Err(MigrationError::PathError(_e)) => unreachable!(), } // As a sanity check, in debug mode, confirm that it is now an upper file @@ -905,6 +914,7 @@ impl< Ok(()) } + Err(TruncateError::Io) => Err(TruncateError::Io), } } else { // The lower level truncate will correctly identify dir/file and handle @@ -924,6 +934,7 @@ impl< Ok(()) => return Ok(()), Err(e) => match e { ChmodError::NotTheOwner + | ChmodError::Io | ChmodError::ReadOnlyFileSystem | ChmodError::PathError( PathError::ComponentNotADirectory @@ -944,6 +955,7 @@ impl< Ok(()) => {} Err(MigrationError::NoReadPerms) => unimplemented!(), Err(MigrationError::NotAFile) => unimplemented!(), + Err(MigrationError::Io) => return Err(ChmodError::Io), Err(MigrationError::PathError(_e)) => unreachable!(), } // Since it has been migrated, we can just re-trigger, causing it to apply to the @@ -962,6 +974,7 @@ impl< Ok(()) => return Ok(()), Err(e) => match e { ChownError::NotTheOwner + | ChownError::Io | ChownError::ReadOnlyFileSystem | ChownError::PathError( PathError::ComponentNotADirectory @@ -982,6 +995,7 @@ impl< Ok(()) => {} Err(MigrationError::NoReadPerms) => unimplemented!(), Err(MigrationError::NotAFile) => unimplemented!(), + Err(MigrationError::Io) => return Err(ChownError::Io), Err(MigrationError::PathError(_e)) => unreachable!(), } // Since it has been migrated, we can just re-trigger, causing it to apply to the @@ -1005,6 +1019,7 @@ impl< } Err(e) => match e { UnlinkError::NoWritePerms + | UnlinkError::Io | UnlinkError::IsADirectory | UnlinkError::ReadOnlyFileSystem | UnlinkError::PathError( @@ -1054,6 +1069,7 @@ impl< } Err(e) => match e { MkdirError::NoWritePerms + | MkdirError::Io | MkdirError::AlreadyExists | MkdirError::ReadOnlyFileSystem | MkdirError::PathError( @@ -1099,6 +1115,7 @@ impl< } OpenError::PathError(pe) => return Err(pe.into()), OpenError::AccessNotAllowed => todo!(), + OpenError::Io => return Err(RmdirError::Io), OpenError::ReadOnlyFileSystem => { return Err(RmdirError::ReadOnlyFileSystem); } @@ -1112,6 +1129,7 @@ impl< let entries = match self.read_dir(&dir_fd) { Ok(entries) => entries, Err(ReadDirError::ClosedFd | ReadDirError::NotADirectory) => unreachable!(), + Err(ReadDirError::Io) => return Err(RmdirError::Io), }; self.close(&dir_fd).expect("close dir fd failed"); // "." and ".." are always present; anything more => not empty. @@ -1135,6 +1153,7 @@ impl< ) => unreachable!(), RmdirError::Busy | RmdirError::NoWritePerms + | RmdirError::Io | RmdirError::PathError(PathError::NoSearchPerms { .. }) => return Err(e), } } @@ -1161,6 +1180,7 @@ impl< ) => unreachable!(), RmdirError::Busy | RmdirError::NoWritePerms + | RmdirError::Io | RmdirError::PathError(PathError::NoSearchPerms { .. }) => return Err(e), } } @@ -1278,6 +1298,7 @@ impl< // None of these can be handled by lower level, just quit out early return Err(e); } + FileStatusError::Io => return Err(e), FileStatusError::PathError( PathError::NoSuchFileOrDirectory | PathError::MissingComponent, ) => { diff --git a/litebox/src/fs/nine_p.rs b/litebox/src/fs/nine_p.rs deleted file mode 100644 index 968887c15..000000000 --- a/litebox/src/fs/nine_p.rs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! A network file system, using the 9p protocol - -use crate::{LiteBox, platform}; - -/// A backing implementation for [`FileSystem`](super::FileSystem) using a 9p-based network file -/// system. -// TODO(jayb): Reduce the requirements necessary on `Platform` to the most precise one possible. -pub struct FileSystem { - #[expect(dead_code, reason = "placeholder, currently nine_p is unimplemented")] - litebox: LiteBox, -} - -impl FileSystem { - /// Construct a new `FileSystem` instance - /// - /// This function is expected to only be invoked once per platform, as an initialiation step, - /// and the created `FileSystem` handle is expected to be shared across all usage over the - /// system. - #[must_use] - pub fn new(litebox: &LiteBox) -> Self { - Self { - litebox: litebox.clone(), - } - } -} - -impl super::private::Sealed for FileSystem {} - -#[expect(unused_variables, reason = "unimplemented")] -impl super::FileSystem for FileSystem { - fn open( - &self, - path: impl crate::path::Arg, - flags: super::OFlags, - mode: super::Mode, - ) -> Result, super::errors::OpenError> { - todo!() - } - - fn close(&self, fd: &FileFd) -> Result<(), super::errors::CloseError> { - todo!() - } - - fn read( - &self, - fd: &FileFd, - buf: &mut [u8], - offset: Option, - ) -> Result { - todo!() - } - - fn write( - &self, - fd: &FileFd, - buf: &[u8], - offset: Option, - ) -> Result { - todo!() - } - - fn seek( - &self, - fd: &FileFd, - offset: isize, - whence: super::SeekWhence, - ) -> Result { - todo!() - } - - fn truncate( - &self, - fd: &FileFd, - length: usize, - reset_offset: bool, - ) -> Result<(), super::errors::TruncateError> { - todo!() - } - - fn chmod( - &self, - path: impl crate::path::Arg, - mode: super::Mode, - ) -> Result<(), super::errors::ChmodError> { - todo!() - } - - fn chown( - &self, - path: impl crate::path::Arg, - user: Option, - group: Option, - ) -> Result<(), super::errors::ChownError> { - todo!() - } - - fn unlink(&self, path: impl crate::path::Arg) -> Result<(), super::errors::UnlinkError> { - todo!() - } - - fn mkdir( - &self, - path: impl crate::path::Arg, - mode: super::Mode, - ) -> Result<(), super::errors::MkdirError> { - todo!() - } - - fn rmdir(&self, path: impl crate::path::Arg) -> Result<(), super::errors::RmdirError> { - todo!() - } - - fn read_dir( - &self, - fd: &FileFd, - ) -> Result, super::errors::ReadDirError> { - todo!() - } - - fn file_status( - &self, - path: impl crate::path::Arg, - ) -> Result { - todo!() - } - - fn fd_file_status( - &self, - fd: &FileFd, - ) -> Result { - todo!() - } -} - -crate::fd::enable_fds_for_subsystem! { - @Platform: { platform::Provider + Sync + 'static }; - FileSystem; - (); - -> FileFd; -} diff --git a/litebox/src/fs/nine_p/client.rs b/litebox/src/fs/nine_p/client.rs new file mode 100644 index 000000000..759e5d555 --- /dev/null +++ b/litebox/src/fs/nine_p/client.rs @@ -0,0 +1,571 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! 9P client implementation +//! +//! This module provides a high-level client for the 9P2000.L protocol. + +use alloc::vec::Vec; +use core::sync::atomic::{AtomicU16, Ordering}; + +use crate::sync::{Mutex, RawSyncPrimitivesProvider}; + +use super::Error; +use super::fcall::{self, Fcall, FcallStr, GetattrMask, TaggedFcall}; +use super::transport::{self, Read, Write}; + +/// ID generator for fids +struct IdGenerator { + next: u32, + free_ids: Vec, +} + +impl IdGenerator { + const fn new() -> Self { + IdGenerator { + next: 0, + free_ids: Vec::new(), + } + } + + fn next(&mut self) -> u32 { + if let Some(id) = self.free_ids.pop() { + id + } else { + let id = self.next; + self.next = self.next.checked_add(1).expect("out of fids"); + id + } + } + + fn free(&mut self, id: u32) { + self.free_ids.push(id); + } +} + +/// Fid generator with thread-safe access +struct FidGenerator { + inner: Mutex, +} + +impl Default for FidGenerator { + fn default() -> Self { + Self::new() + } +} + +impl FidGenerator { + /// Create a new fid generator + fn new() -> Self { + FidGenerator { + inner: Mutex::new(IdGenerator::new()), + } + } + + /// Allocate a new fid + fn next(&self) -> u32 { + self.inner.lock().next() + } + + /// Release a fid for reuse + fn free(&self, id: u32) { + self.inner.lock().free(id); + } +} + +/// 9P client state for writing to the connection +struct ClientWriteState { + /// The underlying transport + transport: T, + /// Write buffer + wbuf: Vec, +} + +/// 9P client +/// +/// This client provides synchronous 9P protocol operations. It uses a transport +/// that implements both Read and Write traits. +pub(super) struct Client { + /// Maximum message size negotiated with server + msize: u32, + /// Write state protected by a mutex + write_state: Mutex>, + /// Read buffer for responses + rbuf: Mutex>, + /// Fid generator + fids: FidGenerator, + /// Next tag for synchronous operations + next_tag: AtomicU16, +} + +impl Client { + /// Create a new 9P client and perform version negotiation + /// + /// # Arguments + /// * `transport` - The underlying transport for read/write operations + /// * `max_msize` - Maximum message size to request + pub(super) fn new(mut transport: T, max_msize: u32) -> Result { + const MIN_MSIZE: u32 = 4096 + fcall::READDIRHDRSZ; + let bufsize = max_msize.max(MIN_MSIZE); + + let mut wbuf = Vec::with_capacity(bufsize as usize); + let mut rbuf = Vec::with_capacity(bufsize as usize); + + // Perform version handshake + transport::write_message( + &mut transport, + &mut wbuf, + TaggedFcall { + tag: fcall::NOTAG, + fcall: Fcall::Tversion(fcall::Tversion { + msize: bufsize, + version: fcall::FcallStr::Borrowed(b"9P2000.L"), + }), + }, + ) + .map_err(|_| Error::Io)?; + + let response = transport::read_message(&mut transport, &mut rbuf)?; + + let msize = match response { + TaggedFcall { + tag: fcall::NOTAG, + fcall: Fcall::Rversion(fcall::Rversion { msize, version }), + } => { + if &*version != b"9P2000.L" { + return Err(Error::InvalidResponse); + } + msize.min(bufsize) + } + TaggedFcall { + fcall: Fcall::Rlerror(e), + .. + } => return Err(Error::from(e)), + _ => return Err(Error::InvalidResponse), + }; + + wbuf.truncate(msize as usize); + rbuf.truncate(msize as usize); + + Ok(Client { + msize, + write_state: Mutex::new(ClientWriteState { transport, wbuf }), + rbuf: Mutex::new(rbuf), + fids: FidGenerator::new(), + next_tag: AtomicU16::new(1), + }) + } + + /// Send a request and wait for the response + fn fcall(&self, fcall: Fcall<'_>, f: F) -> Result + where + F: FnOnce(Fcall<'_>) -> Result, + { + let tag = self.next_tag.fetch_add(1, Ordering::Relaxed); + if tag == fcall::NOTAG { + todo!("tag wraparound"); + } + + let mut write_state = self.write_state.lock(); + let ClientWriteState { transport, wbuf } = &mut *write_state; + transport::write_message(transport, wbuf, TaggedFcall { tag, fcall }) + .map_err(|_| Error::Io)?; + + let mut rbuf = self.rbuf.lock(); + + // Loop until we get a response with matching tag (in case of stale responses) + // TODO: support concurrent requests by allowing out-of-order responses and matching tags accordingly + loop { + let response = transport::read_message(transport, &mut rbuf)?; + if response.tag == tag { + return f(response.fcall); + } + } + } + + /// Attach to a remote filesystem + pub(super) fn attach( + &self, + uname: &str, + aname: &str, + ) -> Result<(fcall::Qid, fcall::Fid), Error> { + let fid = self.fids.next(); + let res = self.fcall( + Fcall::Tattach(fcall::Tattach { + afid: fcall::NOFID, + fid, + n_uname: fcall::NONUNAME, + uname: fcall::FcallStr::Borrowed(uname.as_bytes()), + aname: fcall::FcallStr::Borrowed(aname.as_bytes()), + }), + |response| match response { + Fcall::Rattach(fcall::Rattach { qid }) => Ok((qid, fid)), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ); + if res.is_err() { + self.fids.free(fid); + } + res + } + + /// Walks the path from the given fid. + /// + /// The given wnames should not exceed the maximum number of elements (fcall::MAXWELEM), + /// which is checked at the beginning of the function. This is an internal function that + /// is used by [`walk_chunked`](Client::walk_chunked), which handles the case where the number of elements exceeds the limit. + fn walk_once( + &self, + fid: fcall::Fid, + wnames: &[FcallStr], + ) -> Result<(Vec, fcall::Fid), Error> { + if wnames.len() > fcall::MAXWELEM { + return Err(Error::InvalidPathname); + } + let new_fid = self.fids.next(); + let ret = self.fcall( + Fcall::Twalk(fcall::Twalk { + fid, + new_fid, + wnames: wnames.to_vec(), + }), + |response| match response { + Fcall::Rwalk(fcall::Rwalk { wqids }) => Ok((wqids, new_fid)), + Fcall::Rlerror(err) => Err(Error::from(err)), + _ => Err(Error::InvalidResponse), + }, + ); + if ret.is_err() { + self.fids.free(new_fid); + } + ret + } + + /// 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. + fn walk_chunked( + &self, + fid: fcall::Fid, + wnames: &[FcallStr], + ) -> Result<(Vec, fcall::Fid), Error> { + if wnames.is_empty() { + return self.walk_once(fid, wnames); + } + let mut wqids = Vec::with_capacity(fcall::MAXWELEM); + let mut f = fid; + for wnames in wnames.chunks(fcall::MAXWELEM) { + let (mut new_wqids, new_f) = self.walk_once(f, wnames)?; + let new_len = new_wqids.len(); + wqids.append(&mut new_wqids); + // Clunk the old fid if it's not the original fid + if f != fid { + let _ = self.clunk(f); + } + f = new_f; + // It means that the walk failed at the nwqid-th element + if new_len < wnames.len() { + if wqids + .last() + .is_some_and(|e| e.typ == fcall::QidType::SYMLINK) + { + todo!("symlink"); + } + let _ = self.clunk(f); + return Err(Error::Remote(super::ENOENT)); + } + } + Ok((wqids, f)) + } + + /// 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: fcall::Fid, + wnames: &[S], + ) -> Result<(Vec, fcall::Fid), Error> { + let wnames: Vec> = wnames + .iter() + .map(|s| fcall::FcallStr::Borrowed(s.as_ref())) + .collect(); + self.walk_chunked(fid, &wnames) + } + + /// Open a file + pub(super) fn open( + &self, + fid: fcall::Fid, + flags: fcall::LOpenFlags, + ) -> Result { + self.fcall( + Fcall::Tlopen(fcall::Tlopen { fid, flags }), + |response| match response { + Fcall::Rlopen(fcall::Rlopen { qid, .. }) => Ok(qid), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Create a file with the given name and flags. + /// + /// The input dfid initially represents the parent directory of the new file. + /// After the call it represents the new file. + pub(super) fn create( + &self, + dfid: fcall::Fid, + name: &str, + flags: fcall::LOpenFlags, + mode: u32, + gid: u32, + ) -> Result<(fcall::Qid, fcall::Fid), Error> { + self.fcall( + Fcall::Tlcreate(fcall::Tlcreate { + fid: dfid, + name: fcall::FcallStr::Borrowed(name.as_bytes()), + flags, + mode, + gid, + }), + |response| match response { + Fcall::Rlcreate(fcall::Rlcreate { qid, iounit: _ }) => Ok((qid, dfid)), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Read from a file + pub(super) fn read( + &self, + fid: fcall::Fid, + offset: u64, + buf: &mut [u8], + ) -> Result { + let count = buf.len().min((self.msize - fcall::IOHDRSZ) as usize); + self.fcall( + Fcall::Tread(fcall::Tread { + fid, + offset, + count: u32::try_from(count).expect("count exceeds u32"), + }), + |response| match response { + Fcall::Rread(fcall::Rread { data }) => { + buf[..data.len()].copy_from_slice(&data); + Ok(data.len()) + } + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Write to a file + pub(super) fn write(&self, fid: fcall::Fid, offset: u64, data: &[u8]) -> Result { + let count = data.len().min((self.msize - fcall::IOHDRSZ) as usize); + self.fcall( + Fcall::Twrite(fcall::Twrite { + fid, + offset, + data: alloc::borrow::Cow::Borrowed(&data[..count]), + }), + |response| match response { + Fcall::Rwrite(fcall::Rwrite { count }) => Ok(count as usize), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Get file attributes + pub(super) fn getattr( + &self, + fid: fcall::Fid, + req_mask: GetattrMask, + ) -> Result { + self.fcall( + Fcall::Tgetattr(fcall::Tgetattr { fid, req_mask }), + |response| match response { + Fcall::Rgetattr(r) => Ok(r), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Set file attributes + pub(super) fn setattr( + &self, + fid: fcall::Fid, + valid: fcall::SetattrMask, + stat: fcall::SetAttr, + ) -> Result<(), Error> { + self.fcall( + Fcall::Tsetattr(fcall::Tsetattr { fid, valid, stat }), + |response| match response { + Fcall::Rsetattr(_) => Ok(()), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Read directory entries + pub(super) fn readdir( + &self, + fid: fcall::Fid, + offset: u64, + ) -> Result>, Error> { + let count = self.msize - fcall::READDIRHDRSZ; + self.fcall( + Fcall::Treaddir(fcall::Treaddir { fid, offset, count }), + |response| match response { + Fcall::Rreaddir(fcall::Rreaddir { data }) => Ok(data + .data + .into_iter() + .map(fcall::DirEntry::into_owned) + .collect()), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Read all directory entries + pub(super) fn readdir_all( + &self, + fid: fcall::Fid, + ) -> Result>, Error> { + let mut all_entries = Vec::new(); + let mut offset = 0u64; + loop { + let entries = self.readdir(fid, offset)?; + if entries.is_empty() { + break; + } + offset = entries.last().unwrap().offset; + all_entries.extend(entries); + } + Ok(all_entries) + } + + /// Create a directory + pub(super) fn mkdir( + &self, + dfid: fcall::Fid, + name: &str, + mode: u32, + gid: u32, + ) -> Result { + self.fcall( + Fcall::Tmkdir(fcall::Tmkdir { + dfid, + name: fcall::FcallStr::Borrowed(name.as_bytes()), + mode, + gid, + }), + |response| match response { + Fcall::Rmkdir(fcall::Rmkdir { qid }) => Ok(qid), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Remove the file represented by fid and clunk the fid, even if the remove fails + pub(super) fn remove(&self, fid: fcall::Fid) -> Result<(), Error> { + self.fcall( + Fcall::Tremove(fcall::Tremove { fid }), + |response| match response { + Fcall::Rremove(_) => Ok(()), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Remove (unlink) a file or directory + pub(super) fn unlinkat(&self, dfid: fcall::Fid, name: &str, flags: u32) -> Result<(), Error> { + self.fcall( + Fcall::Tunlinkat(fcall::Tunlinkat { + dfid, + name: fcall::FcallStr::Borrowed(name.as_bytes()), + flags, + }), + |response| match response { + Fcall::Runlinkat(_) => Ok(()), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Rename a file + #[expect(dead_code)] + pub(super) fn rename( + &self, + fid: fcall::Fid, + dfid: fcall::Fid, + name: &str, + ) -> Result<(), Error> { + self.fcall( + Fcall::Trename(fcall::Trename { + fid, + dfid, + name: fcall::FcallStr::Borrowed(name.as_bytes()), + }), + |response| match response { + Fcall::Rrename(_) => Ok(()), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Fsync a file + #[expect(dead_code)] + pub(super) fn fsync(&self, fid: fcall::Fid, datasync: bool) -> Result<(), Error> { + self.fcall( + Fcall::Tfsync(fcall::Tfsync { + fid, + datasync: u32::from(datasync), + }), + |response| match response { + Fcall::Rfsync(_) => Ok(()), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ) + } + + /// Clunk (close) a fid + pub(super) fn clunk(&self, fid: fcall::Fid) -> Result<(), Error> { + let result = self.fcall( + Fcall::Tclunk(fcall::Tclunk { fid }), + |response| match response { + Fcall::Rclunk(_) => Ok(()), + Fcall::Rlerror(e) => Err(Error::from(e)), + _ => Err(Error::InvalidResponse), + }, + ); + self.fids.free(fid); + result + } + + /// Clone a fid (walk with empty path) + pub(super) fn clone_fid(&self, fid: fcall::Fid) -> Result { + let empty: [&str; 0] = []; + let (_, new_fid) = self.walk(fid, &empty)?; + Ok(new_fid) + } + + /// Release a fid back to the pool without clunking + /// + /// Use this when the fid has already been invalidated (e.g., after remove) + pub(super) fn free_fid(&self, fid: fcall::Fid) { + self.fids.free(fid); + } +} diff --git a/litebox/src/fs/nine_p/fcall.rs b/litebox/src/fs/nine_p/fcall.rs new file mode 100644 index 000000000..8e4fdc68e --- /dev/null +++ b/litebox/src/fs/nine_p/fcall.rs @@ -0,0 +1,1921 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! 9P2000.L protocol message definitions and encoding/decoding +//! +//! This module implements the 9P2000.L protocol used for network filesystem access. +//! See and + +use core::fmt::Display; + +use super::transport::{self, Write}; +use alloc::{borrow::Cow, vec::Vec}; +use bitflags::bitflags; + +/// File identifier type +pub(super) type Fid = u32; + +/// Special tag which `Tversion`/`Rversion` must use as `tag` +pub(super) const NOTAG: u16 = !0; + +/// Special value which `Tattach` with no auth must use as `afid` +/// +/// If the client does not wish to authenticate the connection, or knows that authentication is +/// not required, the afid field in the attach message should be set to `NOFID` +pub(super) const NOFID: u32 = !0; + +/// Special uid which `Tauth`/`Tattach` use as `n_uname` to indicate no uid is specified +pub(super) const NONUNAME: u32 = !0; + +/// Room for `Twrite`/`Rread` header +/// +/// size\[4\] Tread/Twrite\[2\] tag\[2\] fid\[4\] offset\[8\] count\[4\] +pub(super) const IOHDRSZ: u32 = 24; + +/// Room for readdir header +pub(super) const READDIRHDRSZ: u32 = 24; + +/// Maximum elements in a single walk. +pub(super) const MAXWELEM: usize = 13; + +bitflags! { + /// Flags passed to Tlopen. + /// + /// Same as Linux's open flags. + /// https://elixir.bootlin.com/linux/v6.12/source/include/net/9p/9p.h#L263 + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(super) struct LOpenFlags: u32 { + const O_RDONLY = 0; + const O_WRONLY = 1; + const O_RDWR = 2; + + const O_CREAT = 0o100; + const O_EXCL = 0o200; + const O_NOCTTY = 0o400; + const O_TRUNC = 0o1000; + const O_APPEND = 0o2000; + const O_NONBLOCK = 0o4000; + const O_DSYNC = 0o10000; + const FASYNC = 0o20000; + const O_DIRECT = 0o40000; + const O_LARGEFILE = 0o100000; + const O_DIRECTORY = 0o200000; + const O_NOFOLLOW = 0o400000; + const O_NOATIME = 0o1000000; + const O_CLOEXEC = 0o2000000; + const O_SYNC = 0o4000000; + } +} + +bitflags! { + /// File lock type, Flock.typ + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(super) struct LockType: u8 { + const RDLOCK = 0; + const WRLOCK = 1; + const UNLOCK = 2; + } +} + +bitflags! { + /// File lock flags, Flock.flags + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(super) struct LockFlag: u32 { + /// Blocking request + const BLOCK = 1; + /// Reserved for future use + const RECLAIM = 2; + } +} + +bitflags! { + /// File lock status + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(super) struct LockStatus: u8 { + const SUCCESS = 0; + const BLOCKED = 1; + const ERROR = 2; + const GRACE = 3; + } +} + +bitflags! { + /// Bits in Qid.typ + /// + /// QidType can be constructed from std::fs::FileType via From trait + /// + /// # Protocol + /// 9P2000/9P2000.L + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + pub(super) struct QidType: u8 { + /// Type bit for directories + const DIR = 0x80; + /// Type bit for append only files + const APPEND = 0x40; + /// Type bit for exclusive use files + const EXCL = 0x20; + /// Type bit for mounted channel + const MOUNT = 0x10; + /// Type bit for authentication file + const AUTH = 0x08; + /// Type bit for not-backed-up file + const TMP = 0x04; + /// Type bits for symbolic links (9P2000.u) + const SYMLINK = 0x02; + /// Type bits for hard-link (9P2000.u) + const LINK = 0x01; + /// Plain file + const FILE = 0x00; + } +} + +bitflags! { + /// Bits in `mask` and `valid` of `Tgetattr` and `Rgetattr`. + /// + /// # Protocol + /// 9P2000.L + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(super) struct GetattrMask: u64 { + const MODE = 0x00000001; + const NLINK = 0x00000002; + const UID = 0x00000004; + const GID = 0x00000008; + const RDEV = 0x00000010; + const ATIME = 0x00000020; + const MTIME = 0x00000040; + const CTIME = 0x00000080; + const INO = 0x00000100; + const SIZE = 0x00000200; + const BLOCKS = 0x00000400; + + const BTIME = 0x00000800; + const GEN = 0x00001000; + const DATA_VERSION = 0x00002000; + + /// Mask for fields up to BLOCKS + const BASIC = 0x000007ff; + /// Mask for All fields above + const ALL = 0x00003fff; + } +} + +bitflags! { + /// Bits in `mask` of `Tsetattr`. + /// + /// If a time bit is set without the corresponding SET bit, the current + /// system time on the server is used instead of the value sent in the request. + /// + /// # Protocol + /// 9P2000.L + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(super) struct SetattrMask: u32 { + const MODE = 0x00000001; + const UID = 0x00000002; + const GID = 0x00000004; + const SIZE = 0x00000008; + const ATIME = 0x00000010; + const MTIME = 0x00000020; + const CTIME = 0x00000040; + const ATIME_SET = 0x00000080; + const MTIME_SET = 0x00000100; + } +} + +/// String type used in 9P protocol messages +pub(super) type FcallStr<'a> = Cow<'a, [u8]>; + +/// Directory entry data container +#[derive(Clone, Debug)] +pub(super) struct DirEntryData<'a> { + pub(super) data: Vec>, +} + +impl<'a> DirEntryData<'a> { + /// Create directory entry data from a vector + fn with(v: Vec>) -> DirEntryData<'a> { + DirEntryData { data: v } + } + + /// Calculate the total size of all entries + fn size(&self) -> u64 { + self.data.iter().fold(0, |a, e| a + e.size()) + } +} + +/// Define a `#[repr($int_ty)]` enum and auto-generate a typed conversion method. +/// +/// # Example +/// ```ignore +/// repr_enum! { +/// #[derive(Copy, Clone, Debug)] +/// enum Color: u8, from_u8 { +/// Red = 1, +/// Green = 2, +/// Blue = 3, +/// } +/// } +/// assert_eq!(Color::from_u8(2), Some(Color::Green)); +/// ``` +macro_rules! repr_enum { + ( + $(#[$meta:meta])* + $vis:vis enum $name:ident : $int_ty:ty, $from_fn:ident { + $( + $(#[$vmeta:meta])* + $variant:ident = $value:expr + ),* $(,)? + } + ) => { + $(#[$meta])* + #[repr($int_ty)] + $vis enum $name { + $( + $(#[$vmeta])* + $variant = $value, + )* + } + + impl $name { + /// Convert a raw integer to the enum, returning `None` for unknown values. + fn $from_fn(v: $int_ty) -> Option { + match v { + $( $value => Some(Self::$variant), )* + _ => None, + } + } + } + }; +} + +repr_enum! { + /// 9P message types + #[derive(Copy, Clone, Debug)] + enum FcallType: u8, from_u8 { + // 9P2000.L + Rlerror = 7, + Tstatfs = 8, + Rstatfs = 9, + Tlopen = 12, + Rlopen = 13, + Tlcreate = 14, + Rlcreate = 15, + Tsymlink = 16, + Rsymlink = 17, + Tmknod = 18, + Rmknod = 19, + Trename = 20, + Rrename = 21, + Treadlink = 22, + Rreadlink = 23, + Tgetattr = 24, + Rgetattr = 25, + Tsetattr = 26, + Rsetattr = 27, + Txattrwalk = 30, + Rxattrwalk = 31, + Txattrcreate = 32, + Rxattrcreate = 33, + Treaddir = 40, + Rreaddir = 41, + Tfsync = 50, + Rfsync = 51, + Tlock = 52, + Rlock = 53, + Tgetlock = 54, + Rgetlock = 55, + Tlink = 70, + Rlink = 71, + Tmkdir = 72, + Rmkdir = 73, + Trenameat = 74, + Rrenameat = 75, + Tunlinkat = 76, + Runlinkat = 77, + + // 9P2000 + Tversion = 100, + Rversion = 101, + Tauth = 102, + Rauth = 103, + Tattach = 104, + Rattach = 105, + Tflush = 108, + Rflush = 109, + Twalk = 110, + Rwalk = 111, + Tread = 116, + Rread = 117, + Twrite = 118, + Rwrite = 119, + Tclunk = 120, + Rclunk = 121, + Tremove = 122, + Rremove = 123, + } +} + +/// Unique identifier for a file +#[derive(Clone, Debug, Copy)] +pub(super) struct Qid { + pub(super) typ: QidType, + pub(super) version: u32, + pub(super) path: u64, +} + +/// File system statistics +#[derive(Clone, Debug, Copy)] +struct Statfs { + typ: u32, + bsize: u32, + blocks: u64, + bfree: u64, + bavail: u64, + files: u64, + ffree: u64, + fsid: u64, + namelen: u32, +} + +/// Time structure +#[derive(Clone, Debug, Copy, Default)] +pub(super) struct Time { + sec: u64, + nsec: u64, +} + +/// File attributes +#[derive(Clone, Debug, Copy)] +pub(super) struct Stat { + pub(super) mode: u32, + pub(super) uid: u32, + pub(super) gid: u32, + pub(super) nlink: u64, + pub(super) rdev: u64, + pub(super) size: u64, + pub(super) blksize: u64, + pub(super) blocks: u64, + pub(super) atime: Time, + pub(super) mtime: Time, + pub(super) ctime: Time, + pub(super) btime: Time, + pub(super) generation: u64, + pub(super) data_version: u64, +} + +/// Set file attributes +#[derive(Clone, Debug, Copy, Default)] +pub(super) struct SetAttr { + pub(super) mode: u32, + pub(super) uid: u32, + pub(super) gid: u32, + pub(super) size: u64, + pub(super) atime: Time, + pub(super) mtime: Time, +} + +/// Directory entry +#[derive(Clone, Debug)] +pub(super) struct DirEntry<'a> { + pub(super) qid: Qid, + pub(super) offset: u64, + pub(super) typ: u8, + pub(super) name: FcallStr<'a>, +} + +impl DirEntry<'_> { + /// Create an owned copy of this directory entry. + pub(super) fn into_owned(self) -> DirEntry<'static> { + DirEntry { + qid: self.qid, + offset: self.offset, + typ: self.typ, + name: FcallStr::Owned(self.name.clone().into_owned()), + } + } + + /// Calculate the size of this entry when encoded + fn size(&self) -> u64 { + (13 + 8 + 1 + 2 + self.name.len()) as u64 + } +} + +/// File lock request +#[derive(Clone, Debug)] +pub(super) struct Flock<'a> { + typ: LockType, + flags: LockFlag, + start: u64, + length: u64, + proc_id: u32, + client_id: FcallStr<'a>, +} + +/// Get lock request +#[derive(Clone, Debug)] +struct Getlock<'a> { + typ: LockType, + start: u64, + length: u64, + proc_id: u32, + client_id: FcallStr<'a>, +} + +// ============================================================================ +// Response/Request structures +// ============================================================================ + +/// Error response +#[derive(Clone, Debug)] +pub(super) struct Rlerror { + pub(super) ecode: u32, +} + +impl Display for Rlerror { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Remote error: {}", self.ecode) + } +} + +/// Attach request +#[derive(Clone, Debug)] +pub(super) struct Tattach<'a> { + pub(super) fid: u32, + pub(super) afid: u32, + pub(super) uname: FcallStr<'a>, + pub(super) aname: FcallStr<'a>, + pub(super) n_uname: u32, +} + +/// Attach response +#[derive(Clone, Debug)] +pub(super) struct Rattach { + pub(super) qid: Qid, +} + +/// Statfs request +#[derive(Clone, Debug)] +pub(super) struct Tstatfs { + fid: u32, +} + +/// Statfs response +#[derive(Clone, Debug)] +pub(super) struct Rstatfs { + statfs: Statfs, +} + +/// Open request +#[derive(Clone, Debug)] +pub(super) struct Tlopen { + pub(super) fid: u32, + pub(super) flags: LOpenFlags, +} + +/// Open response +#[derive(Clone, Debug)] +pub(super) struct Rlopen { + pub(super) qid: Qid, + pub(super) iounit: u32, +} + +/// Create request +#[derive(Clone, Debug)] +pub(super) struct Tlcreate<'a> { + pub(super) fid: u32, + pub(super) name: FcallStr<'a>, + pub(super) flags: LOpenFlags, + pub(super) mode: u32, + pub(super) gid: u32, +} + +/// Create response +#[derive(Clone, Debug)] +pub(super) struct Rlcreate { + pub(super) qid: Qid, + pub(super) iounit: u32, +} + +/// Symlink request +#[derive(Clone, Debug)] +pub(super) struct Tsymlink<'a> { + fid: u32, + name: FcallStr<'a>, + symtgt: FcallStr<'a>, + gid: u32, +} + +/// Symlink response +#[derive(Clone, Debug)] +pub(super) struct Rsymlink { + qid: Qid, +} + +/// Mknod request +#[derive(Clone, Debug)] +pub(super) struct Tmknod<'a> { + dfid: u32, + name: FcallStr<'a>, + mode: u32, + major: u32, + minor: u32, + gid: u32, +} + +/// Mknod response +#[derive(Clone, Debug)] +pub(super) struct Rmknod { + qid: Qid, +} + +/// Rename request +#[derive(Clone, Debug)] +pub(super) struct Trename<'a> { + pub(super) fid: u32, + pub(super) dfid: u32, + pub(super) name: FcallStr<'a>, +} + +/// Rename response +#[derive(Clone, Debug)] +pub(super) struct Rrename {} + +/// Readlink request +#[derive(Clone, Debug)] +pub(super) struct Treadlink { + fid: u32, +} + +/// Readlink response +#[derive(Clone, Debug)] +pub(super) struct Rreadlink<'a> { + target: FcallStr<'a>, +} + +/// Getattr request +#[derive(Clone, Debug)] +pub(super) struct Tgetattr { + pub(super) fid: u32, + pub(super) req_mask: GetattrMask, +} + +/// Getattr response +#[derive(Clone, Debug)] +pub(super) struct Rgetattr { + pub(super) valid: GetattrMask, + pub(super) qid: Qid, + pub(super) stat: Stat, +} + +/// Setattr request +#[derive(Clone, Debug)] +pub(super) struct Tsetattr { + pub(super) fid: u32, + pub(super) valid: SetattrMask, + pub(super) stat: SetAttr, +} + +/// Setattr response +#[derive(Clone, Debug)] +pub(super) struct Rsetattr {} + +/// Xattr walk request +#[derive(Clone, Debug)] +pub(super) struct Txattrwalk<'a> { + fid: u32, + new_fid: u32, + name: FcallStr<'a>, +} + +/// Xattr walk response +#[derive(Clone, Debug)] +pub(super) struct Rxattrwalk { + size: u64, +} + +/// Xattr create request +#[derive(Clone, Debug)] +pub(super) struct Txattrcreate<'a> { + fid: u32, + name: FcallStr<'a>, + attr_size: u64, + flags: u32, +} + +/// Xattr create response +#[derive(Clone, Debug)] +pub(super) struct Rxattrcreate {} + +/// Readdir request +#[derive(Clone, Debug)] +pub(super) struct Treaddir { + pub(super) fid: u32, + pub(super) offset: u64, + pub(super) count: u32, +} + +/// Readdir response +#[derive(Clone, Debug)] +pub(super) struct Rreaddir<'a> { + pub(super) data: DirEntryData<'a>, +} + +/// Fsync request +#[derive(Clone, Debug)] +pub(super) struct Tfsync { + pub(super) fid: u32, + pub(super) datasync: u32, +} + +/// Fsync response +#[derive(Clone, Debug)] +pub(super) struct Rfsync {} + +/// Lock request +#[derive(Clone, Debug)] +pub(super) struct Tlock<'a> { + fid: u32, + flock: Flock<'a>, +} + +/// Lock response +#[derive(Clone, Debug)] +pub(super) struct Rlock { + status: LockStatus, +} + +/// Getlock request +#[derive(Clone, Debug)] +pub(super) struct Tgetlock<'a> { + fid: u32, + flock: Getlock<'a>, +} + +/// Getlock response +#[derive(Clone, Debug)] +pub(super) struct Rgetlock<'a> { + flock: Getlock<'a>, +} + +/// Link request +#[derive(Clone, Debug)] +pub(super) struct Tlink<'a> { + dfid: u32, + fid: u32, + name: FcallStr<'a>, +} + +/// Link response +#[derive(Clone, Debug)] +pub(super) struct Rlink {} + +/// Mkdir request +#[derive(Clone, Debug)] +pub(super) struct Tmkdir<'a> { + pub(super) dfid: u32, + pub(super) name: FcallStr<'a>, + pub(super) mode: u32, + pub(super) gid: u32, +} + +/// Mkdir response +#[derive(Clone, Debug)] +pub(super) struct Rmkdir { + pub(super) qid: Qid, +} + +/// Renameat request +#[derive(Clone, Debug)] +pub(super) struct Trenameat<'a> { + olddfid: u32, + oldname: FcallStr<'a>, + newdfid: u32, + newname: FcallStr<'a>, +} + +/// Renameat response +#[derive(Clone, Debug)] +pub(super) struct Rrenameat {} + +/// Unlinkat request +#[derive(Clone, Debug)] +pub(super) struct Tunlinkat<'a> { + pub(super) dfid: u32, + pub(super) name: FcallStr<'a>, + pub(super) flags: u32, +} + +/// Unlinkat response +#[derive(Clone, Debug)] +pub(super) struct Runlinkat {} + +/// Auth request +#[derive(Clone, Debug)] +pub(super) struct Tauth<'a> { + afid: u32, + uname: FcallStr<'a>, + aname: FcallStr<'a>, + n_uname: u32, +} + +/// Auth response +#[derive(Clone, Debug)] +pub(super) struct Rauth { + aqid: Qid, +} + +/// Version request +#[derive(Clone, Debug)] +pub(super) struct Tversion<'a> { + pub(super) msize: u32, + pub(super) version: FcallStr<'a>, +} + +/// Version response +#[derive(Clone, Debug)] +pub(super) struct Rversion<'a> { + pub(super) msize: u32, + pub(super) version: FcallStr<'a>, +} + +/// Flush request +#[derive(Clone, Debug)] +pub(super) struct Tflush { + oldtag: u16, +} + +/// Flush response +#[derive(Clone, Debug)] +pub(super) struct Rflush {} + +/// Walk request +#[derive(Clone, Debug)] +pub(super) struct Twalk<'a> { + pub(super) fid: u32, + pub(super) new_fid: u32, + pub(super) wnames: Vec>, +} + +/// Walk response +#[derive(Clone, Debug)] +pub(super) struct Rwalk { + pub(super) wqids: Vec, +} + +/// Read request +#[derive(Clone, Debug)] +pub(super) struct Tread { + pub(super) fid: u32, + pub(super) offset: u64, + pub(super) count: u32, +} + +/// Read response +#[derive(Clone, Debug)] +pub(super) struct Rread<'a> { + pub(super) data: Cow<'a, [u8]>, +} + +/// Write request +#[derive(Clone, Debug)] +pub(super) struct Twrite<'a> { + pub(super) fid: u32, + pub(super) offset: u64, + pub(super) data: Cow<'a, [u8]>, +} + +/// Write response +#[derive(Clone, Debug)] +pub(super) struct Rwrite { + pub(super) count: u32, +} + +/// Clunk request +#[derive(Clone, Debug)] +pub(super) struct Tclunk { + pub(super) fid: u32, +} + +/// Clunk response +#[derive(Clone, Debug)] +pub(super) struct Rclunk {} + +/// Remove request +#[derive(Clone, Debug)] +pub(super) struct Tremove { + pub(super) fid: u32, +} + +/// Remove response +#[derive(Clone, Debug)] +pub(super) struct Rremove {} + +// ============================================================================ +// Fcall enum and conversions +// ============================================================================ + +/// 9P protocol message +#[derive(Clone, Debug)] +pub(super) enum Fcall<'a> { + Rlerror(Rlerror), + Tattach(Tattach<'a>), + Rattach(Rattach), + Tstatfs(Tstatfs), + Rstatfs(Rstatfs), + Tlopen(Tlopen), + Rlopen(Rlopen), + Tlcreate(Tlcreate<'a>), + Rlcreate(Rlcreate), + Tsymlink(Tsymlink<'a>), + Rsymlink(Rsymlink), + Tmknod(Tmknod<'a>), + Rmknod(Rmknod), + Trename(Trename<'a>), + Rrename(Rrename), + Treadlink(Treadlink), + Rreadlink(Rreadlink<'a>), + Tgetattr(Tgetattr), + Rgetattr(Rgetattr), + Tsetattr(Tsetattr), + Rsetattr(Rsetattr), + Txattrwalk(Txattrwalk<'a>), + Rxattrwalk(Rxattrwalk), + Txattrcreate(Txattrcreate<'a>), + Rxattrcreate(Rxattrcreate), + Treaddir(Treaddir), + Rreaddir(Rreaddir<'a>), + Tfsync(Tfsync), + Rfsync(Rfsync), + Tlock(Tlock<'a>), + Rlock(Rlock), + Tgetlock(Tgetlock<'a>), + Rgetlock(Rgetlock<'a>), + Tlink(Tlink<'a>), + Rlink(Rlink), + Tmkdir(Tmkdir<'a>), + Rmkdir(Rmkdir), + Trenameat(Trenameat<'a>), + Rrenameat(Rrenameat), + Tunlinkat(Tunlinkat<'a>), + Runlinkat(Runlinkat), + Tauth(Tauth<'a>), + Rauth(Rauth), + Tversion(Tversion<'a>), + Rversion(Rversion<'a>), + Tflush(Tflush), + Rflush(Rflush), + Twalk(Twalk<'a>), + Rwalk(Rwalk), + Tread(Tread), + Rread(Rread<'a>), + Twrite(Twrite<'a>), + Rwrite(Rwrite), + Tclunk(Tclunk), + Rclunk(Rclunk), + Tremove(Tremove), + Rremove(Rremove), +} + +// Implement From for all message types +macro_rules! impl_from_for_fcall { + ($($variant:ident($ty:ty)),* $(,)?) => { + $( + impl<'a> From<$ty> for Fcall<'a> { + fn from(v: $ty) -> Fcall<'a> { + Fcall::$variant(v) + } + } + )* + }; +} + +impl_from_for_fcall! { + Rlerror(Rlerror), + Rattach(Rattach), + Tstatfs(Tstatfs), + Rstatfs(Rstatfs), + Tlopen(Tlopen), + Rlopen(Rlopen), + Rlcreate(Rlcreate), + Rsymlink(Rsymlink), + Rmknod(Rmknod), + Rrename(Rrename), + Treadlink(Treadlink), + Tgetattr(Tgetattr), + Rgetattr(Rgetattr), + Tsetattr(Tsetattr), + Rsetattr(Rsetattr), + Rxattrwalk(Rxattrwalk), + Rxattrcreate(Rxattrcreate), + Treaddir(Treaddir), + Tfsync(Tfsync), + Rfsync(Rfsync), + Rlock(Rlock), + Rlink(Rlink), + Rmkdir(Rmkdir), + Rrenameat(Rrenameat), + Runlinkat(Runlinkat), + Rauth(Rauth), + Tflush(Tflush), + Rflush(Rflush), + Rwalk(Rwalk), + Tread(Tread), + Rwrite(Rwrite), + Tclunk(Tclunk), + Rclunk(Rclunk), + Tremove(Tremove), + Rremove(Rremove), + Tattach(Tattach<'a>), + Tlcreate(Tlcreate<'a>), + Tsymlink(Tsymlink<'a>), + Tmknod(Tmknod<'a>), + Trename(Trename<'a>), + Rreadlink(Rreadlink<'a>), + Txattrwalk(Txattrwalk<'a>), + Txattrcreate(Txattrcreate<'a>), + Rreaddir(Rreaddir<'a>), + Tlock(Tlock<'a>), + Tgetlock(Tgetlock<'a>), + Rgetlock(Rgetlock<'a>), + Tlink(Tlink<'a>), + Tmkdir(Tmkdir<'a>), + Trenameat(Trenameat<'a>), + Tunlinkat(Tunlinkat<'a>), + Tauth(Tauth<'a>), + Tversion(Tversion<'a>), + Rversion(Rversion<'a>), + Twalk(Twalk<'a>), + Rread(Rread<'a>), + Twrite(Twrite<'a>), +} + +/// Tagged 9P message +#[derive(Clone, Debug)] +pub(super) struct TaggedFcall<'a> { + pub(super) tag: u16, + pub(super) fcall: Fcall<'a>, +} + +impl<'a> TaggedFcall<'a> { + /// Encode the message to a buffer + pub(super) fn encode_to_buf(self, buf: &mut Vec) -> Result<(), transport::WriteError> { + let TaggedFcall { tag, fcall } = self; + + buf.clear(); + buf.resize(4, 0); // Reserve space for size + + // Encode the message directly to the buffer (appending after the size field) + encode_fcall(buf, tag, fcall)?; + + // Write the size at the beginning + let size = u32::try_from(buf.len()).expect("buffer length exceeds u32"); + buf[0..4].copy_from_slice(&size.to_le_bytes()); + + Ok(()) + } + + /// Decode a message from a buffer + pub(super) fn decode(buf: &'a [u8]) -> Result, super::Error> { + if buf.len() < 7 { + return Err(super::Error::InvalidResponse); + } + + let mut decoder = FcallDecoder { buf: &buf[4..] }; + decoder.decode() + } +} + +// ============================================================================ +// Encoding functions +// ============================================================================ + +fn encode_u8(w: &mut W, v: u8) -> Result<(), transport::WriteError> { + w.write_all(&[v]) +} + +fn encode_u16(w: &mut W, v: u16) -> Result<(), transport::WriteError> { + w.write_all(&v.to_le_bytes()) +} + +fn encode_u32(w: &mut W, v: u32) -> Result<(), transport::WriteError> { + w.write_all(&v.to_le_bytes()) +} + +fn encode_u64(w: &mut W, v: u64) -> Result<(), transport::WriteError> { + w.write_all(&v.to_le_bytes()) +} + +fn encode_str(w: &mut W, v: &FcallStr<'_>) -> Result<(), transport::WriteError> { + encode_u16(w, u16::try_from(v.len()).expect("str length exceeds u16"))?; + w.write_all(v) +} + +fn encode_data_buf(w: &mut W, v: &[u8]) -> Result<(), transport::WriteError> { + encode_u32( + w, + u32::try_from(v.len()).expect("data buffer length exceeds u32"), + )?; + w.write_all(v) +} + +fn encode_vec_str(w: &mut W, v: &[FcallStr<'_>]) -> Result<(), transport::WriteError> { + encode_u16(w, u16::try_from(v.len()).expect("vec length exceeds u16"))?; + for s in v { + encode_str(w, s)?; + } + Ok(()) +} + +fn encode_vec_qid(w: &mut W, v: Vec) -> Result<(), transport::WriteError> { + encode_u16(w, u16::try_from(v.len()).expect("vec length exceeds u16"))?; + for q in v { + encode_qid(w, q)?; + } + Ok(()) +} + +fn encode_qidtype(w: &mut W, v: QidType) -> Result<(), transport::WriteError> { + encode_u8(w, v.bits()) +} + +fn encode_locktype(w: &mut W, v: LockType) -> Result<(), transport::WriteError> { + encode_u8(w, v.bits()) +} + +fn encode_lockstatus(w: &mut W, v: LockStatus) -> Result<(), transport::WriteError> { + encode_u8(w, v.bits()) +} + +fn encode_lockflag(w: &mut W, v: LockFlag) -> Result<(), transport::WriteError> { + encode_u32(w, v.bits()) +} + +fn encode_getattrmask(w: &mut W, v: GetattrMask) -> Result<(), transport::WriteError> { + encode_u64(w, v.bits()) +} + +fn encode_setattrmask(w: &mut W, v: SetattrMask) -> Result<(), transport::WriteError> { + encode_u32(w, v.bits()) +} + +fn encode_qid(w: &mut W, v: Qid) -> Result<(), transport::WriteError> { + encode_qidtype(w, v.typ)?; + encode_u32(w, v.version)?; + encode_u64(w, v.path)?; + Ok(()) +} + +fn encode_statfs(w: &mut W, v: Statfs) -> Result<(), transport::WriteError> { + encode_u32(w, v.typ)?; + encode_u32(w, v.bsize)?; + encode_u64(w, v.blocks)?; + encode_u64(w, v.bfree)?; + encode_u64(w, v.bavail)?; + encode_u64(w, v.files)?; + encode_u64(w, v.ffree)?; + encode_u64(w, v.fsid)?; + encode_u32(w, v.namelen)?; + Ok(()) +} + +fn encode_time(w: &mut W, v: Time) -> Result<(), transport::WriteError> { + encode_u64(w, v.sec)?; + encode_u64(w, v.nsec)?; + Ok(()) +} + +fn encode_stat(w: &mut W, v: Stat) -> Result<(), transport::WriteError> { + encode_u32(w, v.mode)?; + encode_u32(w, v.uid)?; + encode_u32(w, v.gid)?; + encode_u64(w, v.nlink)?; + encode_u64(w, v.rdev)?; + encode_u64(w, v.size)?; + encode_u64(w, v.blksize)?; + encode_u64(w, v.blocks)?; + encode_time(w, v.atime)?; + encode_time(w, v.mtime)?; + encode_time(w, v.ctime)?; + encode_time(w, v.btime)?; + encode_u64(w, v.generation)?; + encode_u64(w, v.data_version)?; + Ok(()) +} + +fn encode_setattr(w: &mut W, v: SetAttr) -> Result<(), transport::WriteError> { + encode_u32(w, v.mode)?; + encode_u32(w, v.uid)?; + encode_u32(w, v.gid)?; + encode_u64(w, v.size)?; + encode_time(w, v.atime)?; + encode_time(w, v.mtime)?; + Ok(()) +} + +fn encode_direntrydata( + w: &mut W, + v: DirEntryData<'_>, +) -> Result<(), transport::WriteError> { + encode_u32( + w, + u32::try_from(v.size()).expect("direntrydata size exceeds u32"), + )?; + for e in v.data { + encode_direntry(w, e)?; + } + Ok(()) +} + +fn encode_direntry(w: &mut W, v: DirEntry<'_>) -> Result<(), transport::WriteError> { + encode_qid(w, v.qid)?; + encode_u64(w, v.offset)?; + encode_u8(w, v.typ)?; + encode_str(w, &v.name)?; + Ok(()) +} + +fn encode_flock(w: &mut W, v: Flock<'_>) -> Result<(), transport::WriteError> { + encode_locktype(w, v.typ)?; + encode_lockflag(w, v.flags)?; + encode_u64(w, v.start)?; + encode_u64(w, v.length)?; + encode_u32(w, v.proc_id)?; + encode_str(w, &v.client_id)?; + Ok(()) +} + +fn encode_getlock(w: &mut W, v: Getlock<'_>) -> Result<(), transport::WriteError> { + encode_locktype(w, v.typ)?; + encode_u64(w, v.start)?; + encode_u64(w, v.length)?; + encode_u32(w, v.proc_id)?; + encode_str(w, &v.client_id)?; + Ok(()) +} + +fn encode_fcall( + w: &mut W, + tag: u16, + fcall: Fcall<'_>, +) -> Result<(), transport::WriteError> { + match fcall { + Fcall::Rlerror(v) => { + encode_u8(w, FcallType::Rlerror as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.ecode)?; + } + Fcall::Tattach(v) => { + encode_u8(w, FcallType::Tattach as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_u32(w, v.afid)?; + encode_str(w, &v.uname)?; + encode_str(w, &v.aname)?; + encode_u32(w, v.n_uname)?; + } + Fcall::Rattach(v) => { + encode_u8(w, FcallType::Rattach as u8)?; + encode_u16(w, tag)?; + encode_qid(w, v.qid)?; + } + Fcall::Tstatfs(v) => { + encode_u8(w, FcallType::Tstatfs as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + } + Fcall::Rstatfs(v) => { + encode_u8(w, FcallType::Rstatfs as u8)?; + encode_u16(w, tag)?; + encode_statfs(w, v.statfs)?; + } + Fcall::Tlopen(v) => { + encode_u8(w, FcallType::Tlopen as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_u32(w, v.flags.bits())?; + } + Fcall::Rlopen(v) => { + encode_u8(w, FcallType::Rlopen as u8)?; + encode_u16(w, tag)?; + encode_qid(w, v.qid)?; + encode_u32(w, v.iounit)?; + } + Fcall::Tlcreate(v) => { + encode_u8(w, FcallType::Tlcreate as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_str(w, &v.name)?; + encode_u32(w, v.flags.bits())?; + encode_u32(w, v.mode)?; + encode_u32(w, v.gid)?; + } + Fcall::Rlcreate(v) => { + encode_u8(w, FcallType::Rlcreate as u8)?; + encode_u16(w, tag)?; + encode_qid(w, v.qid)?; + encode_u32(w, v.iounit)?; + } + Fcall::Tsymlink(v) => { + encode_u8(w, FcallType::Tsymlink as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_str(w, &v.name)?; + encode_str(w, &v.symtgt)?; + encode_u32(w, v.gid)?; + } + Fcall::Rsymlink(v) => { + encode_u8(w, FcallType::Rsymlink as u8)?; + encode_u16(w, tag)?; + encode_qid(w, v.qid)?; + } + Fcall::Tmknod(v) => { + encode_u8(w, FcallType::Tmknod as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.dfid)?; + encode_str(w, &v.name)?; + encode_u32(w, v.mode)?; + encode_u32(w, v.major)?; + encode_u32(w, v.minor)?; + encode_u32(w, v.gid)?; + } + Fcall::Rmknod(v) => { + encode_u8(w, FcallType::Rmknod as u8)?; + encode_u16(w, tag)?; + encode_qid(w, v.qid)?; + } + Fcall::Trename(v) => { + encode_u8(w, FcallType::Trename as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_u32(w, v.dfid)?; + encode_str(w, &v.name)?; + } + Fcall::Rrename(_) => { + encode_u8(w, FcallType::Rrename as u8)?; + encode_u16(w, tag)?; + } + Fcall::Treadlink(v) => { + encode_u8(w, FcallType::Treadlink as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + } + Fcall::Rreadlink(v) => { + encode_u8(w, FcallType::Rreadlink as u8)?; + encode_u16(w, tag)?; + encode_str(w, &v.target)?; + } + Fcall::Tgetattr(v) => { + encode_u8(w, FcallType::Tgetattr as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_getattrmask(w, v.req_mask)?; + } + Fcall::Rgetattr(v) => { + encode_u8(w, FcallType::Rgetattr as u8)?; + encode_u16(w, tag)?; + encode_getattrmask(w, v.valid)?; + encode_qid(w, v.qid)?; + encode_stat(w, v.stat)?; + } + Fcall::Tsetattr(v) => { + encode_u8(w, FcallType::Tsetattr as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_setattrmask(w, v.valid)?; + encode_setattr(w, v.stat)?; + } + Fcall::Rsetattr(_) => { + encode_u8(w, FcallType::Rsetattr as u8)?; + encode_u16(w, tag)?; + } + Fcall::Txattrwalk(v) => { + encode_u8(w, FcallType::Txattrwalk as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_u32(w, v.new_fid)?; + encode_str(w, &v.name)?; + } + Fcall::Rxattrwalk(v) => { + encode_u8(w, FcallType::Rxattrwalk as u8)?; + encode_u16(w, tag)?; + encode_u64(w, v.size)?; + } + Fcall::Txattrcreate(v) => { + encode_u8(w, FcallType::Txattrcreate as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_str(w, &v.name)?; + encode_u64(w, v.attr_size)?; + encode_u32(w, v.flags)?; + } + Fcall::Rxattrcreate(_) => { + encode_u8(w, FcallType::Rxattrcreate as u8)?; + encode_u16(w, tag)?; + } + Fcall::Treaddir(v) => { + encode_u8(w, FcallType::Treaddir as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_u64(w, v.offset)?; + encode_u32(w, v.count)?; + } + Fcall::Rreaddir(v) => { + encode_u8(w, FcallType::Rreaddir as u8)?; + encode_u16(w, tag)?; + encode_direntrydata(w, v.data)?; + } + Fcall::Tfsync(v) => { + encode_u8(w, FcallType::Tfsync as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_u32(w, v.datasync)?; + } + Fcall::Rfsync(_) => { + encode_u8(w, FcallType::Rfsync as u8)?; + encode_u16(w, tag)?; + } + Fcall::Tlock(v) => { + encode_u8(w, FcallType::Tlock as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_flock(w, v.flock)?; + } + Fcall::Rlock(v) => { + encode_u8(w, FcallType::Rlock as u8)?; + encode_u16(w, tag)?; + encode_lockstatus(w, v.status)?; + } + Fcall::Tgetlock(v) => { + encode_u8(w, FcallType::Tgetlock as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_getlock(w, v.flock)?; + } + Fcall::Rgetlock(v) => { + encode_u8(w, FcallType::Rgetlock as u8)?; + encode_u16(w, tag)?; + encode_getlock(w, v.flock)?; + } + Fcall::Tlink(v) => { + encode_u8(w, FcallType::Tlink as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.dfid)?; + encode_u32(w, v.fid)?; + encode_str(w, &v.name)?; + } + Fcall::Rlink(_) => { + encode_u8(w, FcallType::Rlink as u8)?; + encode_u16(w, tag)?; + } + Fcall::Tmkdir(v) => { + encode_u8(w, FcallType::Tmkdir as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.dfid)?; + encode_str(w, &v.name)?; + encode_u32(w, v.mode)?; + encode_u32(w, v.gid)?; + } + Fcall::Rmkdir(v) => { + encode_u8(w, FcallType::Rmkdir as u8)?; + encode_u16(w, tag)?; + encode_qid(w, v.qid)?; + } + Fcall::Trenameat(v) => { + encode_u8(w, FcallType::Trenameat as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.olddfid)?; + encode_str(w, &v.oldname)?; + encode_u32(w, v.newdfid)?; + encode_str(w, &v.newname)?; + } + Fcall::Rrenameat(_) => { + encode_u8(w, FcallType::Rrenameat as u8)?; + encode_u16(w, tag)?; + } + Fcall::Tunlinkat(v) => { + encode_u8(w, FcallType::Tunlinkat as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.dfid)?; + encode_str(w, &v.name)?; + encode_u32(w, v.flags)?; + } + Fcall::Runlinkat(_) => { + encode_u8(w, FcallType::Runlinkat as u8)?; + encode_u16(w, tag)?; + } + Fcall::Tauth(v) => { + encode_u8(w, FcallType::Tauth as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.afid)?; + encode_str(w, &v.uname)?; + encode_str(w, &v.aname)?; + encode_u32(w, v.n_uname)?; + } + Fcall::Rauth(v) => { + encode_u8(w, FcallType::Rauth as u8)?; + encode_u16(w, tag)?; + encode_qid(w, v.aqid)?; + } + Fcall::Tversion(v) => { + encode_u8(w, FcallType::Tversion as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.msize)?; + encode_str(w, &v.version)?; + } + Fcall::Rversion(v) => { + encode_u8(w, FcallType::Rversion as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.msize)?; + encode_str(w, &v.version)?; + } + Fcall::Tflush(v) => { + encode_u8(w, FcallType::Tflush as u8)?; + encode_u16(w, tag)?; + encode_u16(w, v.oldtag)?; + } + Fcall::Rflush(_) => { + encode_u8(w, FcallType::Rflush as u8)?; + encode_u16(w, tag)?; + } + Fcall::Twalk(v) => { + encode_u8(w, FcallType::Twalk as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_u32(w, v.new_fid)?; + encode_vec_str(w, &v.wnames)?; + } + Fcall::Rwalk(v) => { + encode_u8(w, FcallType::Rwalk as u8)?; + encode_u16(w, tag)?; + encode_vec_qid(w, v.wqids)?; + } + Fcall::Tread(v) => { + encode_u8(w, FcallType::Tread as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_u64(w, v.offset)?; + encode_u32(w, v.count)?; + } + Fcall::Rread(v) => { + encode_u8(w, FcallType::Rread as u8)?; + encode_u16(w, tag)?; + encode_data_buf(w, &v.data)?; + } + Fcall::Twrite(v) => { + encode_u8(w, FcallType::Twrite as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + encode_u64(w, v.offset)?; + encode_data_buf(w, &v.data)?; + } + Fcall::Rwrite(v) => { + encode_u8(w, FcallType::Rwrite as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.count)?; + } + Fcall::Tclunk(v) => { + encode_u8(w, FcallType::Tclunk as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + } + Fcall::Rclunk(_) => { + encode_u8(w, FcallType::Rclunk as u8)?; + encode_u16(w, tag)?; + } + Fcall::Tremove(v) => { + encode_u8(w, FcallType::Tremove as u8)?; + encode_u16(w, tag)?; + encode_u32(w, v.fid)?; + } + Fcall::Rremove(_) => { + encode_u8(w, FcallType::Rremove as u8)?; + encode_u16(w, tag)?; + } + } + Ok(()) +} + +// ============================================================================ +// Decoding +// ============================================================================ + +struct FcallDecoder<'b> { + buf: &'b [u8], +} + +impl<'b> FcallDecoder<'b> { + fn decode_u8(&mut self) -> Result { + if let Some(v) = self.buf.first() { + self.buf = &self.buf[1..]; + Ok(*v) + } else { + Err(super::Error::InvalidResponse) + } + } + + fn decode_u16(&mut self) -> Result { + if self.buf.len() >= 2 { + let v = u16::from_le_bytes(self.buf[0..2].try_into().unwrap()); + self.buf = &self.buf[2..]; + Ok(v) + } else { + Err(super::Error::InvalidResponse) + } + } + + fn decode_u32(&mut self) -> Result { + if self.buf.len() >= 4 { + let v = u32::from_le_bytes(self.buf[0..4].try_into().unwrap()); + self.buf = &self.buf[4..]; + Ok(v) + } else { + Err(super::Error::InvalidResponse) + } + } + + fn decode_u64(&mut self) -> Result { + if self.buf.len() >= 8 { + let v = u64::from_le_bytes(self.buf[0..8].try_into().unwrap()); + self.buf = &self.buf[8..]; + Ok(v) + } else { + Err(super::Error::InvalidResponse) + } + } + + fn decode_str(&mut self) -> Result, super::Error> { + let n = self.decode_u16()? as usize; + if self.buf.len() >= n { + let v = FcallStr::Borrowed(&self.buf[..n]); + self.buf = &self.buf[n..]; + Ok(v) + } else { + Err(super::Error::InvalidResponse) + } + } + + fn decode_data_buf(&mut self) -> Result, super::Error> { + let n = self.decode_u32()? as usize; + if self.buf.len() >= n { + let v = &self.buf[..n]; + self.buf = &self.buf[n..]; + Ok(Cow::from(v)) + } else { + Err(super::Error::InvalidResponse) + } + } + + fn decode_vec_qid(&mut self) -> Result, super::Error> { + let len = self.decode_u16()?; + let mut v = Vec::new(); + for _ in 0..len { + v.push(self.decode_qid()?); + } + Ok(v) + } + + fn decode_direntrydata(&mut self) -> Result, super::Error> { + let end_len = self.buf.len() - self.decode_u32()? as usize; + let mut v = Vec::new(); + while self.buf.len() > end_len { + v.push(self.decode_direntry()?); + } + Ok(DirEntryData::with(v)) + } + + fn decode_qidtype(&mut self) -> Result { + Ok(QidType::from_bits_truncate(self.decode_u8()?)) + } + + fn decode_locktype(&mut self) -> Result { + Ok(LockType::from_bits_truncate(self.decode_u8()?)) + } + + fn decode_lockstatus(&mut self) -> Result { + Ok(LockStatus::from_bits_truncate(self.decode_u8()?)) + } + + fn decode_lockflag(&mut self) -> Result { + Ok(LockFlag::from_bits_truncate(self.decode_u32()?)) + } + + fn decode_getattrmask(&mut self) -> Result { + Ok(GetattrMask::from_bits_truncate(self.decode_u64()?)) + } + + fn decode_setattrmask(&mut self) -> Result { + Ok(SetattrMask::from_bits_truncate(self.decode_u32()?)) + } + + fn decode_qid(&mut self) -> Result { + Ok(Qid { + typ: self.decode_qidtype()?, + version: self.decode_u32()?, + path: self.decode_u64()?, + }) + } + + fn decode_statfs(&mut self) -> Result { + Ok(Statfs { + typ: self.decode_u32()?, + bsize: self.decode_u32()?, + blocks: self.decode_u64()?, + bfree: self.decode_u64()?, + bavail: self.decode_u64()?, + files: self.decode_u64()?, + ffree: self.decode_u64()?, + fsid: self.decode_u64()?, + namelen: self.decode_u32()?, + }) + } + + fn decode_time(&mut self) -> Result { + Ok(Time { + sec: self.decode_u64()?, + nsec: self.decode_u64()?, + }) + } + + fn decode_stat(&mut self) -> Result { + Ok(Stat { + mode: self.decode_u32()?, + uid: self.decode_u32()?, + gid: self.decode_u32()?, + nlink: self.decode_u64()?, + rdev: self.decode_u64()?, + size: self.decode_u64()?, + blksize: self.decode_u64()?, + blocks: self.decode_u64()?, + atime: self.decode_time()?, + mtime: self.decode_time()?, + ctime: self.decode_time()?, + btime: self.decode_time()?, + generation: self.decode_u64()?, + data_version: self.decode_u64()?, + }) + } + + fn decode_setattr(&mut self) -> Result { + Ok(SetAttr { + mode: self.decode_u32()?, + uid: self.decode_u32()?, + gid: self.decode_u32()?, + size: self.decode_u64()?, + atime: self.decode_time()?, + mtime: self.decode_time()?, + }) + } + + fn decode_direntry(&mut self) -> Result, super::Error> { + Ok(DirEntry { + qid: self.decode_qid()?, + offset: self.decode_u64()?, + typ: self.decode_u8()?, + name: self.decode_str()?, + }) + } + + fn decode_flock(&mut self) -> Result, super::Error> { + Ok(Flock { + typ: self.decode_locktype()?, + flags: self.decode_lockflag()?, + start: self.decode_u64()?, + length: self.decode_u64()?, + proc_id: self.decode_u32()?, + client_id: self.decode_str()?, + }) + } + + fn decode_getlock(&mut self) -> Result, super::Error> { + Ok(Getlock { + typ: self.decode_locktype()?, + start: self.decode_u64()?, + length: self.decode_u64()?, + proc_id: self.decode_u32()?, + client_id: self.decode_str()?, + }) + } + + fn decode(&mut self) -> Result, super::Error> { + let msg_type = FcallType::from_u8(self.decode_u8()?); + let tag = self.decode_u16()?; + let fcall = match msg_type { + Some(FcallType::Rlerror) => Fcall::Rlerror(Rlerror { + ecode: self.decode_u32()?, + }), + Some(FcallType::Tattach) => Fcall::Tattach(Tattach { + fid: self.decode_u32()?, + afid: self.decode_u32()?, + uname: self.decode_str()?, + aname: self.decode_str()?, + n_uname: self.decode_u32()?, + }), + Some(FcallType::Rattach) => Fcall::Rattach(Rattach { + qid: self.decode_qid()?, + }), + Some(FcallType::Tstatfs) => Fcall::Tstatfs(Tstatfs { + fid: self.decode_u32()?, + }), + Some(FcallType::Rstatfs) => Fcall::Rstatfs(Rstatfs { + statfs: self.decode_statfs()?, + }), + Some(FcallType::Tlopen) => Fcall::Tlopen(Tlopen { + fid: self.decode_u32()?, + flags: LOpenFlags::from_bits_truncate(self.decode_u32()?), + }), + Some(FcallType::Rlopen) => Fcall::Rlopen(Rlopen { + qid: self.decode_qid()?, + iounit: self.decode_u32()?, + }), + Some(FcallType::Tlcreate) => Fcall::Tlcreate(Tlcreate { + fid: self.decode_u32()?, + name: self.decode_str()?, + flags: LOpenFlags::from_bits_truncate(self.decode_u32()?), + mode: self.decode_u32()?, + gid: self.decode_u32()?, + }), + Some(FcallType::Rlcreate) => Fcall::Rlcreate(Rlcreate { + qid: self.decode_qid()?, + iounit: self.decode_u32()?, + }), + Some(FcallType::Tsymlink) => Fcall::Tsymlink(Tsymlink { + fid: self.decode_u32()?, + name: self.decode_str()?, + symtgt: self.decode_str()?, + gid: self.decode_u32()?, + }), + Some(FcallType::Rsymlink) => Fcall::Rsymlink(Rsymlink { + qid: self.decode_qid()?, + }), + Some(FcallType::Tmknod) => Fcall::Tmknod(Tmknod { + dfid: self.decode_u32()?, + name: self.decode_str()?, + mode: self.decode_u32()?, + major: self.decode_u32()?, + minor: self.decode_u32()?, + gid: self.decode_u32()?, + }), + Some(FcallType::Rmknod) => Fcall::Rmknod(Rmknod { + qid: self.decode_qid()?, + }), + Some(FcallType::Trename) => Fcall::Trename(Trename { + fid: self.decode_u32()?, + dfid: self.decode_u32()?, + name: self.decode_str()?, + }), + Some(FcallType::Rrename) => Fcall::Rrename(Rrename {}), + Some(FcallType::Treadlink) => Fcall::Treadlink(Treadlink { + fid: self.decode_u32()?, + }), + Some(FcallType::Rreadlink) => Fcall::Rreadlink(Rreadlink { + target: self.decode_str()?, + }), + Some(FcallType::Tgetattr) => Fcall::Tgetattr(Tgetattr { + fid: self.decode_u32()?, + req_mask: self.decode_getattrmask()?, + }), + Some(FcallType::Rgetattr) => Fcall::Rgetattr(Rgetattr { + valid: self.decode_getattrmask()?, + qid: self.decode_qid()?, + stat: self.decode_stat()?, + }), + Some(FcallType::Tsetattr) => Fcall::Tsetattr(Tsetattr { + fid: self.decode_u32()?, + valid: self.decode_setattrmask()?, + stat: self.decode_setattr()?, + }), + Some(FcallType::Rsetattr) => Fcall::Rsetattr(Rsetattr {}), + Some(FcallType::Txattrwalk) => Fcall::Txattrwalk(Txattrwalk { + fid: self.decode_u32()?, + new_fid: self.decode_u32()?, + name: self.decode_str()?, + }), + Some(FcallType::Rxattrwalk) => Fcall::Rxattrwalk(Rxattrwalk { + size: self.decode_u64()?, + }), + Some(FcallType::Txattrcreate) => Fcall::Txattrcreate(Txattrcreate { + fid: self.decode_u32()?, + name: self.decode_str()?, + attr_size: self.decode_u64()?, + flags: self.decode_u32()?, + }), + Some(FcallType::Rxattrcreate) => Fcall::Rxattrcreate(Rxattrcreate {}), + Some(FcallType::Treaddir) => Fcall::Treaddir(Treaddir { + fid: self.decode_u32()?, + offset: self.decode_u64()?, + count: self.decode_u32()?, + }), + Some(FcallType::Rreaddir) => Fcall::Rreaddir(Rreaddir { + data: self.decode_direntrydata()?, + }), + Some(FcallType::Tfsync) => Fcall::Tfsync(Tfsync { + fid: self.decode_u32()?, + datasync: self.decode_u32()?, + }), + Some(FcallType::Rfsync) => Fcall::Rfsync(Rfsync {}), + Some(FcallType::Tlock) => Fcall::Tlock(Tlock { + fid: self.decode_u32()?, + flock: self.decode_flock()?, + }), + Some(FcallType::Rlock) => Fcall::Rlock(Rlock { + status: self.decode_lockstatus()?, + }), + Some(FcallType::Tgetlock) => Fcall::Tgetlock(Tgetlock { + fid: self.decode_u32()?, + flock: self.decode_getlock()?, + }), + Some(FcallType::Rgetlock) => Fcall::Rgetlock(Rgetlock { + flock: self.decode_getlock()?, + }), + Some(FcallType::Tlink) => Fcall::Tlink(Tlink { + dfid: self.decode_u32()?, + fid: self.decode_u32()?, + name: self.decode_str()?, + }), + Some(FcallType::Rlink) => Fcall::Rlink(Rlink {}), + Some(FcallType::Tmkdir) => Fcall::Tmkdir(Tmkdir { + dfid: self.decode_u32()?, + name: self.decode_str()?, + mode: self.decode_u32()?, + gid: self.decode_u32()?, + }), + Some(FcallType::Rmkdir) => Fcall::Rmkdir(Rmkdir { + qid: self.decode_qid()?, + }), + Some(FcallType::Trenameat) => Fcall::Trenameat(Trenameat { + olddfid: self.decode_u32()?, + oldname: self.decode_str()?, + newdfid: self.decode_u32()?, + newname: self.decode_str()?, + }), + Some(FcallType::Rrenameat) => Fcall::Rrenameat(Rrenameat {}), + Some(FcallType::Tunlinkat) => Fcall::Tunlinkat(Tunlinkat { + dfid: self.decode_u32()?, + name: self.decode_str()?, + flags: self.decode_u32()?, + }), + Some(FcallType::Runlinkat) => Fcall::Runlinkat(Runlinkat {}), + Some(FcallType::Tauth) => Fcall::Tauth(Tauth { + afid: self.decode_u32()?, + uname: self.decode_str()?, + aname: self.decode_str()?, + n_uname: self.decode_u32()?, + }), + Some(FcallType::Rauth) => Fcall::Rauth(Rauth { + aqid: self.decode_qid()?, + }), + Some(FcallType::Tversion) => Fcall::Tversion(Tversion { + msize: self.decode_u32()?, + version: self.decode_str()?, + }), + Some(FcallType::Rversion) => Fcall::Rversion(Rversion { + msize: self.decode_u32()?, + version: self.decode_str()?, + }), + Some(FcallType::Tflush) => Fcall::Tflush(Tflush { + oldtag: self.decode_u16()?, + }), + Some(FcallType::Rflush) => Fcall::Rflush(Rflush {}), + Some(FcallType::Twalk) => Fcall::Twalk(Twalk { + fid: self.decode_u32()?, + new_fid: self.decode_u32()?, + wnames: { + let len = self.decode_u16()?; + let mut wnames = Vec::new(); + for _ in 0..len { + wnames.push(self.decode_str()?); + } + wnames + }, + }), + Some(FcallType::Rwalk) => Fcall::Rwalk(Rwalk { + wqids: self.decode_vec_qid()?, + }), + Some(FcallType::Tread) => Fcall::Tread(Tread { + fid: self.decode_u32()?, + offset: self.decode_u64()?, + count: self.decode_u32()?, + }), + Some(FcallType::Rread) => Fcall::Rread(Rread { + data: self.decode_data_buf()?, + }), + Some(FcallType::Twrite) => Fcall::Twrite(Twrite { + fid: self.decode_u32()?, + offset: self.decode_u64()?, + data: self.decode_data_buf()?, + }), + Some(FcallType::Rwrite) => Fcall::Rwrite(Rwrite { + count: self.decode_u32()?, + }), + Some(FcallType::Tclunk) => Fcall::Tclunk(Tclunk { + fid: self.decode_u32()?, + }), + Some(FcallType::Rclunk) => Fcall::Rclunk(Rclunk {}), + Some(FcallType::Tremove) => Fcall::Tremove(Tremove { + fid: self.decode_u32()?, + }), + Some(FcallType::Rremove) => Fcall::Rremove(Rremove {}), + None => return Err(super::Error::InvalidResponse), + }; + Ok(TaggedFcall { tag, fcall }) + } +} diff --git a/litebox/src/fs/nine_p/mod.rs b/litebox/src/fs/nine_p/mod.rs new file mode 100644 index 000000000..d727658fb --- /dev/null +++ b/litebox/src/fs/nine_p/mod.rs @@ -0,0 +1,925 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! 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 +//! compatibility with POSIX semantics. +//! +//! # Submodules +//! +//! The 9P implementation is split into several submodules: +//! - `fcall` - Protocol message definitions and encoding/decoding +//! - `transport` - Transport layer traits and message I/O +//! - `client` - High-level 9P client for protocol operations + +use alloc::string::String; +use alloc::vec::Vec; +use core::num::NonZeroUsize; +use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +use thiserror::Error; + +use crate::fs::OFlags; +use crate::fs::errors::{ + ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, + ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, +}; +use crate::fs::nine_p::fcall::Rlerror; +use crate::path::Arg; +use crate::{LiteBox, sync}; + +mod client; +mod fcall; + +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. +/// +/// 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. +/// - `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, + /// 9P client for protocol operations + client: client::Client, + /// Root (attached to the root of the remote filesystem) + root: (fcall::Qid, fcall::Fid, String), + // cwd invariant: always ends with a `/` + current_working_dir: String, + /// Whether `unlinkat` is supported by the server + unlinkat_supported: AtomicBool, +} + +impl + FileSystem +{ + /// 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. + /// + /// # 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) + /// + /// # Errors + /// + /// Returns an error if version negotiation or attach fails. + pub fn new( + litebox: &LiteBox, + transport: T, + msize: u32, + username: &str, + path: &str, + ) -> Result { + let client = client::Client::new(transport, msize)?; + let (qid, fid) = client.attach(username, path)?; + + Ok(Self { + litebox: litebox.clone(), + client, + root: (qid, fid, String::from(path)), + current_working_dir: String::from("/"), + 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()?) + } + } + + /// Walk to a path and return the fid + fn walk_to(&self, path: &str) -> Result { + 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) + } + } + + /// Walk to the parent of a path and return the parent fid and the name + fn walk_to_parent<'a>(&self, path: &'a str) -> Result<(fcall::Fid, &'a str), Error> { + let components: Vec<&str> = path + .normalized_components() + .map_err(|_| Error::InvalidPathname)? + .collect(); + if components.is_empty() { + return Err(Error::InvalidPathname); + } + + 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)) + } + } + + /// 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 + } + + /// 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 + } + } + + /// Convert getattr response to FileStatus + fn rgetattr_to_file_status(attr: &fcall::Rgetattr) -> super::FileStatus { + let file_type = Self::qid_type_to_file_type(attr.qid.typ); + + if attr.valid.contains(fcall::GetattrMask::BASIC) { + super::FileStatus { + file_type, + mode: super::Mode::from_bits_truncate(attr.stat.mode), + size: usize::try_from(attr.stat.size).expect("file size exceeds usize"), + owner: super::UserInfo { + user: u16::try_from(attr.stat.uid).expect("uid exceeds u16"), + group: u16::try_from(attr.stat.gid).expect("gid exceeds u16"), + }, + node_info: super::NodeInfo { + dev: DEVICE_ID, + ino: usize::try_from(attr.qid.path).expect("inode number exceeds usize"), + rdev: NonZeroUsize::new( + usize::try_from(attr.stat.rdev).expect("rdev exceeds usize"), + ), + }, + blksize: usize::try_from(attr.stat.blksize).expect("block size exceeds usize"), + } + } else { + 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).expect("file size exceeds usize") + } else { + 0 + }, + owner: super::UserInfo { + user: if attr.valid.contains(fcall::GetattrMask::UID) { + u16::try_from(attr.stat.uid).expect("uid exceeds u16") + } else { + 0 + }, + group: if attr.valid.contains(fcall::GetattrMask::GID) { + u16::try_from(attr.stat.gid).expect("gid exceeds u16") + } else { + 0 + }, + }, + node_info: super::NodeInfo { + dev: DEVICE_ID, + ino: usize::try_from(attr.qid.path).expect("inode number exceeds usize"), + rdev: if attr.valid.contains(fcall::GetattrMask::RDEV) { + NonZeroUsize::new( + usize::try_from(attr.stat.rdev).expect("rdev exceeds usize"), + ) + } else { + None + }, + }, + blksize: if attr.valid.contains(fcall::GetattrMask::BLOCKS) { + usize::try_from(attr.stat.blksize).expect("block size exceeds usize") + } else { + 0 + }, + } + } + } + + 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 }); + let _ = 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; + } + } + + let fid = self.walk_to(&path)?; + let result = self.client.remove(fid); + self.client.free_fid(fid); + result + } +} + +impl Drop + for FileSystem +{ + fn drop(&mut self) { + let _ = self.client.clunk(self.root.1); + } +} + +impl + super::private::Sealed for FileSystem +{ +} + +impl + super::FileSystem for FileSystem +{ + #[allow(clippy::similar_names)] + fn open( + &self, + path: impl crate::path::Arg, + flags: super::OFlags, + mode: super::Mode, + ) -> Result, super::errors::OpenError> { + 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:?}") + } + + 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 = self.client.open(new_fid, lflags)?; + (qid, new_fid) + }; + + let descriptor = Descriptor { + fid: new_fid, + offset: AtomicUsize::new(0), + qid: new_qid, + }; + + 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 { + let _ = self.client.clunk(entry.entry.fid); + } + Ok(()) + } + + fn read( + &self, + fd: &FileFd, + buf: &mut [u8], + offset: Option, + ) -> Result { + // Extract fid and current offset, releasing the descriptor table lock + // before performing potentially blocking I/O. + let (fid, current_offset) = self + .litebox + .descriptor_table() + .with_entry(fd, |desc| { + (desc.entry.fid, desc.entry.offset.load(Ordering::SeqCst)) + }) + .ok_or(super::errors::ReadError::ClosedFd)?; + + let read_offset = match offset { + Some(o) => o, + None => current_offset, + }; + + let bytes_read = self.client.read(fid, read_offset as u64, buf)?; + + // Update offset if not using explicit offset + if offset.is_none() { + self.litebox.descriptor_table().with_entry(fd, |desc| { + desc.entry.offset.fetch_add(bytes_read, Ordering::SeqCst); + }); + } + + Ok(bytes_read) + } + + fn write( + &self, + fd: &FileFd, + buf: &[u8], + offset: Option, + ) -> Result { + // Extract fid and current offset, releasing the descriptor table lock + // before performing potentially blocking I/O. + let (fid, current_offset) = self + .litebox + .descriptor_table() + .with_entry(fd, |desc| { + (desc.entry.fid, desc.entry.offset.load(Ordering::SeqCst)) + }) + .ok_or(super::errors::WriteError::ClosedFd)?; + + let write_offset = match offset { + Some(o) => o, + None => current_offset, + }; + + let bytes_written = self.client.write(fid, write_offset as u64, buf)?; + + // Update offset if not using explicit offset + if offset.is_none() { + self.litebox.descriptor_table().with_entry(fd, |desc| { + desc.entry.offset.fetch_add(bytes_written, Ordering::SeqCst); + }); + } + + Ok(bytes_written) + } + + fn seek( + &self, + fd: &FileFd, + offset: isize, + whence: super::SeekWhence, + ) -> Result { + // Extract fid and current offset, releasing the descriptor table lock + // before performing potentially blocking I/O (getattr for SeekWhence::RelativeToEnd). + let (fid, current_offset) = self + .litebox + .descriptor_table() + .with_entry(fd, |desc| { + (desc.entry.fid, desc.entry.offset.load(Ordering::SeqCst)) + }) + .ok_or(SeekError::ClosedFd)?; + + let base = match whence { + super::SeekWhence::RelativeToBeginning => 0, + super::SeekWhence::RelativeToCurrentOffset => current_offset, + super::SeekWhence::RelativeToEnd => { + let attr = self.client.getattr(fid, fcall::GetattrMask::SIZE)?; + usize::try_from(attr.stat.size).expect("file size exceeds usize") + } + }; + let new_offset = base + .checked_add_signed(offset) + .ok_or(SeekError::InvalidOffset)?; + + self.litebox.descriptor_table().with_entry(fd, |desc| { + desc.entry.offset.store(new_offset, Ordering::SeqCst); + }); + Ok(new_offset) + } + + fn truncate( + &self, + fd: &FileFd, + length: usize, + reset_offset: bool, + ) -> Result<(), super::errors::TruncateError> { + // Extract fid and qid, releasing the descriptor table lock + // before performing potentially blocking I/O. + let (fid, qid) = self + .litebox + .descriptor_table() + .with_entry(fd, |desc| (desc.entry.fid, desc.entry.qid)) + .ok_or(super::errors::TruncateError::ClosedFd)?; + + if qid.typ.contains(fcall::QidType::DIR) { + return Err(super::errors::TruncateError::IsDirectory); + } + + let stat = fcall::SetAttr { + mode: 0, + uid: 0, + gid: 0, + size: length as u64, + atime: fcall::Time::default(), + mtime: fcall::Time::default(), + }; + + self.client.setattr(fid, fcall::SetattrMask::SIZE, stat)?; + + if reset_offset { + self.litebox.descriptor_table().with_entry(fd, |desc| { + desc.entry.offset.store(0, Ordering::SeqCst); + }); + } + + Ok(()) + } + + fn chmod( + &self, + path: impl crate::path::Arg, + mode: super::Mode, + ) -> Result<(), super::errors::ChmodError> { + let path = self.absolute_path(path)?; + let fid = self.walk_to(&path)?; + + let stat = fcall::SetAttr { + mode: mode.bits(), + uid: 0, + gid: 0, + size: 0, + atime: fcall::Time::default(), + mtime: fcall::Time::default(), + }; + + let result = self.client.setattr(fid, fcall::SetattrMask::MODE, stat); + let _ = self.client.clunk(fid); + + result.map_err(ChmodError::from) + } + + fn chown( + &self, + path: impl crate::path::Arg, + user: Option, + group: Option, + ) -> Result<(), super::errors::ChownError> { + let path = self.absolute_path(path)?; + let fid = self.walk_to(&path)?; + + let mut valid = fcall::SetattrMask::empty(); + let uid = match user { + Some(u) => { + valid |= fcall::SetattrMask::UID; + u32::from(u) + } + None => 0, + }; + let gid = match group { + Some(g) => { + valid |= fcall::SetattrMask::GID; + u32::from(g) + } + None => 0, + }; + let stat = fcall::SetAttr { + mode: 0, + uid, + gid, + size: 0, + atime: fcall::Time::default(), + mtime: fcall::Time::default(), + }; + + let result = self.client.setattr(fid, valid, stat); + let _ = self.client.clunk(fid); + + result.map_err(ChownError::from) + } + + fn unlink(&self, path: impl crate::path::Arg) -> Result<(), super::errors::UnlinkError> { + self.remove_file_or_dir(path, true) + .map_err(UnlinkError::from) + } + + fn mkdir(&self, path: impl crate::path::Arg, mode: super::Mode) -> Result<(), MkdirError> { + let path = self.absolute_path(path)?; + + let (parent_fid, name) = self.walk_to_parent(&path)?; + + let result = self.client.mkdir(parent_fid, name, mode.bits(), 0); + let _ = self.client.clunk(parent_fid); + + result.map(|_| ()).map_err(MkdirError::from) + } + + fn rmdir(&self, path: impl crate::path::Arg) -> Result<(), RmdirError> { + self.remove_file_or_dir(path, false) + .map_err(RmdirError::from) + } + + fn read_dir( + &self, + fd: &FileFd, + ) -> Result, super::errors::ReadDirError> { + // Extract fid and qid, releasing the descriptor table lock + // before performing potentially blocking I/O. + let (fid, qid) = self + .litebox + .descriptor_table() + .with_entry(fd, |desc| (desc.entry.fid, desc.entry.qid)) + .ok_or(super::errors::ReadDirError::ClosedFd)?; + + if !qid.typ.contains(fcall::QidType::DIR) { + return Err(super::errors::ReadDirError::NotADirectory); + } + + // Perform blocking I/O without holding any locks. + let entries = self.client.readdir_all(fid)?; + + 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 + }; + + 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).expect("inode number exceeds usize"), + rdev: None, + }), + } + }) + .collect(); + + Ok(dir_entries) + } + + fn file_status( + &self, + path: impl crate::path::Arg, + ) -> Result { + let path = self.absolute_path(path)?; + let fid = self.walk_to(&path)?; + + let result = self.client.getattr(fid, fcall::GetattrMask::ALL); + let _ = self.client.clunk(fid); + + result + .map(|attr| Self::rgetattr_to_file_status(&attr)) + .map_err(FileStatusError::from) + } + + fn fd_file_status( + &self, + fd: &FileFd, + ) -> Result { + // Extract fid, releasing the descriptor table lock + // before performing potentially blocking I/O. + let fid = self + .litebox + .descriptor_table() + .with_entry(fd, |desc| desc.entry.fid) + .ok_or(super::errors::FileStatusError::ClosedFd)?; + + // Perform blocking I/O without holding any locks. + let attr = self.client.getattr(fid, fcall::GetattrMask::ALL)?; + + Ok(Self::rgetattr_to_file_status(&attr)) + } +} + +/// Internal descriptor state for a 9P file descriptor +#[derive(Debug)] +struct Descriptor { + /// The 9P fid for this file + fid: fcall::Fid, + /// Current file offset (9P doesn't track this server-side) + offset: AtomicUsize, + /// 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; + Descriptor; + -> FileFd; +} diff --git a/litebox/src/fs/nine_p/tests.rs b/litebox/src/fs/nine_p/tests.rs new file mode 100644 index 000000000..197a0840f --- /dev/null +++ b/litebox/src/fs/nine_p/tests.rs @@ -0,0 +1,675 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +extern crate std; + +use std::io::{Read as _, Write as _}; +use std::net::{TcpListener, TcpStream}; +use std::path::Path; + +use crate::fs::errors::{ + FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, SeekError, + TruncateError, UnlinkError, WriteError, +}; +use crate::fs::{FileSystem as _, Mode, OFlags}; +use crate::platform::mock::MockPlatform; + +use super::transport; + +// --------------------------------------------------------------------------- +// Transport adapter: implement litebox 9P transport traits for TcpStream +// --------------------------------------------------------------------------- + +/// A wrapper around `TcpStream` that implements the litebox 9P transport traits. +struct TcpTransport { + stream: TcpStream, +} + +impl TcpTransport { + fn connect(addr: &str) -> Self { + let stream = TcpStream::connect(addr).expect("failed to connect to 9P server"); + Self { stream } + } +} + +impl transport::Read for TcpTransport { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.stream.read(buf).map_err(|_| transport::ReadError) + } +} + +impl transport::Write for TcpTransport { + fn write(&mut self, buf: &[u8]) -> Result { + self.stream.write(buf).map_err(|_| transport::WriteError) + } +} + +// --------------------------------------------------------------------------- +// diod server management +// --------------------------------------------------------------------------- + +/// Find a free TCP port by binding to port 0. +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() +} + +/// A running `diod` 9P server instance that exports a temporary directory. +struct DiodServer { + child: std::process::Child, + port: u16, + _export_dir: tempfile::TempDir, + export_path: std::path::PathBuf, +} + +impl DiodServer { + /// Start a new `diod` server exporting a fresh temporary directory. + fn start() -> Self { + let export_dir = tempfile::tempdir().expect("failed to create temp dir"); + let export_path = export_dir.path().to_path_buf(); + let port = find_free_port(); + + let child = std::process::Command::new("diod") + .args([ + "--foreground", + "--no-auth", + "--export", + export_dir.path().to_str().unwrap(), + "--listen", + &std::format!("127.0.0.1:{port}"), + "--nwthreads", + "1", + "-d", + "100000", + ]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .spawn() + .expect("failed to start diod – is it installed? (`apt install diod`)"); + + // Give the server a moment to start listening + std::thread::sleep(std::time::Duration::from_millis(500)); + + Self { + child, + port, + _export_dir: export_dir, + export_path, + } + } + + /// TCP address of the server (e.g., "127.0.0.1:12345"). + fn addr(&self) -> std::string::String { + std::format!("127.0.0.1:{}", self.port) + } + + /// Path to the exported directory on the host. + 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() { + 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: create a connected 9P filesystem +// --------------------------------------------------------------------------- + +fn connect_9p( + litebox: &crate::LiteBox, + server: &DiodServer, +) -> super::FileSystem { + 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") +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[test] +fn test_nine_p_create_and_read_file() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + let fs = connect_9p(&litebox, &server); + + // Create a file and write to it + let fd = fs + .open("/hello.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("failed to create file via 9P"); + + let data = b"Hello from litebox 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 the file exists on the 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 9P!"); + + // Read the file back through 9P + let fd = fs + .open("/hello.txt", OFlags::RDONLY, Mode::empty()) + .expect("failed to open file for reading via 9P"); + + let mut buf = alloc::vec![0u8; 256]; + let bytes_read = fs.read(&fd, &mut buf, None).expect("failed to read via 9P"); + assert_eq!(&buf[..bytes_read], data); + + fs.close(&fd).expect("failed to close file"); +} + +#[test] +fn test_nine_p_mkdir_and_readdir() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + let fs = connect_9p(&litebox, &server); + + // Create directories + fs.mkdir("/subdir", Mode::RWXU) + .expect("failed to mkdir via 9P"); + fs.mkdir("/subdir/nested", Mode::RWXU) + .expect("failed to mkdir nested via 9P"); + + // Create a file inside the subdirectory + let fd = fs + .open( + "/subdir/file.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("failed to create file in subdir"); + fs.write(&fd, b"nested content", None).unwrap(); + fs.close(&fd).unwrap(); + + // Read the root directory + let fd = fs + .open("/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .expect("failed to open root dir"); + let entries = fs.read_dir(&fd).expect("failed to readdir root"); + fs.close(&fd).unwrap(); + + let names: alloc::vec::Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); + assert!( + names.contains(&"subdir"), + "root should contain 'subdir', got: {names:?}" + ); + + // Read the subdirectory + let fd = fs + .open("/subdir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .expect("failed to open subdir"); + let entries = fs.read_dir(&fd).expect("failed to readdir subdir"); + fs.close(&fd).unwrap(); + + let names: alloc::vec::Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); + assert!( + names.contains(&"nested"), + "subdir should contain 'nested', got: {names:?}" + ); + assert!( + names.contains(&"file.txt"), + "subdir should contain 'file.txt', got: {names:?}" + ); +} + +#[test] +fn test_nine_p_unlink_and_rmdir() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + let fs = connect_9p(&litebox, &server); + + // Create a file, then delete it + let fd = fs + .open("/to_delete.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("failed to create file"); + fs.close(&fd).unwrap(); + + fs.unlink("/to_delete.txt") + .expect("failed to unlink file via 9P"); + + // Verify the file is gone + assert!( + fs.open("/to_delete.txt", OFlags::RDONLY, Mode::empty()) + .is_err(), + "file should no longer exist" + ); + + // Create a directory, then remove it + fs.mkdir("/to_remove", Mode::RWXU).expect("failed to mkdir"); + fs.rmdir("/to_remove").expect("failed to rmdir via 9P"); + + // Verify the directory is gone on the host + assert!( + !server.export_path().join("to_remove").exists(), + "directory should no longer exist on host" + ); +} + +#[test] +fn test_nine_p_file_status() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + let fs = connect_9p(&litebox, &server); + + // Create a file with known content + let fd = fs + .open( + "/status_test.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("failed to create file"); + let data = b"1234567890"; + fs.write(&fd, data, None).unwrap(); + fs.close(&fd).unwrap(); + + // Check file_status via path + let status = fs + .file_status("/status_test.txt") + .expect("failed to stat file"); + assert_eq!( + status.file_type, + crate::fs::FileType::RegularFile, + "should be a regular file" + ); + assert_eq!(status.size, 10, "file size should be 10 bytes"); + + // Check directory status + fs.mkdir("/stat_dir", Mode::RWXU).unwrap(); + let status = fs.file_status("/stat_dir").expect("failed to stat dir"); + assert_eq!( + status.file_type, + crate::fs::FileType::Directory, + "should be a directory" + ); +} + +#[test] +fn test_nine_p_seek_and_partial_read() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + let fs = connect_9p(&litebox, &server); + + // Write a file with known content + let fd = fs + .open("/seek_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("failed to create file"); + fs.write(&fd, b"ABCDEFGHIJ", None).unwrap(); + fs.close(&fd).unwrap(); + + // Open for reading and seek + let fd = fs + .open("/seek_test.txt", OFlags::RDONLY, Mode::empty()) + .expect("failed to open file for reading"); + + // Seek to offset 5 + let pos = fs + .seek(&fd, 5, crate::fs::SeekWhence::RelativeToBeginning) + .expect("failed to seek"); + assert_eq!(pos, 5); + + // Read from offset 5 → should get "FGHIJ" + let mut buf = alloc::vec![0u8; 10]; + let n = fs.read(&fd, &mut buf, None).expect("failed to read"); + assert_eq!(&buf[..n], b"FGHIJ"); + + fs.close(&fd).unwrap(); +} + +#[test] +fn test_nine_p_truncate() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + let fs = connect_9p(&litebox, &server); + + // Write a file + let fd = fs + .open("/trunc_test.txt", OFlags::CREAT | OFlags::RDWR, Mode::RWXU) + .expect("failed to create file"); + fs.write(&fd, b"Hello, World!", None).unwrap(); + + // Truncate to 5 bytes + fs.truncate(&fd, 5, true) + .expect("failed to truncate via 9P"); + fs.close(&fd).unwrap(); + + // Verify on host + let content = std::fs::read_to_string(server.export_path().join("trunc_test.txt")).unwrap(); + assert_eq!(content, "Hello"); +} + +#[test] +fn test_nine_p_host_files_visible() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + + // Pre-populate some 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(&litebox, &server); + + // Read file created on the host through 9P + let fd = fs + .open("/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( + "/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:?}" + ); +} + +// --------------------------------------------------------------------------- +// Broken-connection transport: wraps TcpTransport and breaks after N writes +// --------------------------------------------------------------------------- + +use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +/// A transport wrapper that allows a fixed number of write-message calls to +/// succeed, then fails all subsequent I/O. This simulates a connection that +/// breaks in the middle of a session. +/// +/// Reads are only failed once a write has actually been rejected, so the +/// response to the last successful write is still received. +struct BrokenTransport { + inner: TcpTransport, + /// Number of `write` calls remaining before the connection "breaks". + remaining_writes: AtomicUsize, + /// Set to `true` once a write has been rejected. + broken: AtomicBool, +} + +impl BrokenTransport { + /// Create a new `BrokenTransport` that allows `allowed_writes` successful + /// `write` calls before all I/O starts failing. + fn new(inner: TcpTransport, allowed_writes: usize) -> Self { + Self { + inner, + remaining_writes: AtomicUsize::new(allowed_writes), + broken: AtomicBool::new(false), + } + } +} + +impl transport::Read for BrokenTransport { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.broken.load(Ordering::SeqCst) { + return Err(transport::ReadError); + } + self.inner.read(buf) + } +} + +impl transport::Write for BrokenTransport { + fn write(&mut self, buf: &[u8]) -> Result { + if self.remaining_writes.load(Ordering::SeqCst) == 0 { + self.broken.store(true, Ordering::SeqCst); + return Err(transport::WriteError); + } + self.remaining_writes.fetch_sub(1, Ordering::SeqCst); + self.inner.write(buf) + } +} + +/// Helper: connect to a diod server and build a `FileSystem` backed by +/// `BrokenTransport` that will break after `allowed_writes` write calls. +/// +/// The version handshake and attach each consume one write, so +/// `allowed_writes` must be >= 2 for the filesystem to be constructed +/// successfully. Any FS operation after construction will consume one +/// additional write. +fn connect_9p_broken( + litebox: &crate::LiteBox, + server: &DiodServer, + allowed_writes: usize, +) -> super::FileSystem { + 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)") +} + +// --------------------------------------------------------------------------- +// Broken-connection failure tests +// --------------------------------------------------------------------------- + +/// Opening a file should fail with an I/O-class error when the connection +/// breaks after the filesystem has been attached. +#[test] +fn test_nine_p_broken_open() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + // 2 writes: version + attach. The next write (open's walk) will fail. + let fs = connect_9p_broken(&litebox, &server, 2); + + let result = fs.open("/anything.txt", OFlags::RDONLY, Mode::empty()); + assert!(matches!(result, Err(OpenError::Io))); +} + +/// Creating a file should fail when the connection is broken. +#[test] +fn test_nine_p_broken_create() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + let fs = connect_9p_broken(&litebox, &server, 2); + + let result = fs.open("/new.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU); + assert!(matches!(result, Err(OpenError::Io))); +} + +/// Reading from an fd obtained before the break should fail. +#[test] +fn test_nine_p_broken_read() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + + // Pre-create a file via normal connection + { + let fs = connect_9p(&litebox, &server); + let fd = fs + .open("/read_me.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .unwrap(); + fs.write(&fd, b"data", None).unwrap(); + fs.close(&fd).unwrap(); + } + + // 4 writes: version + attach + walk + lopen. Then read will fail. + let fs = connect_9p_broken(&litebox, &server, 4); + let fd = fs + .open("/read_me.txt", OFlags::RDONLY, Mode::empty()) + .expect("open should succeed before break"); + + let mut buf = alloc::vec![0u8; 64]; + let result = fs.read(&fd, &mut buf, None); + assert!(matches!(result, Err(ReadError::Io))); +} + +/// Writing to an fd obtained before the break should fail. +#[test] +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); + let fd = fs + .open("/write_me.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("create should succeed before break"); + + let result = fs.write(&fd, b"data", None); + assert!(matches!(result, Err(WriteError::Io))); +} + +/// mkdir should fail when the connection is broken. +#[test] +fn test_nine_p_broken_mkdir() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + let fs = connect_9p_broken(&litebox, &server, 2); + + let result = fs.mkdir("/broken_dir", Mode::RWXU); + assert!(matches!(result, Err(MkdirError::Io))); +} + +/// readdir should fail when the connection breaks during the directory read. +#[test] +fn test_nine_p_broken_readdir() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + + // 4 writes: version + attach + walk + lopen for the directory. + let fs = connect_9p_broken(&litebox, &server, 4); + let fd = fs + .open("/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .expect("open dir should succeed before break"); + + let result = fs.read_dir(&fd); + assert!(matches!(result, Err(ReadDirError::Io))); +} + +/// unlink should fail when the connection is broken. +#[test] +fn test_nine_p_broken_unlink() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + + // Pre-create a file + { + let fs = connect_9p(&litebox, &server); + let fd = fs + .open("/to_unlink.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .unwrap(); + fs.close(&fd).unwrap(); + } + + let fs = connect_9p_broken(&litebox, &server, 2); + let result = fs.unlink("/to_unlink.txt"); + assert!(matches!(result, Err(UnlinkError::Io))); +} + +/// rmdir should fail when the connection is broken. +#[test] +fn test_nine_p_broken_rmdir() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + + // Pre-create a directory + { + let fs = connect_9p(&litebox, &server); + fs.mkdir("/to_rmdir", Mode::RWXU).unwrap(); + } + + let fs = connect_9p_broken(&litebox, &server, 2); + let result = fs.rmdir("/to_rmdir"); + assert!(matches!(result, Err(RmdirError::Io))); +} + +/// file_status should fail when the connection is broken. +#[test] +fn test_nine_p_broken_file_status() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + let fs = connect_9p_broken(&litebox, &server, 2); + + let result = fs.file_status("/"); + assert!(matches!(result, Err(FileStatusError::Io))); +} + +/// truncate should fail when the connection breaks after open. +#[test] +fn test_nine_p_broken_truncate() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + + // Pre-create a file + { + let fs = connect_9p(&litebox, &server); + let fd = fs + .open("/to_trunc.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .unwrap(); + fs.write(&fd, b"some data", None).unwrap(); + fs.close(&fd).unwrap(); + } + + // 4 writes: version + attach + walk + lopen. Then truncate will fail. + let fs = connect_9p_broken(&litebox, &server, 4); + let fd = fs + .open("/to_trunc.txt", OFlags::RDWR, Mode::empty()) + .expect("open should succeed before break"); + + let result = fs.truncate(&fd, 0, true); + assert!(matches!(result, Err(TruncateError::Io))); +} + +/// seek (RelativeToEnd, which requires a getattr) should fail when broken. +#[test] +fn test_nine_p_broken_seek() { + let litebox = crate::LiteBox::new(MockPlatform::new()); + let server = DiodServer::start(); + + // Pre-create a file + { + let fs = connect_9p(&litebox, &server); + let fd = fs + .open("/to_seek.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .unwrap(); + fs.write(&fd, b"data", None).unwrap(); + fs.close(&fd).unwrap(); + } + + // 4 writes: version + attach + walk + lopen. Then the getattr for seek will fail. + let fs = connect_9p_broken(&litebox, &server, 4); + let fd = fs + .open("/to_seek.txt", OFlags::RDONLY, Mode::empty()) + .expect("open should succeed before break"); + + let result = fs.seek(&fd, -1, crate::fs::SeekWhence::RelativeToEnd); + assert!(matches!(result, Err(SeekError::Io))); +} diff --git a/litebox/src/fs/nine_p/transport.rs b/litebox/src/fs/nine_p/transport.rs new file mode 100644 index 000000000..acef56bf1 --- /dev/null +++ b/litebox/src/fs/nine_p/transport.rs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! 9P transport layer abstraction +//! +//! This module defines traits for reading and writing 9P protocol messages over +//! an underlying transport (e.g., TCP socket, virtio-9p, etc.). + +use alloc::vec::Vec; + +pub struct ReadError; +pub struct WriteError; + +/// Trait for reading bytes from a transport +pub trait Read { + /// Read bytes into the buffer + /// + /// Returns the number of bytes read + fn read(&mut self, buf: &mut [u8]) -> Result; + + /// Read exactly `buf.len()` bytes into the buffer + fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), ReadError> { + let mut total_read = 0; + while total_read < buf.len() { + let n = self.read(&mut buf[total_read..])?; + if n == 0 { + return Err(ReadError); + } + total_read += n; + } + Ok(()) + } +} + +/// Trait for writing bytes to a transport +pub trait Write { + /// Write bytes from the buffer + /// + /// Returns the number of bytes written + fn write(&mut self, buf: &[u8]) -> Result; + + /// Write all bytes from the buffer + fn write_all(&mut self, buf: &[u8]) -> Result<(), WriteError> { + let mut total_written = 0; + while total_written < buf.len() { + let n = self.write(&buf[total_written..])?; + if n == 0 { + return Err(WriteError); + } + total_written += n; + } + Ok(()) + } +} + +/// Write a 9P message to a transport +pub(super) fn write_message( + w: &mut W, + buf: &mut Vec, + fcall: super::fcall::TaggedFcall<'_>, +) -> Result<(), WriteError> { + fcall.encode_to_buf(buf)?; + w.write_all(&buf[..]) +} + +/// Read a 9P message size header (4 bytes) and then the full message +pub(super) fn read_to_buf(r: &mut R, buf: &mut Vec) -> Result<(), super::Error> { + buf.resize(4, 0); + r.read_exact(&mut buf[..]).map_err(|_| super::Error::Io)?; + let sz = u32::from_le_bytes(buf[..4].try_into().unwrap()) as usize; + if sz < 7 { + // Minimum message size: size(4) + type(1) + tag(2) + return Err(super::Error::InvalidResponse); + } + if sz > buf.capacity() { + buf.reserve(sz - buf.len()); + } + buf.resize(sz, 0); + r.read_exact(&mut buf[4..]).map_err(|_| super::Error::Io) +} + +/// Read a 9P message from a transport +pub(super) fn read_message<'a, R: Read>( + r: &mut R, + buf: &'a mut Vec, +) -> Result, super::Error> { + read_to_buf(r, buf)?; + super::fcall::TaggedFcall::decode(&buf[..]) +} + +impl Write for &mut [u8] { + fn write(&mut self, buf: &[u8]) -> Result { + let amt = self.len().min(buf.len()); + let (a, b) = core::mem::take(self).split_at_mut(amt); + a.copy_from_slice(&buf[..amt]); + *self = b; + Ok(amt) + } +} + +impl Write for Vec { + fn write(&mut self, buf: &[u8]) -> Result { + self.extend_from_slice(buf); + Ok(buf.len()) + } +} diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index ef025206d..15109c374 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -131,6 +131,7 @@ impl From for Errno { litebox::fs::errors::OpenError::PathError(path_error) => path_error.into(), litebox::fs::errors::OpenError::ReadOnlyFileSystem => Errno::EROFS, litebox::fs::errors::OpenError::AlreadyExists => Errno::EEXIST, + litebox::fs::errors::OpenError::Io => Errno::EIO, _ => unimplemented!(), } } @@ -142,6 +143,7 @@ impl From for Errno { litebox::fs::errors::UnlinkError::NoWritePerms => Errno::EACCES, litebox::fs::errors::UnlinkError::IsADirectory => Errno::EISDIR, litebox::fs::errors::UnlinkError::ReadOnlyFileSystem => Errno::EROFS, + litebox::fs::errors::UnlinkError::Io => Errno::EIO, litebox::fs::errors::UnlinkError::PathError(path_error) => path_error.into(), _ => unimplemented!(), } @@ -156,6 +158,7 @@ impl From for Errno { litebox::fs::errors::RmdirError::NotEmpty => Errno::ENOTEMPTY, litebox::fs::errors::RmdirError::NotADirectory => Errno::ENOTDIR, litebox::fs::errors::RmdirError::ReadOnlyFileSystem => Errno::EROFS, + litebox::fs::errors::RmdirError::Io => Errno::EIO, litebox::fs::errors::RmdirError::PathError(path_error) => path_error.into(), _ => unimplemented!(), } @@ -185,6 +188,7 @@ impl From for Errno { match value { litebox::fs::errors::ReadError::NotAFile => Errno::EISDIR, litebox::fs::errors::ReadError::NotForReading => Errno::EACCES, + litebox::fs::errors::ReadError::Io => Errno::EIO, _ => unimplemented!(), } } @@ -195,6 +199,7 @@ impl From for Errno { match value { litebox::fs::errors::WriteError::NotAFile => Errno::EISDIR, litebox::fs::errors::WriteError::NotForWriting => Errno::EACCES, + litebox::fs::errors::WriteError::Io => Errno::EIO, _ => unimplemented!(), } } @@ -208,6 +213,7 @@ impl From for Errno { } litebox::fs::errors::SeekError::InvalidOffset => Errno::EINVAL, litebox::fs::errors::SeekError::NonSeekable => Errno::ESPIPE, + litebox::fs::errors::SeekError::Io => Errno::EIO, _ => unimplemented!(), } } @@ -220,6 +226,7 @@ impl From for Errno { litebox::fs::errors::MkdirError::AlreadyExists => Errno::EEXIST, litebox::fs::errors::MkdirError::ReadOnlyFileSystem => Errno::EROFS, litebox::fs::errors::MkdirError::NoWritePerms => Errno::EACCES, + litebox::fs::errors::MkdirError::Io => Errno::EIO, _ => unimplemented!(), } } @@ -555,6 +562,7 @@ impl From for Errno { litebox::fs::errors::TruncateError::NotForWriting => Errno::EACCES, litebox::fs::errors::TruncateError::IsTerminalDevice => Errno::EINVAL, litebox::fs::errors::TruncateError::ClosedFd => Errno::EBADF, + litebox::fs::errors::TruncateError::Io => Errno::EIO, } } }