From 3fbddd5b4e5cf7ac1c2ffb97b3f2ae1e457674e9 Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Sun, 23 Feb 2025 09:36:59 +0000 Subject: [PATCH 01/12] wip: investigating having a sansio API for ninep --- crates/ninep/src/lib.rs | 1 + crates/ninep/src/sansio.rs | 1453 ++++++++++++++++++++++++++++++++++++ 2 files changed, 1454 insertions(+) create mode 100644 crates/ninep/src/sansio.rs diff --git a/crates/ninep/src/lib.rs b/crates/ninep/src/lib.rs index bc0228f..4bbc28f 100644 --- a/crates/ninep/src/lib.rs +++ b/crates/ninep/src/lib.rs @@ -20,6 +20,7 @@ use std::{ pub mod client; pub mod fs; pub mod protocol; +pub mod sansio; pub mod server; use protocol::{Format9p, Rdata, Rmessage}; diff --git a/crates/ninep/src/sansio.rs b/crates/ninep/src/sansio.rs new file mode 100644 index 0000000..ac41e68 --- /dev/null +++ b/crates/ninep/src/sansio.rs @@ -0,0 +1,1453 @@ +//! Sans-io 9p protocol implementation +//! +//! http://man.cat-v.org/plan_9/5/ +use std::{ + fmt, + io::{self, ErrorKind, Read}, + mem::size_of, +}; + +/// The size of variable length data is denoted using a u16 so anything longer +/// than u16::MAX is not something we can handle. +pub const MAX_SIZE_FIELD: usize = u16::MAX as usize; +/// For data fields in read/write messages the size field is 32bits not 16 +pub const MAX_DATA_SIZE_FIELD: usize = u32::MAX as usize; +/// The maximum number of bytes we allow in a Data buffer: a client attempting +/// to use more than this is an error. +pub const MAX_DATA_LEN: usize = 32 * 1024 * 1024; + +/// Non IO releated errors that can occur when attempting to serialize a [NineP] type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WriteError { + /// The maximum number of bytes we allow in a Data buffer is [MAX_DATA_LEN]: a client + /// attempting to use more than this is an error. + DataLength(usize), + /// The size of variable length data is denoted using a u16 so anything longer + /// than u16::MAX is not something we can handle. + FieldLength(usize), +} + +impl fmt::Display for WriteError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DataLength(n_bytes) => write!( + f, + "data field too long: max={MAX_DATA_SIZE_FIELD} len={n_bytes}" + ), + Self::FieldLength(len) => write!(f, "string too long: max={MAX_SIZE_FIELD} len={len}"), + } + } +} + +/// Something that can be encoded to and decoded 9p protocol messages. +/// +/// From [INTRO(5)](http://man.cat-v.org/plan_9/5/intro): +/// Each message consists of a sequence of bytes. Two-, four-, and eight-byte fields hold +/// unsigned integers represented in little-endian order (least significant byte first). +pub trait NineP: Sized { + /// The [Read9p] implementation used to decode an instance of this type from a bytestream + type Reader: Read9p; + + /// Number of bytes required to encode + fn n_bytes(&self) -> usize; + + /// Encode self as bytes for the 9p protocol into a given buffer which the caller must + /// ensure is sized to be at least [NineP::n_bytes]. + fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError>; + + /// Construct a new reader for parsing this type from a source of bytes + fn reader() -> Self::Reader; +} + +/// A paired helper type for decoding a [Format9p] type from a bytestream. +pub trait Read9p: Sized { + /// The parent [Format9p] type being decoded into + type T: NineP; + + /// The number of bytes that need to be passed to the next call to [Read9p::accept_bytes]. + fn needs_bytes(&self) -> usize; + + /// Accept the requested number of bytes and return a new [NinepReader] state machine. + fn accept_bytes(self, bytes: &[u8]) -> io::Result>; +} + +#[inline] +fn accept_bytes_final(r: R, bytes: &[u8]) -> io::Result { + match r.accept_bytes(bytes)? { + NinepReader::Complete(t) => Ok(t), + NinepReader::Pending(_) => unreachable!(), + } +} + +/// Attempt to read a [NineP] value from a byte buffer that contains sufficient data without +/// requiring further IO. +pub fn try_read_9p_bytes(mut bytes: &[u8]) -> io::Result { + let mut nr = NinepReader::Pending(T::reader()); + + loop { + match nr { + NinepReader::Pending(r) => { + let n = T::Reader::needs_bytes(&r); + nr = r.accept_bytes(&bytes[0..n])?; + bytes = &bytes[n..]; + } + + NinepReader::Complete(t) => return Ok(t), + } + } +} + +/// Serialize `t` into a byte buffer ready for transmission. +pub fn write_9p_bytes(t: &T) -> Result, WriteError> { + let mut buf = vec![0; t.n_bytes()]; + t.write_bytes(&mut buf)?; + + Ok(buf) +} + +/// State machine enum for writing concrete readers from a [Format9p] type. +#[derive(Debug)] +pub enum NinepReader +where + T: NineP, +{ + /// A reader that requires more input to complete + Pending(T::Reader), + /// A reader that completed successfully + Complete(T), +} + +// Unsigned integer types can all be treated the same way so we stamp them out using a macro. +// They are written and read in their little-endian byte form. +macro_rules! impl_u { + ($($ty:ty),+) => { + $( + impl NineP for $ty { + type Reader = $ty; + + fn n_bytes(&self) -> usize { + size_of::<$ty>() + } + + fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError> { + buf[0..size_of::<$ty>()].copy_from_slice(&self.to_le_bytes()); + Ok(()) + } + + fn reader() -> $ty { + 0 + } + } + + impl Read9p for $ty { + type T = $ty; + + fn needs_bytes(&self) -> usize { + size_of::<$ty>() + } + + fn accept_bytes(self, bytes: &[u8]) -> io::Result> { + let buf = bytes[0..size_of::<$ty>()].try_into().unwrap(); + + Ok(NinepReader::Complete(<$ty>::from_le_bytes(buf))) + } + } + )+ + }; +} + +impl_u!(u8, u16, u32, u64); + +// [size: u16] [content as bytes...] +// +// From [INTRO(5)](http://man.cat-v.org/plan_9/5/intro): +// Data items of larger or variable lengths are represented by a two-byte field specifying +// a count, n, followed by n bytes of data. Text strings are represented this way, with +// the text itself stored as a UTF-8 encoded sequence of Unicode charac- ters (see utf(6)). +// +// Text strings in 9P messages are not NUL- terminated: n counts the bytes of UTF-8 data, +// which include no final zero byte. The NUL character is illegal in all text strings +// in 9P, and is therefore excluded from file names, user names, and so on. +impl NineP for String { + type Reader = StringReader; + + fn n_bytes(&self) -> usize { + size_of::() + self.len() + } + + fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError> { + let len = self.len(); + if len > MAX_SIZE_FIELD { + return Err(WriteError::FieldLength(len)); + } + + (len as u16).write_bytes(&mut buf[0..2])?; + buf[2..len + 2].copy_from_slice(self.as_bytes()); + + Ok(()) + } + + fn reader() -> StringReader { + StringReader::Start + } +} + +#[derive(Debug)] +#[allow(missing_docs)] +pub enum StringReader { + Start, + WithLen(usize), +} + +impl Read9p for StringReader { + type T = String; + + fn needs_bytes(&self) -> usize { + match self { + Self::Start => size_of::(), + Self::WithLen(len) => *len, + } + } + + fn accept_bytes(self, mut bytes: &[u8]) -> io::Result> { + match self { + Self::Start => { + let len = accept_bytes_final(0u16, bytes)? as usize; + Ok(NinepReader::Pending(Self::WithLen(len))) + } + + Self::WithLen(len) => { + let mut s = String::with_capacity(len); + bytes.read_to_string(&mut s)?; + let actual = s.len(); + + if actual < len { + return Err(io::Error::new( + ErrorKind::UnexpectedEof, + format!("unexpected end of string: wanted {len}, got {actual}"), + )); + } + + Ok(NinepReader::Complete(s)) + } + } + } +} + +// [size: u16] [content as bytes...] +// +// From [INTRO(5)](http://man.cat-v.org/plan_9/5/intro): +// Data items of larger or variable lengths are represented by a two-byte field specifying +// a count, n, followed by n bytes of data. +impl NineP for Vec { + type Reader = VecReader; + + fn n_bytes(&self) -> usize { + size_of::() + self.iter().map(|t| t.n_bytes()).sum::() + } + + fn write_bytes(&self, mut buf: &mut [u8]) -> Result<(), WriteError> { + let n_bytes = self.iter().map(|t| t.n_bytes()).sum::(); + if n_bytes > MAX_SIZE_FIELD { + return Err(WriteError::FieldLength(n_bytes)); + } + + (self.len() as u16).write_bytes(&mut buf[0..2])?; + buf = &mut buf[2..]; + for t in self { + let n = t.n_bytes(); + t.write_bytes(buf)?; + buf = &mut buf[n..]; + } + + Ok(()) + } + + fn reader() -> VecReader { + VecReader::Start + } +} + +#[derive(Debug)] +#[allow(missing_docs)] +pub enum VecReader +where + T: NineP, +{ + Start, + Reading(usize, T::Reader, Vec), +} + +impl Read9p for VecReader { + type T = Vec; + + fn needs_bytes(&self) -> usize { + match self { + Self::Start => size_of::(), + Self::Reading(_, r, _) => r.needs_bytes(), + } + } + + fn accept_bytes(self, bytes: &[u8]) -> io::Result>> { + match self { + Self::Start => { + let len = accept_bytes_final(0u16, bytes)? as usize; + let buf = Vec::with_capacity(len); + let r = T::reader(); + + Ok(NinepReader::Pending(VecReader::Reading(len, r, buf))) + } + + Self::Reading(n, r, mut buf) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(VecReader::Reading(n, r, buf))), + + NinepReader::Complete(t) => { + buf.push(t); + if n == 1 { + Ok(NinepReader::Complete(buf)) + } else { + let r = T::reader(); + Ok(NinepReader::Pending(VecReader::Reading(n - 1, r, buf))) + } + } + }, + } + } +} + +/// A wrapper around a Vec for handling data fields in read/write messages +/// ```text +/// READ(5) +/// NAME +/// read, write - transfer data from and to a file +/// +/// SYNOPSIS +/// size[4] Tread tag[2] fid[4] offset[8] count[4] +/// size[4] Rread tag[2] count[4] data[count] +/// +/// size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] +/// size[4] Rwrite tag[2] count[4] +/// ``` +#[derive(Clone, PartialEq, Eq)] +pub struct Data(pub(super) Vec); + +impl fmt::Debug for Data { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Data(n_bytes={})", self.0.len()) + } +} + +impl From> for Data { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl TryFrom for Vec { + type Error = io::Error; + + fn try_from(Data(bytes): Data) -> Result { + let mut buf = Vec::new(); + let mut bytes = bytes.as_slice(); + let n = size_of::(); + + loop { + match try_read_9p_bytes::(bytes) { + Ok(rs) => { + buf.push(rs); + bytes = &bytes[n..]; + } + Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, + Err(e) => return Err(e), + } + } + + Ok(buf) + } +} + +impl NineP for Data { + type Reader = DataReader; + + fn n_bytes(&self) -> usize { + size_of::() + self.0.len() + } + + fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError> { + let n_bytes = self.0.len(); + if n_bytes > MAX_DATA_SIZE_FIELD { + return Err(WriteError::DataLength(n_bytes)); + } + + (n_bytes as u32).write_bytes(&mut buf[0..4])?; + buf[4..].copy_from_slice(&self.0); + + Ok(()) + } + + fn reader() -> DataReader { + DataReader::Start + } +} + +#[derive(Debug)] +#[allow(missing_docs)] +pub enum DataReader { + Start, + WithLen(usize), +} + +impl Read9p for DataReader { + type T = Data; + + fn needs_bytes(&self) -> usize { + match self { + Self::Start => size_of::(), + Self::WithLen(len) => *len, + } + } + + fn accept_bytes(self, bytes: &[u8]) -> io::Result> { + match self { + DataReader::Start => { + let len = accept_bytes_final(0u32, bytes)? as usize; + if len > MAX_DATA_LEN { + return Err(io::Error::new( + ErrorKind::InvalidData, + format!("data field too long: max={MAX_DATA_LEN} len={len}"), + )); + } + + Ok(NinepReader::Pending(DataReader::WithLen(len))) + } + + DataReader::WithLen(len) => { + let actual = bytes.len(); + if actual < len { + return Err(io::Error::new( + ErrorKind::UnexpectedEof, + format!("unexpected end of data: wanted {len}, got {actual}"), + )); + } + + Ok(NinepReader::Complete(Data(bytes.to_vec()))) + } + } + } +} + +/// A machine-independent directory entry +/// http://man.cat-v.org/plan_9/5/stat +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawStat { + /// size[2] total byte count of the following data + size: u16, + /// type[2] for kernel use + ty: u16, + /// dev[4] for kernel use + dev: u32, + /// Qid type, version and path + qid: Qid, + /// mode[4] permissions and flags + mode: u32, + /// atime[4] last access time + atime: u32, + /// mtime[4] last modification time + mtime: u32, + /// length[8] length of file in bytes + length: u64, + /// name[ s ] file name; must be / if the file is the root directory of the server + name: String, + /// uid[ s ] owner name + uid: String, + /// gid[ s ] group name + gid: String, + /// muid[ s ] name of the user who last modified the file + muid: String, +} + +macro_rules! write_fields { + ($buf:expr, $self:expr, $($field:ident),+) => { + #[allow(unused_assignments)] + { + $( + let len = $self.$field.n_bytes(); + $self.$field.write_bytes(&mut $buf[0..len])?; + $buf = &mut $buf[len..]; + )+ + Ok(()) + } + + }; +} + +impl NineP for RawStat { + type Reader = RawStatReader; + + fn n_bytes(&self) -> usize { + // 2 2 4 13 4 4 4 8 -> 41 + 41 + self.name.n_bytes() + self.uid.n_bytes() + self.gid.n_bytes() + self.muid.n_bytes() + } + + fn write_bytes(&self, mut buf: &mut [u8]) -> Result<(), WriteError> { + write_fields!( + buf, self, size, ty, dev, qid, mode, atime, mtime, length, name, uid, gid, muid + ) + } + + fn reader() -> RawStatReader { + RawStatReader::Start + } +} + +#[derive(Debug)] +#[allow(missing_docs)] +pub enum RawStatReader { + Start, + Name(RawStat, StringReader), + Uid(RawStat, StringReader), + Gid(RawStat, StringReader), + Muid(RawStat, StringReader), +} + +impl Read9p for RawStatReader { + type T = RawStat; + + fn needs_bytes(&self) -> usize { + match self { + Self::Start => 41, + Self::Name(_, r) => r.needs_bytes(), + Self::Uid(_, r) => r.needs_bytes(), + Self::Gid(_, r) => r.needs_bytes(), + Self::Muid(_, r) => r.needs_bytes(), + } + } + + fn accept_bytes(self, bytes: &[u8]) -> io::Result> { + match self { + Self::Start => { + let size = accept_bytes_final(0u16, bytes)?; + let ty = accept_bytes_final(0u16, &bytes[2..])?; + let dev = accept_bytes_final(0u32, &bytes[4..])?; + let qid: Qid = try_read_9p_bytes(&bytes[8..])?; + let mode = accept_bytes_final(0u32, &bytes[21..])?; + let atime = accept_bytes_final(0u32, &bytes[25..])?; + let mtime = accept_bytes_final(0u32, &bytes[29..])?; + let length = accept_bytes_final(0u64, &bytes[33..])?; + let rs = RawStat { + size, + ty, + dev, + qid, + mode, + atime, + mtime, + length, + name: String::default(), + uid: String::default(), + gid: String::default(), + muid: String::default(), + }; + + Ok(NinepReader::Pending(Self::Name(rs, StringReader::Start))) + } + + Self::Name(mut rs, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::Name(rs, r))), + NinepReader::Complete(s) => { + rs.name = s; + Ok(NinepReader::Pending(Self::Uid(rs, StringReader::Start))) + } + }, + + Self::Uid(mut rs, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::Uid(rs, r))), + NinepReader::Complete(s) => { + rs.uid = s; + Ok(NinepReader::Pending(Self::Gid(rs, StringReader::Start))) + } + }, + + Self::Gid(mut rs, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::Gid(rs, r))), + NinepReader::Complete(s) => { + rs.gid = s; + Ok(NinepReader::Pending(Self::Muid(rs, StringReader::Start))) + } + }, + + Self::Muid(mut rs, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::Muid(rs, r))), + NinepReader::Complete(s) => { + rs.muid = s; + Ok(NinepReader::Complete(rs)) + } + }, + } + } +} + +/// Helper for defining a struct that implements Format9p by just serialising their +/// fields directly without a size field. +macro_rules! impl_message_datatype { + ( + $reader:ident; + $(#[$docs:meta])+ + struct $struct:ident { + $( + $(#[$field_docs:meta])* + $field:ident: $ty:ty, + )* + } + ) => { + $(#[$docs])+ + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct $struct { + $( + $(#[$field_docs])* + pub $field: $ty, + )* + } + impl_message_datatype!(@tuple $struct $reader $($field: $ty),*); + }; + + // No fields + (@tuple $struct:ident $reader:ident) => { + impl NineP for $struct { + type Reader = $reader; + fn n_bytes(&self) -> usize { 0 } + fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError> { Ok(()) } + fn reader() -> $reader { $reader($ty::reader()) } + } + + #[derive(Debug)] + #[allow(missing_docs)] + pub struct $reader; + impl Read9p for $reader { + type T = $struct; + fn needs_bytes(&self) -> usize { 0 } + fn accept_bytes(self, bytes: &[u8]) -> io::Result> { + debug_assert!(bytes.is_empty()); + Ok(NinepReader::Complete($struct { })) + } + } + }; + + // Single field + (@tuple $struct:ident $reader:ident $field:ident: $ty:ty) => { + impl NineP for $struct { + type Reader = $reader; + fn n_bytes(&self) -> usize { self.$field.n_bytes() } + fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError> { self.$field.write_bytes(buf) } + fn reader() -> $reader { $reader(<$ty as NineP>::reader()) } + } + + #[derive(Debug)] + #[allow(missing_docs)] + pub struct $reader($ty::Reader); + impl Read9p for $reader { + type T = $struct; + fn needs_bytes(&self) -> usize { self.0.needs_bytes() } + fn accept_bytes(self, bytes: &[u8]) -> io::Result> { + match self.0.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self(r))), + NinepReader::Complete($field) => Ok(NinepReader::Complete($struct { $field })) + } + } + } + }; + + // Two fields + (@tuple $struct:ident $reader:ident $f1:ident: $t1:ty, $f2:ident: $t2:ty) => { + impl NineP for $struct { + type Reader = $reader; + fn n_bytes(&self) -> usize { + self.$f1.n_bytes() + self.$f2.n_bytes() + } + fn write_bytes(&self, mut buf: &mut [u8]) -> Result<(), WriteError> { + write_fields!(buf, self, $f1, $f2) + } + fn reader() -> $reader { $reader::T1(<$t1 as NineP>::reader()) } + } + + #[derive(Debug)] + #[allow(missing_docs)] + pub enum $reader { + T1($t1::Reader), + T2($t1, $t2::Reader), + } + impl Read9p for $reader { + type T = $struct; + fn needs_bytes(&self) -> usize { + match self { + Self::T1(r) => r.needs_bytes(), + Self::T2(_, r) => r.needs_bytes(), + } + } + fn accept_bytes(self, bytes: &[u8]) -> io::Result { + match self { + Self::T1(r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T1(r))), + NinepReader::Complete(t1) => Ok(NinepReader::Pending(Self::T2(t1, <$t2 as NineP>::reader()))), + }, + Self::T2($f1, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T2($f1, r))), + NinepReader::Complete($f2) => Ok(NinepReader::Complete($struct { $f1, $f2 })), + }, + } + } + } + }; + + // Three fields + (@tuple $struct:ident $reader:ident $f1:ident: $t1:ty, $f2:ident: $t2:ty, $f3:ident: $t3:ty) => { + impl NineP for $struct { + type Reader = $reader; + fn n_bytes(&self) -> usize { + self.$f1.n_bytes() + self.$f2.n_bytes() + self.$f3.n_bytes() + } + fn write_bytes(&self, mut buf: &mut [u8]) -> Result<(), WriteError> { + write_fields!(buf, self, $f1, $f2, $f3) + } + fn reader() -> $reader { $reader::T1(<$t1 as NineP>::reader()) } + } + + #[derive(Debug)] + #[allow(missing_docs)] + pub enum $reader { + T1(<$t1 as NineP>::Reader), + T2($t1, <$t2 as NineP>::Reader), + T3($t1, $t2, <$t3 as NineP>::Reader), + } + impl Read9p for $reader { + type T = $struct; + fn needs_bytes(&self) -> usize { + match self { + Self::T1(r) => r.needs_bytes(), + Self::T2(_, r) => r.needs_bytes(), + Self::T3(_, _, r) => r.needs_bytes(), + } + } + fn accept_bytes(self, bytes: &[u8]) -> io::Result> { + match self { + Self::T1(r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T1(r))), + NinepReader::Complete(t1) => Ok(NinepReader::Pending(Self::T2(t1, <$t2 as NineP>::reader()))), + }, + Self::T2(t1, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T2(t1, r))), + NinepReader::Complete(t2) => { + Ok(NinepReader::Pending(Self::T3(t1, t2, <$t3 as NineP>::reader()))) + } + }, + Self::T3($f1, $f2, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T3($f1, $f2, r))), + NinepReader::Complete($f3) => Ok(NinepReader::Complete($struct { $f1, $f2, $f3 })), + }, + } + } + } + }; + + // Four fields + (@tuple $struct:ident $reader:ident $f1:ident: $t1:ty, $f2:ident: $t2:ty, $f3:ident: $t3:ty, $f4:ident: $t4:ty) => { + impl NineP for $struct { + type Reader = $reader; + fn n_bytes(&self) -> usize { + self.$f1.n_bytes() + self.$f2.n_bytes() + self.$f3.n_bytes() + self.$f4.n_bytes() + } + fn write_bytes(&self, mut buf: &mut [u8]) -> Result<(), WriteError> { + write_fields!(buf, self, $f1, $f2, $f3, $f4) + } + fn reader() -> $reader { $reader::T1(<$t1 as NineP>::reader()) } + } + + #[derive(Debug)] + #[allow(missing_docs)] + pub enum $reader { + T1(T1::Reader), + T2(T1, T2::Reader), + T3(T1, T2, T3::Reader), + T4(T1, T2, T3, T4::Reader), + } + impl Read9p for $reader { + type T = $struct; + fn needs_bytes(&self) -> usize { + match self { + Self::T1(r) => r.needs_bytes(), + Self::T2(_, r) => r.needs_bytes(), + Self::T3(_, _, r) => r.needs_bytes(), + Self::T4(_, _, _, r) => r.needs_bytes(), + } + } + fn accept_bytes(self, bytes: &[u8]) -> io::Result> { + match self { + Self::T1(r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T1(r))), + NinepReader::Complete(t1) => Ok(NinepReader::Pending(Self::T2(t1, <$t2 as NineP>::reader()))), + }, + Self::T2(t1, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T2(t1, r))), + NinepReader::Complete(t2) => { + Ok(NinepReader::Pending(Self::T3(t1, t2, <$t3 as NineP>::reader()))) + } + }, + Self::T3(t1, t2, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T3(t1, t2, r))), + NinepReader::Complete(t3) => { + Ok(NinepReader::Pending(Self::T4(t1, t2, t3, <$t4 as NineP>::reader()))) + } + }, + Self::T4($f1, $f2, $f3, r) => match r.accept_bytes(bytes)? { + NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T4($f1, $f2, $f3, r))), + NinepReader::Complete($f4) => Ok(NinepReader::Complete($struct { $f1, $f2, $f3, $f4 })), + }, + } + } + } + }; +} + +impl_message_datatype!( + QidReader; + + /// A qid represents the server's unique identification for the file being accessed: two files + /// on the same server hierarchy are the same if and only if their qids are the same. + #[derive(Copy)] + struct Qid { + /// qid.type[1] + /// the type of the file (directory, etc.), repre- sented as a bit vector corresponding to the + /// high 8 bits of the file's mode word. + ty: u8, + /// qid.vers[4] version number for given path + version: u32, + /// qid.path[8] the file server's unique identification for the file + path: u64, + } +); + +/// Taken from the enum in fcall.h in the plan9 source. +/// https://github.com/9fans/plan9port/blob/master/include/fcall.h#L80 +/// +/// This is just used internally to help with defining the encode / decode behaviour +/// of the various message types. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct MessageType(u8); + +#[allow(non_upper_case_globals)] +impl MessageType { + const Tversion: Self = Self(100); + const Rversion: Self = Self(101); + + const Tauth: Self = Self(102); + const Rauth: Self = Self(103); + + const Tattach: Self = Self(104); + const Rattach: Self = Self(105); + + // Terror = 106, + const Rerror: Self = Self(107); + + const Tflush: Self = Self(108); + const Rflush: Self = Self(109); + + const Twalk: Self = Self(110); + const Rwalk: Self = Self(111); + + const Topen: Self = Self(112); + const Ropen: Self = Self(113); + + const Tcreate: Self = Self(114); + const Rcreate: Self = Self(115); + + const Tread: Self = Self(116); + const Rread: Self = Self(117); + + const Twrite: Self = Self(118); + const Rwrite: Self = Self(119); + + const Tclunk: Self = Self(120); + const Rclunk: Self = Self(121); + + const Tremove: Self = Self(122); + const Rremove: Self = Self(123); + + const Tstat: Self = Self(124); + const Rstat: Self = Self(125); + + const Twstat: Self = Self(126); + const Rwstat: Self = Self(127); + // Tmax = 128, + // Topenfd = 98, + // Ropenfd = 99, +} + +/// Helper for implementing Tmessage and Rmessage +macro_rules! impl_message_format { + ( + $message_ty:ident, $reader:ident, $enum_ty:ident, $err:expr; + $($enum_variant:ident => $message_variant:ident { + $($field:ident: $ty:ty,)* + })+ + ) => { + impl NineP for $message_ty { + type Reader = $reader; + + fn n_bytes(&self) -> usize { + let content_size = match &self.content { + $( + $enum_ty::$enum_variant { $($field,)* } => { + #[allow(unused_mut)] + let mut n = 0; + $(n += $field.n_bytes();)* + n + } + )+ + }; + + // size[4] type[1] tag[2] | content[...] + 4 + 1 + 2 + content_size + } + + #[allow(unused_assignments)] + fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError> { + let ty = match self.content { + $($enum_ty::$enum_variant { .. } => MessageType::$message_variant.0,)+ + }; + + (self.n_bytes() as u32).write_bytes(buf)?; // 4 + ty.write_bytes(&mut buf[4..])?; // 1 + self.tag.write_bytes(&mut buf[5..])?; // 2 + let mut offset = 7; + + match &self.content { + $( + $enum_ty::$enum_variant { $($field,)* } => { + $( + $field.write_bytes(&mut buf[offset..])?; + offset += $field.n_bytes(); + )* + }, + )+ + } + + Ok(()) + } + + fn reader() -> $reader { + $reader::Start + } + } + +// pub trait Read9p: Sized { +// type T: NineP; +// fn needs_bytes(&self) -> usize; +// fn accept_bytes(self, bytes: &[u8]) -> io::Result>; +// } + + #[derive(Debug)] + #[allow(missing_docs)] + pub enum $reader { + Start, + WithSize(usize), + } + + impl Read9p for $reader { + type T = $message_ty; + + fn needs_bytes(&self) -> usize { + match self { + Self::Start => size_of::(), + // the size field includes the number of bytes for the field itself so we + // trim that off before decoding the rest of the message + Self::WithSize(n) => n - 4, + } + } + + #[allow(unused_assignments)] + fn accept_bytes(self, bytes: &[u8]) -> io::Result> { + match self { + Self::Start => { + let size = accept_bytes_final(0u32, bytes)? as usize; + Ok(NinepReader::Pending($reader::WithSize(size))) + } + Self::WithSize(_) => { + let ty = accept_bytes_final(0u8, bytes)?; + let tag = accept_bytes_final(0u16, &bytes[1..])?; + let mut offset = 3; + let content = match MessageType(ty) { + $( + MessageType::$message_variant => $enum_ty::$enum_variant { + $( + $field: { + let val: $ty = try_read_9p_bytes(&bytes[offset..])?; + offset += val.n_bytes(); + val + }, + )* + }, + )+ + + MessageType(ty) => return Err(io::Error::new( + ErrorKind::InvalidData, + format!($err, ty), + )), + }; + + Ok(NinepReader::Complete($message_ty { tag, content })) + } + } + } + } + }; +} + +/// The Plan 9 File Protocol, 9P, is used for messages between clients and servers. A client +/// transmits requests (T- messages) to a server, which subsequently returns replies (R-messages) +/// to the client. The combined acts of transmitting (receiving) a request of a particular type, +/// and receiving (transmitting) its reply is called a transaction of that type. +/// +/// The data we decode into this struct is of the following form: +/// ```txt +/// size[4] type[1] tag[2] | content[...] +/// ``` +/// where the [MessageType] is a T variant. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Tmessage { + /// Each T-message has a tag field, chosen and used by the client to identify the message. The + /// reply to the message will have the same tag. Clients must arrange that no two outstanding + /// messages on the same connection have the same tag. An exception is the tag NOTAG, defined + /// as (ushort)~0 in : the client can use it, when establishing a connection, to + /// override tag matching in version messages. + pub tag: u16, + /// The t-message variant specific data sent by the client + pub content: Tdata, +} + +/// Generate the Tdata enum along with the wrapped T-message types and their implementations of NineP +macro_rules! impl_tdata { + ($( + $(#[$docs:meta])+ + $enum_variant:ident => $message_variant:ident { + $( + $(#[$field_docs:meta])+ + $field:ident: $ty:ty, + )* + } + )+) => { + /// T-message data variants + /// + /// The [Tmessage] struct is used to decode T-messages from clients. + /// See the individual message structs for docs on the format and semantics of each variant. + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum Tdata { + $( $(#[$docs])+ $enum_variant { $($(#[$field_docs])+ $field: $ty,)* }, )+ + } + + impl_message_format!( + Tmessage, TmessageReader, Tdata, "invalid message type for t-message: {}"; + $($enum_variant => $message_variant { + $($field: $ty,)* + })+ + ); + }; +} + +impl_tdata! { + /// http://man.cat-v.org/plan_9/5/version + /// size[4] Tversion tag[2] | msize[4] version[s] + Version => Tversion { + /// The requested message size + msize: u32, + /// The requested protocol version + version: String, + } + + /// http://man.cat-v.org/plan_9/5/attach + /// size[4] Tauth tag[2] | afid[4] uname[s] aname[s] + Auth => Tauth { + /// The fid to authenticate against + afid: u32, + /// The user authenticating + uname: String, + /// The filetree to access + aname: String, + } + + /// http://man.cat-v.org/plan_9/5/attach + /// size[4] Tattach tag[2] | fid[4] afid[4] uname[s] aname[s] + Attach => Tattach { + /// The fid to attach to + fid: u32, + /// The fid to authenticate against + afid: u32, + /// The user attaching + uname: String, + /// The filetree to access + aname: String, + } + + /// http://man.cat-v.org/plan_9/5/flush + /// size[4] Tflush tag[2] | oldtag[2] + Flush => Tflush { + /// The tag to flush + old_tag: u16, + } + + /// http://man.cat-v.org/plan_9/5/walk + /// size[4] Twalk tag[2] | fid[4] newfid[4] nwname[2] nwname*(wname[s]) + Walk => Twalk { + /// The fid to walk from + fid: u32, + /// The fid to associate with the end of the walk + new_fid: u32, + /// Path segments to walk from fid to new_fid + wnames: Vec, + } + + /// http://man.cat-v.org/plan_9/5/open + /// size[4] Topen tag[2] | fid[4] mode[1] + Open => Topen { + /// The fid to open + fid: u32, + /// The mode to open the resource in + mode: u8, + } + + /// http://man.cat-v.org/plan_9/5/open + /// size[4] Tcreate tag[2] | fid[4] name[s] perm[4] mode[1] + Create => Tcreate { + /// The fid to associate with the directory where the file should be created + fid: u32, + /// The name of the new file + name: String, + /// The permissions to use + perm: u32, + /// The mode to use + mode: u8, + } + + /// http://man.cat-v.org/plan_9/5/read + /// size[4] Tread tag[2] | fid[4] offset[8] count[4] + Read => Tread { + /// The fid to read + fid: u32, + /// The offset in bytes to start reading at + offset: u64, + /// The numbder of bytes to read + count: u32, + } + + /// http://man.cat-v.org/plan_9/5/read + /// size[4] Twrite tag[2] | fid[4] offset[8] count[4] data[count] + Write => Twrite { + /// The fid to write to + fid: u32, + /// The offset in bytes to start writing at + offset: u64, + /// The data to write + data: Data, + } + + /// http://man.cat-v.org/plan_9/5/clunk + /// size[4] Tclunk tag[2] | fid[4] + Clunk => Tclunk { + /// The fid to be closed + fid: u32, + } + + /// http://man.cat-v.org/plan_9/5/remove + /// size[4] Tremove tag[2] | fid[4] + Remove => Tremove { + /// The fid to be removed + fid: u32, + } + + /// http://man.cat-v.org/plan_9/5/stat + /// size[4] Tstat tag[2] | fid[4] + Stat => Tstat { + /// The fid to request a [Stat] for + fid: u32, + } + + /// http://man.cat-v.org/plan_9/5/stat + /// size[4] Twstat tag[2] | fid[4] stat[n] + Wstat => Twstat { + /// The fid to update the [Stat] for + fid: u32, + /// The size of the following stat + size: u16, + /// The stat data to be written + stat: RawStat, + } +} + +/// The Plan 9 File Protocol, 9P, is used for messages between clients and servers. A client +/// transmits requests (T- messages) to a server, which subsequently returns replies (R-messages) +/// to the client. The combined acts of transmitting (receiving) a request of a particular type, +/// and receiving (transmitting) its reply is called a transaction of that type. +/// +/// The data we decode into this struct is of the following form: +/// ```txt +/// size[4] type[1] tag[2] | content[...] +/// ``` +/// where the [MessageType] is a R variant. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Rmessage { + /// Each T-message has a tag field, chosen and used by the client to identify the message. The + /// reply to the message will have the same tag. Clients must arrange that no two outstanding + /// messages on the same connection have the same tag. An exception is the tag NOTAG, defined + /// as (ushort)~0 in : the client can use it, when establishing a connection, to + /// override tag matching in version messages. + pub tag: u16, + /// The r-message variant specific data sent by the client + pub content: Rdata, +} + +/// Generate the Rdata enum along with the wrapped R-message types +/// and their implementations of Format9p +macro_rules! impl_rdata { + ($( + $(#[$docs:meta])+ + $enum_variant:ident => $message_variant:ident { + $( + $(#[$field_docs:meta])+ + $field:ident: $ty:ty, + )* + } + )+) => { + /// R-message data variants + /// + /// The [Rmessage] struct is used to encode and send R-messages to clients. + /// See the individual message structs for docs on the format and semantics of each variant. + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum Rdata { + $( $(#[$docs])+ $enum_variant { $($(#[$field_docs])+ $field: $ty,)* }, )+ + } + + impl_message_format!( + Rmessage, RmessageReader, Rdata, "invalid message type for r-message: {}"; + $($enum_variant => $message_variant { + $($field: $ty,)* + })+ + ); + }; +} + +impl_rdata! { + /// http://man.cat-v.org/plan_9/5/version + /// size[4] Rversion tag[2] | msize[4] version[s] + Version => Rversion { + /// Supported message size + msize: u32, + /// Supported protocol version + version: String, + } + + /// http://man.cat-v.org/plan_9/5/attach + /// size[4] Rauth tag[2] | aqid[13] + Auth => Rauth { + /// The authenticated Qid of the connected root + aqid: Qid, + } + + /// http://man.cat-v.org/plan_9/5/error + /// size[4] Rerror tag[2] | ename[s] + Error => Rerror { + /// The contents of the error being returned + ename: String, + } + + /// http://man.cat-v.org/plan_9/5/attach + /// size[4] Rattach tag[2] | aquid[13] + Attach => Rattach { + /// Qid corresponding to the Fid used to attach + aqid: Qid, + } + + /// http://man.cat-v.org/plan_9/5/flush + /// size[4] Rflush tag[2] + Flush => Rflush {} + + /// http://man.cat-v.org/plan_9/5/walk + /// size[4] Rwalk tag[2] | nwqid[2] nwqid*(wqid[13]) + Walk => Rwalk { + /// Qids for the path elements walked + wqids: Vec, + } + + /// http://man.cat-v.org/plan_9/5/open + /// size[4] Ropen tag[2] | qid[13] iounit[4] + Open => Ropen { + /// Qid of the opened resource + qid: Qid, + /// IO unit for subsequent read / write operations + iounit: u32, + } + + /// http://man.cat-v.org/plan_9/5/open + /// size[4] Rcreate tag[2] | qid[13] iounit[4] + Create => Rcreate { + /// Qid of the created resource + qid: Qid, + /// IO unit for subsequent read / write operations + iounit: u32, + } + + /// http://man.cat-v.org/plan_9/5/read + /// size[4] Rread tag[2] | count[4] data[count] + Read => Rread { + /// The bytes read + data: Data, + } + + /// http://man.cat-v.org/plan_9/5/read + /// size[4] Rwrite tag[2] | count[4] + Write => Rwrite { + /// The number of bytes written + count: u32, + } + + /// http://man.cat-v.org/plan_9/5/clunk + /// size[4] Rclunk tag[2] + Clunk => Rclunk {} + + /// http://man.cat-v.org/plan_9/5/remove + /// size[4] Rremove tag[2] + Remove => Rremove {} + + /// http://man.cat-v.org/plan_9/5/stat + /// size[4] Rstat tag[2] | stat[n] + Stat => Rstat { + /// The size of the following stat + size: u16, + /// The stat data for the requested fid + stat: RawStat, + } + + /// http://man.cat-v.org/plan_9/5/stat + /// size[4] Rwstat tag[2] + Wstat => Rwstat {} +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::Format9p as _; + use simple_test_case::test_case; + use std::{cmp::PartialEq, io::Cursor}; + + #[test] + fn uint_decode() { + let buf: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]; + + assert_eq!(0x01, try_read_9p_bytes::(&buf).unwrap()); + assert_eq!(0x2301, try_read_9p_bytes::(&buf).unwrap()); + assert_eq!(0x67452301, try_read_9p_bytes::(&buf).unwrap()); + assert_eq!(0xefcdab8967452301, try_read_9p_bytes::(&buf).unwrap()); + } + + #[test] + fn reading_a_string_works() { + let s = "Hello, world!".to_owned(); + let mut buf = Cursor::new(Vec::new()); + s.write_to(&mut buf).unwrap(); + + let res = try_read_9p_bytes::(&buf.into_inner()).unwrap(); + assert_eq!(res, "Hello, world!"); + } + + #[test_case("test", &[0x04, 0x00, 0x74, 0x65, 0x73, 0x74]; "single byte chars only")] + #[test_case("", &[0x00, 0x00]; "empty string")] + #[test_case( + "Hello, 世界", + &[0x0d, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c]; + "including multi-byte chars" + )] + #[test] + fn string_encode(s: &str, bytes: &[u8]) { + let s = s.to_string(); + let buf = write_9p_bytes(&s).unwrap(); + assert_eq!(&buf, bytes); + } + + enum F9 { + U8(u8), + U16(u16), + U32(u32), + U64(u64), + S(&'static str), + V(Vec), + D(Vec), + RawStat, + Clunk, + Walk, + } + + // simple_test_case doesn't handle generic args for parameterised + // tests so I'm wrapping things up in the above enum and destructuring + // into the inner types before calling this instead. + fn round_trip_inner(t1: T) + where + T: NineP + PartialEq + fmt::Debug, + { + let buf = write_9p_bytes(&t1).unwrap(); + let t2 = try_read_9p_bytes::(&buf).unwrap(); + + assert_eq!(t1, t2); + } + + #[test_case(F9::U8(42); "u8_")] + #[test_case(F9::U16(17); "u16_")] + #[test_case(F9::U32(773); "u32_")] + #[test_case(F9::U64(123456); "u64_")] + #[test_case(F9::S("testing"); "single-byte char string")] + #[test_case(F9::S("Hello, 世界"); "multi-byte char string")] + #[test_case(F9::V(vec!["foo".to_string(), "bar".to_string()]); "vec String")] + #[test_case(F9::D(vec![5, 6, 7, 8, u8::MAX]); "data")] + #[test_case(F9::RawStat; "raw stat")] + #[test_case(F9::Clunk; "clunk")] + #[test_case(F9::Walk; "walk")] + #[test] + fn round_trip_is_fine(data: F9) { + match data { + F9::U8(t) => round_trip_inner(t), + F9::U16(t) => round_trip_inner(t), + F9::U32(t) => round_trip_inner(t), + F9::U64(t) => round_trip_inner(t), + F9::S(t) => round_trip_inner(t.to_string()), + F9::V(t) => round_trip_inner(t), + F9::D(t) => round_trip_inner(Data(t)), + F9::RawStat => round_trip_inner(RawStat { + size: 1, + ty: 2, + dev: 3, + qid: Qid { + ty: 1, + version: 2, + path: 3, + }, + mode: 4, + atime: 5, + mtime: 6, + length: 7, + name: "test name".to_string(), + uid: "test uid".to_string(), + gid: "test gid".to_string(), + muid: "test muid".to_string(), + }), + F9::Clunk => round_trip_inner(Rmessage { + tag: 0, + content: Rdata::Clunk {}, + }), + F9::Walk => round_trip_inner(Tmessage { + tag: 0, + content: Tdata::Walk { + fid: 0, + new_fid: 2, + wnames: vec!["bar".to_string()], + }, + }), + } + } +} From 1a4a9f7531f125491ea7cbaaf759a3da0d8b0c1b Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Sun, 23 Feb 2025 17:28:32 +0000 Subject: [PATCH 02/12] recreating the existing sync API --- crates/ad_client/src/lib.rs | 2 +- crates/ninep/examples/acme_test.rs | 2 +- crates/ninep/examples/ad_test.rs | 2 +- crates/ninep/examples/client.rs | 2 +- crates/ninep/examples/server.rs | 2 +- crates/ninep/src/fs.rs | 2 +- crates/ninep/src/lib.rs | 50 +- crates/ninep/src/protocol.rs | 893 ------------------ crates/ninep/src/sansio/mod.rs | 28 + .../src/{sansio.rs => sansio/protocol.rs} | 81 +- crates/ninep/src/sansio/server.rs | 372 ++++++++ crates/ninep/src/{ => sync}/client.rs | 24 +- crates/ninep/src/sync/mod.rs | 78 ++ crates/ninep/src/{ => sync}/server.rs | 398 +------- src/fsys/buffer.rs | 2 +- src/fsys/event.rs | 2 +- src/fsys/log.rs | 2 +- src/fsys/mod.rs | 2 +- 18 files changed, 573 insertions(+), 1371 deletions(-) delete mode 100644 crates/ninep/src/protocol.rs create mode 100644 crates/ninep/src/sansio/mod.rs rename crates/ninep/src/{sansio.rs => sansio/protocol.rs} (96%) create mode 100644 crates/ninep/src/sansio/server.rs rename crates/ninep/src/{ => sync}/client.rs (98%) create mode 100644 crates/ninep/src/sync/mod.rs rename crates/ninep/src/{ => sync}/server.rs (59%) diff --git a/crates/ad_client/src/lib.rs b/crates/ad_client/src/lib.rs index 6f36dbe..49c8fc4 100644 --- a/crates/ad_client/src/lib.rs +++ b/crates/ad_client/src/lib.rs @@ -10,7 +10,7 @@ rustdoc::all, clippy::undocumented_unsafe_blocks )] -use ninep::client::{ReadLineIter, UnixClient}; +use ninep::sync::client::{ReadLineIter, UnixClient}; use std::{io, io::Write, os::unix::net::UnixStream}; mod event; diff --git a/crates/ninep/examples/acme_test.rs b/crates/ninep/examples/acme_test.rs index 03c1c86..c257e4a 100644 --- a/crates/ninep/examples/acme_test.rs +++ b/crates/ninep/examples/acme_test.rs @@ -1,5 +1,5 @@ //! A simple demo of the 9p client interface -use ninep::client::UnixClient; +use ninep::sync::client::UnixClient; use std::io; fn main() -> io::Result<()> { diff --git a/crates/ninep/examples/ad_test.rs b/crates/ninep/examples/ad_test.rs index 262670c..94efc92 100644 --- a/crates/ninep/examples/ad_test.rs +++ b/crates/ninep/examples/ad_test.rs @@ -1,5 +1,5 @@ //! A simple demo of the 9p client interface -use ninep::client::UnixClient; +use ninep::sync::client::UnixClient; use std::io; fn main() -> io::Result<()> { diff --git a/crates/ninep/examples/client.rs b/crates/ninep/examples/client.rs index 302dbe8..042036c 100644 --- a/crates/ninep/examples/client.rs +++ b/crates/ninep/examples/client.rs @@ -1,5 +1,5 @@ //! A simple demo of the 9p client interface -use ninep::{client::UnixClient, fs::FileType}; +use ninep::{fs::FileType, sync::client::UnixClient}; use std::io; fn main() -> io::Result<()> { diff --git a/crates/ninep/examples/server.rs b/crates/ninep/examples/server.rs index 3a6c254..1fc56ac 100644 --- a/crates/ninep/examples/server.rs +++ b/crates/ninep/examples/server.rs @@ -24,7 +24,7 @@ //! ``` use ninep::{ fs::{FileMeta, IoUnit, Mode, Perm, Stat}, - server::{ClientId, ReadOutcome, Serve9p, Server}, + sync::server::{ClientId, ReadOutcome, Serve9p, Server}, Result, }; use std::{ diff --git a/crates/ninep/src/fs.rs b/crates/ninep/src/fs.rs index 359f999..c0ec9df 100644 --- a/crates/ninep/src/fs.rs +++ b/crates/ninep/src/fs.rs @@ -1,5 +1,5 @@ //! Types for describing files in a 9p virtual filesystem -use super::protocol::{Format9p, Qid, RawStat}; +use super::sansio::protocol::{NineP, Qid, RawStat}; use std::{ mem::size_of, time::{Duration, SystemTime, UNIX_EPOCH}, diff --git a/crates/ninep/src/lib.rs b/crates/ninep/src/lib.rs index 4bbc28f..8826117 100644 --- a/crates/ninep/src/lib.rs +++ b/crates/ninep/src/lib.rs @@ -11,54 +11,12 @@ clippy::undocumented_unsafe_blocks )] -use std::{ - io::{Read, Write}, - net::TcpStream, - os::unix::net::UnixStream, -}; - -pub mod client; pub mod fs; -pub mod protocol; pub mod sansio; -pub mod server; - -use protocol::{Format9p, Rdata, Rmessage}; - -impl From<(u16, Result)> for Rmessage { - fn from((tag, content): (u16, Result)) -> Self { - Rmessage { - tag, - content: content.unwrap_or_else(|ename| Rdata::Error { ename }), - } - } -} +pub mod sync; +// pub mod client; +// pub mod protocol; +// pub mod server; /// A simple result type for errors returned from this crate pub type Result = std::result::Result; - -/// An underlying stream over which we can handle 9p connections -pub trait Stream: Read + Write + Send + Sized + 'static { - /// The underlying try_clone implementations for file descriptors can fail at the libc level so - /// we need to account for that here. - fn try_clone(&self) -> Result; - - /// Reply to the specified tag with a given Result. Err's will be converted to 9p error - /// messages automatically. - fn reply(&mut self, tag: u16, resp: Result) { - let r: Rmessage = (tag, resp).into(); - let _ = r.write_to(self); - } -} - -impl Stream for UnixStream { - fn try_clone(&self) -> Result { - self.try_clone().map_err(|e| e.to_string()) - } -} - -impl Stream for TcpStream { - fn try_clone(&self) -> Result { - self.try_clone().map_err(|e| e.to_string()) - } -} diff --git a/crates/ninep/src/protocol.rs b/crates/ninep/src/protocol.rs deleted file mode 100644 index 6be9a41..0000000 --- a/crates/ninep/src/protocol.rs +++ /dev/null @@ -1,893 +0,0 @@ -//! 9p protocol implementation -//! -//! http://man.cat-v.org/plan_9/5/ -use std::{ - fmt, - io::{self, Cursor, ErrorKind, Read, Write}, - mem::size_of, -}; - -/// The size of variable length data is denoted using a u16 so anything longer -/// than u16::MAX is not something we can handle. -pub const MAX_SIZE_FIELD: usize = u16::MAX as usize; -/// For data fields in read/write messages the size field is 32bits not 16 -pub const MAX_DATA_SIZE_FIELD: usize = u32::MAX as usize; -/// The maximum number of bytes we allow in a Data buffer: a client attempting -/// to use more than this is an error. -pub const MAX_DATA_LEN: usize = 32 * 1024 * 1024; - -/// Something that can be encoded to and decoded 9p protocol messages. -/// -/// From [INTRO(5)](http://man.cat-v.org/plan_9/5/intro): -/// Each message consists of a sequence of bytes. Two-, four-, and eight-byte fields hold -/// unsigned integers represented in little-endian order (least significant byte first). -pub trait Format9p: Sized { - /// Number of bytes required to encode - fn n_bytes(&self) -> usize; - - /// Encode self as bytes for the 9p protocol and write to the given Writer - fn write_to(&self, w: &mut W) -> io::Result<()>; - - /// Decode self from 9p protocol bytes coming from the given Reader - fn read_from(r: &mut R) -> io::Result; -} - -// Unsigned integer types can all be treated the same way so we stamp them out using a macro. -// They are written and read in their little-endian byte form. -macro_rules! impl_u { - ($($ty:ty),+) => { - $(impl Format9p for $ty { - fn n_bytes(&self) -> usize { - size_of::<$ty>() - } - - fn write_to(&self, w: &mut W) -> io::Result<()> { - w.write_all(&self.to_le_bytes()) - } - - fn read_from(r: &mut R) -> io::Result { - let mut buf = [0u8; size_of::<$ty>()]; - r.read_exact(&mut buf)?; - - Ok(<$ty>::from_le_bytes(buf)) - } - })+ - }; -} - -impl_u!(u8, u16, u32, u64); - -// [size: u16] [content as bytes...] -// -// From [INTRO(5)](http://man.cat-v.org/plan_9/5/intro): -// Data items of larger or variable lengths are represented by a two-byte field specifying -// a count, n, followed by n bytes of data. Text strings are represented this way, with -// the text itself stored as a UTF-8 encoded sequence of Unicode charac- ters (see utf(6)). -// -// Text strings in 9P messages are not NUL- terminated: n counts the bytes of UTF-8 data, -// which include no final zero byte. The NUL character is illegal in all text strings -// in 9P, and is therefore excluded from file names, user names, and so on. -impl Format9p for String { - fn n_bytes(&self) -> usize { - size_of::() + self.len() - } - - fn write_to(&self, w: &mut W) -> io::Result<()> { - let len = self.len(); - if len > MAX_SIZE_FIELD { - return Err(io::Error::new( - ErrorKind::InvalidInput, - format!("string too long: max={MAX_SIZE_FIELD} len={len}"), - )); - } - - (len as u16).write_to(w)?; - w.write_all(self.as_bytes()) - } - - fn read_from(r: &mut R) -> io::Result { - let len = u16::read_from(r)? as usize; - let mut s = String::with_capacity(len); - r.take(len as u64).read_to_string(&mut s)?; - let actual = s.len(); - - if actual < len { - return Err(io::Error::new( - ErrorKind::UnexpectedEof, - format!("unexpected end of string: wanted {len}, got {actual}"), - )); - } - - Ok(s) - } -} - -// [size: u16] [content as bytes...] -// -// From [INTRO(5)](http://man.cat-v.org/plan_9/5/intro): -// Data items of larger or variable lengths are represented by a two-byte field specifying -// a count, n, followed by n bytes of data. -impl Format9p for Vec { - fn n_bytes(&self) -> usize { - size_of::() + self.iter().map(|t| t.n_bytes()).sum::() - } - - fn write_to(&self, w: &mut W) -> io::Result<()> { - let n_bytes = self.iter().map(|t| t.n_bytes()).sum::(); - if n_bytes > MAX_SIZE_FIELD { - return Err(io::Error::new( - ErrorKind::InvalidInput, - format!("vec too long: max={MAX_SIZE_FIELD} len={n_bytes}"), - )); - } - - (self.len() as u16).write_to(w)?; - for t in self { - t.write_to(w)?; - } - - Ok(()) - } - - fn read_from(r: &mut R) -> io::Result { - let n = u16::read_from(r)? as usize; - let mut buf = Vec::with_capacity(n); - - for _ in 0..n { - buf.push(T::read_from(r)?); - } - - Ok(buf) - } -} - -/// A wrapper around a Vec for handling data fields in read/write messages -/// ```text -/// READ(5) -/// NAME -/// read, write - transfer data from and to a file -/// -/// SYNOPSIS -/// size[4] Tread tag[2] fid[4] offset[8] count[4] -/// size[4] Rread tag[2] count[4] data[count] -/// -/// size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] -/// size[4] Rwrite tag[2] count[4] -/// ``` -#[derive(Clone, PartialEq, Eq)] -pub struct Data(pub(super) Vec); - -impl fmt::Debug for Data { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Data(n_bytes={})", self.0.len()) - } -} - -impl From> for Data { - fn from(value: Vec) -> Self { - Self(value) - } -} - -impl TryFrom for Vec { - type Error = io::Error; - - fn try_from(value: Data) -> Result { - let mut r = Cursor::new(value.0); - let mut buf = Vec::new(); - - loop { - match RawStat::read_from(&mut r) { - Ok(rs) => buf.push(rs), - Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, - Err(e) => return Err(e), - } - } - - Ok(buf) - } -} - -impl Format9p for Data { - fn n_bytes(&self) -> usize { - size_of::() + self.0.len() - } - - fn write_to(&self, w: &mut W) -> io::Result<()> { - let n_bytes = self.0.len(); - if n_bytes > MAX_DATA_SIZE_FIELD { - return Err(io::Error::new( - ErrorKind::InvalidInput, - format!("data field too long: max={MAX_DATA_SIZE_FIELD} len={n_bytes}"), - )); - } - - (n_bytes as u32).write_to(w)?; - w.write_all(&self.0) - } - - fn read_from(r: &mut R) -> io::Result { - let len = u32::read_from(r)? as usize; - if len > MAX_DATA_LEN { - return Err(io::Error::new( - ErrorKind::InvalidData, - format!("data field too long: max={MAX_DATA_LEN} len={len}"), - )); - } - - let mut buf = Vec::with_capacity(len); - r.take(len as u64).read_to_end(&mut buf)?; - let actual = buf.len(); - - if actual < len { - return Err(io::Error::new( - ErrorKind::UnexpectedEof, - format!("unexpected end of data: wanted {len}, got {actual}"), - )); - } - - Ok(Data(buf)) - } -} - -/// Taken from the enum in fcall.h in the plan9 source. -/// https://github.com/9fans/plan9port/blob/master/include/fcall.h#L80 -/// -/// This is just used internally to help with defining the encode / decode behaviour -/// of the various message types. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -struct MessageType(u8); - -#[allow(non_upper_case_globals)] -impl MessageType { - const Tversion: Self = Self(100); - const Rversion: Self = Self(101); - - const Tauth: Self = Self(102); - const Rauth: Self = Self(103); - - const Tattach: Self = Self(104); - const Rattach: Self = Self(105); - - // Terror = 106, - const Rerror: Self = Self(107); - - const Tflush: Self = Self(108); - const Rflush: Self = Self(109); - - const Twalk: Self = Self(110); - const Rwalk: Self = Self(111); - - const Topen: Self = Self(112); - const Ropen: Self = Self(113); - - const Tcreate: Self = Self(114); - const Rcreate: Self = Self(115); - - const Tread: Self = Self(116); - const Rread: Self = Self(117); - - const Twrite: Self = Self(118); - const Rwrite: Self = Self(119); - - const Tclunk: Self = Self(120); - const Rclunk: Self = Self(121); - - const Tremove: Self = Self(122); - const Rremove: Self = Self(123); - - const Tstat: Self = Self(124); - const Rstat: Self = Self(125); - - const Twstat: Self = Self(126); - const Rwstat: Self = Self(127); - // Tmax = 128, - // Topenfd = 98, - // Ropenfd = 99, -} - -/// Helper for defining a struct that implements Format9p by just serialising their -/// fields directly without a size field. -macro_rules! impl_message_datatype { - ( - $(#[$docs:meta])+ - struct $struct:ident { - $( - $(#[$field_docs:meta])* - $field:ident: $ty:ty, - )* - } - ) => { - $(#[$docs])+ - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct $struct { - $( - $(#[$field_docs])* - pub $field: $ty, - )* - } - - impl Format9p for $struct { - fn n_bytes(&self) -> usize { - #[allow(unused_mut)] - let mut n = 0; - $(n += self.$field.n_bytes();)* - n - } - - fn write_to(&self, _w: &mut W) -> io::Result<()> { - $(self.$field.write_to(_w)?;)* - Ok(()) - } - - fn read_from(_r: &mut R) -> io::Result { - $(let $field = <$ty>::read_from(_r)?;)* - Ok(Self { $($field,)* }) - } - } - }; -} - -macro_rules! impl_message_format { - ( - $message_ty:ident, $enum_ty:ident, $err:expr; - $($enum_variant:ident => $message_variant:ident { - $($field:ident: $ty:ty,)* - })+ - ) => { - impl Format9p for $message_ty { - fn n_bytes(&self) -> usize { - let content_size = match &self.content { - $( - $enum_ty::$enum_variant { $($field,)* } => { - #[allow(unused_mut)] - let mut n = 0; - $(n += $field.n_bytes();)* - n - } - )+ - }; - - // size[4] type[1] tag[2] | content[...] - 4 + 1 + 2 + content_size - } - - fn write_to(&self, w: &mut W) -> io::Result<()> { - let ty = match self.content { - $($enum_ty::$enum_variant { .. } => MessageType::$message_variant.0,)+ - }; - - (self.n_bytes() as u32).write_to(w)?; - ty.write_to(w)?; - self.tag.write_to(w)?; - - match &self.content { - $( - $enum_ty::$enum_variant { $($field,)* } => { - $($field.write_to(w)?;)* - }, - )+ - } - - Ok(()) - } - - fn read_from(r: &mut R) -> io::Result { - // the size field includes the number of bytes for the field itself so we - // trim that off before decoding the rest of the message - let size = u32::read_from(r)?; - let r = &mut r.take((size - 4) as u64); - - let mut ty_buf = [0u8]; - r.read_exact(&mut ty_buf)?; - - let tag = u16::read_from(r)?; - let content = match MessageType(ty_buf[0]) { - $( - MessageType::$message_variant => $enum_ty::$enum_variant { - $($field: Format9p::read_from(r)?,)* - }, - )+ - - MessageType(ty) => return Err(io::Error::new( - ErrorKind::InvalidData, - format!($err, ty), - )), - }; - - Ok(Self { tag, content }) - } - } - - }; -} - -/// Generate the Tmessage enum along with the wrapped T-message types -/// and their implementations of Format9p -macro_rules! impl_tmessages { - ($( - $(#[$docs:meta])+ - $enum_variant:ident => $message_variant:ident { - $( - $(#[$field_docs:meta])+ - $field:ident: $ty:ty, - )* - } - )+) => { - /// T-message data variants - /// - /// The [Tmessage] struct is used to decode T-messages from clients. - /// See the individual message structs for docs on the format and semantics of each variant. - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum Tdata { - $( $(#[$docs])+ $enum_variant { $($(#[$field_docs])+ $field: $ty,)* }, )+ - } - - impl_message_format!( - Tmessage, Tdata, "invalid message type for t-message: {}"; - $($enum_variant => $message_variant { - $($field: $ty,)* - })+ - ); - }; -} - -/// Generate the Rmessage enum along with the wrapped R-message types -/// and their implementations of Format9p -macro_rules! impl_rmessages { - ($( - $(#[$docs:meta])+ - $enum_variant:ident => $message_variant:ident { - $( - $(#[$field_docs:meta])+ - $field:ident: $ty:ty, - )* - } - )+) => { - /// R-message data variants - /// - /// The [Rmessage] struct is used to encode and send R-messages to clients. - /// See the individual message structs for docs on the format and semantics of each variant. - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum Rdata { - $( $(#[$docs])+ $enum_variant { $($(#[$field_docs])+ $field: $ty,)* }, )+ - } - - impl_message_format!( - Rmessage, Rdata, "invalid message type for r-message: {}"; - $($enum_variant => $message_variant { - $($field: $ty,)* - })+ - ); - }; -} - -impl_message_datatype!( - /// A machine-independent directory entry - /// http://man.cat-v.org/plan_9/5/stat - struct RawStat { - /// size[2] total byte count of the following data - size: u16, - /// type[2] for kernel use - ty: u16, - /// dev[4] for kernel use - dev: u32, - /// Qid type, version and path - qid: Qid, - /// mode[4] permissions and flags - mode: u32, - /// atime[4] last access time - atime: u32, - /// mtime[4] last modification time - mtime: u32, - /// length[8] length of file in bytes - length: u64, - /// name[ s ] file name; must be / if the file is the root directory of the server - name: String, - /// uid[ s ] owner name - uid: String, - /// gid[ s ] group name - gid: String, - /// muid[ s ] name of the user who last modified the file - muid: String, - } -); - -impl_message_datatype!( - /// A qid represents the server's unique identification for the file being accessed: two files - /// on the same server hierarchy are the same if and only if their qids are the same. - #[derive(Copy)] - struct Qid { - /// qid.type[1] - /// the type of the file (directory, etc.), repre- sented as a bit vector corresponding to the - /// high 8 bits of the file's mode word. - ty: u8, - /// qid.vers[4] version number for given path - version: u32, - /// qid.path[8] the file server's unique identification for the file - path: u64, - } -); - -/// The Plan 9 File Protocol, 9P, is used for messages between clients and servers. A client -/// transmits requests (T- messages) to a server, which subsequently returns replies (R-messages) -/// to the client. The combined acts of transmitting (receiving) a request of a particular type, -/// and receiving (transmitting) its reply is called a transaction of that type. -/// -/// The data we decode into this struct is of the following form: -/// ```txt -/// size[4] type[1] tag[2] | content[...] -/// ``` -/// where the [MessageType] is a T variant. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Tmessage { - /// Each T-message has a tag field, chosen and used by the client to identify the message. The - /// reply to the message will have the same tag. Clients must arrange that no two outstanding - /// messages on the same connection have the same tag. An exception is the tag NOTAG, defined - /// as (ushort)~0 in : the client can use it, when establishing a connection, to - /// override tag matching in version messages. - pub tag: u16, - /// The t-message variant specific data sent by the client - pub content: Tdata, -} - -impl_tmessages! { - /// http://man.cat-v.org/plan_9/5/version - /// size[4] Tversion tag[2] | msize[4] version[s] - Version => Tversion { - /// The requested message size - msize: u32, - /// The requested protocol version - version: String, - } - - /// http://man.cat-v.org/plan_9/5/attach - /// size[4] Tauth tag[2] | afid[4] uname[s] aname[s] - Auth => Tauth { - /// The fid to authenticate against - afid: u32, - /// The user authenticating - uname: String, - /// The filetree to access - aname: String, - } - - /// http://man.cat-v.org/plan_9/5/attach - /// size[4] Tattach tag[2] | fid[4] afid[4] uname[s] aname[s] - Attach => Tattach { - /// The fid to attach to - fid: u32, - /// The fid to authenticate against - afid: u32, - /// The user attaching - uname: String, - /// The filetree to access - aname: String, - } - - /// http://man.cat-v.org/plan_9/5/flush - /// size[4] Tflush tag[2] | oldtag[2] - Flush => Tflush { - /// The tag to flush - old_tag: u16, - } - - /// http://man.cat-v.org/plan_9/5/walk - /// size[4] Twalk tag[2] | fid[4] newfid[4] nwname[2] nwname*(wname[s]) - Walk => Twalk { - /// The fid to walk from - fid: u32, - /// The fid to associate with the end of the walk - new_fid: u32, - /// Path segments to walk from fid to new_fid - wnames: Vec, - } - - /// http://man.cat-v.org/plan_9/5/open - /// size[4] Topen tag[2] | fid[4] mode[1] - Open => Topen { - /// The fid to open - fid: u32, - /// The mode to open the resource in - mode: u8, - } - - /// http://man.cat-v.org/plan_9/5/open - /// size[4] Tcreate tag[2] | fid[4] name[s] perm[4] mode[1] - Create => Tcreate { - /// The fid to associate with the directory where the file should be created - fid: u32, - /// The name of the new file - name: String, - /// The permissions to use - perm: u32, - /// The mode to use - mode: u8, - } - - /// http://man.cat-v.org/plan_9/5/read - /// size[4] Tread tag[2] | fid[4] offset[8] count[4] - Read => Tread { - /// The fid to read - fid: u32, - /// The offset in bytes to start reading at - offset: u64, - /// The numbder of bytes to read - count: u32, - } - - /// http://man.cat-v.org/plan_9/5/read - /// size[4] Twrite tag[2] | fid[4] offset[8] count[4] data[count] - Write => Twrite { - /// The fid to write to - fid: u32, - /// The offset in bytes to start writing at - offset: u64, - /// The data to write - data: Data, - } - - /// http://man.cat-v.org/plan_9/5/clunk - /// size[4] Tclunk tag[2] | fid[4] - Clunk => Tclunk { - /// The fid to be closed - fid: u32, - } - - /// http://man.cat-v.org/plan_9/5/remove - /// size[4] Tremove tag[2] | fid[4] - Remove => Tremove { - /// The fid to be removed - fid: u32, - } - - /// http://man.cat-v.org/plan_9/5/stat - /// size[4] Tstat tag[2] | fid[4] - Stat => Tstat { - /// The fid to request a [Stat] for - fid: u32, - } - - /// http://man.cat-v.org/plan_9/5/stat - /// size[4] Twstat tag[2] | fid[4] stat[n] - Wstat => Twstat { - /// The fid to update the [Stat] for - fid: u32, - /// The size of the following stat - size: u16, - /// The stat data to be written - stat: RawStat, - } -} - -/// The Plan 9 File Protocol, 9P, is used for messages between clients and servers. A client -/// transmits requests (T- messages) to a server, which subsequently returns replies (R-messages) -/// to the client. The combined acts of transmitting (receiving) a request of a particular type, -/// and receiving (transmitting) its reply is called a transaction of that type. -/// -/// The data we decode into this struct is of the following form: -/// ```txt -/// size[4] type[1] tag[2] | content[...] -/// ``` -/// where the [MessageType] is a R variant. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Rmessage { - /// Each T-message has a tag field, chosen and used by the client to identify the message. The - /// reply to the message will have the same tag. Clients must arrange that no two outstanding - /// messages on the same connection have the same tag. An exception is the tag NOTAG, defined - /// as (ushort)~0 in : the client can use it, when establishing a connection, to - /// override tag matching in version messages. - pub tag: u16, - /// The r-message variant specific data sent by the client - pub content: Rdata, -} - -impl_rmessages! { - /// http://man.cat-v.org/plan_9/5/version - /// size[4] Rversion tag[2] | msize[4] version[s] - Version => Rversion { - /// Supported message size - msize: u32, - /// Supported protocol version - version: String, - } - - /// http://man.cat-v.org/plan_9/5/attach - /// size[4] Rauth tag[2] | aqid[13] - Auth => Rauth { - /// The authenticated Qid of the connected root - aqid: Qid, - } - - /// http://man.cat-v.org/plan_9/5/error - /// size[4] Rerror tag[2] | ename[s] - Error => Rerror { - /// The contents of the error being returned - ename: String, - } - - /// http://man.cat-v.org/plan_9/5/attach - /// size[4] Rattach tag[2] | aquid[13] - Attach => Rattach { - /// Qid corresponding to the Fid used to attach - aqid: Qid, - } - - /// http://man.cat-v.org/plan_9/5/flush - /// size[4] Rflush tag[2] - Flush => Rflush {} - - /// http://man.cat-v.org/plan_9/5/walk - /// size[4] Rwalk tag[2] | nwqid[2] nwqid*(wqid[13]) - Walk => Rwalk { - /// Qids for the path elements walked - wqids: Vec, - } - - /// http://man.cat-v.org/plan_9/5/open - /// size[4] Ropen tag[2] | qid[13] iounit[4] - Open => Ropen { - /// Qid of the opened resource - qid: Qid, - /// IO unit for subsequent read / write operations - iounit: u32, - } - - /// http://man.cat-v.org/plan_9/5/open - /// size[4] Rcreate tag[2] | qid[13] iounit[4] - Create => Rcreate { - /// Qid of the created resource - qid: Qid, - /// IO unit for subsequent read / write operations - iounit: u32, - } - - /// http://man.cat-v.org/plan_9/5/read - /// size[4] Rread tag[2] | count[4] data[count] - Read => Rread { - /// The bytes read - data: Data, - } - - /// http://man.cat-v.org/plan_9/5/read - /// size[4] Rwrite tag[2] | count[4] - Write => Rwrite { - /// The number of bytes written - count: u32, - } - - /// http://man.cat-v.org/plan_9/5/clunk - /// size[4] Rclunk tag[2] - Clunk => Rclunk {} - - /// http://man.cat-v.org/plan_9/5/remove - /// size[4] Rremove tag[2] - Remove => Rremove {} - - /// http://man.cat-v.org/plan_9/5/stat - /// size[4] Rstat tag[2] | stat[n] - Stat => Rstat { - /// The size of the following stat - size: u16, - /// The stat data for the requested fid - stat: RawStat, - } - - /// http://man.cat-v.org/plan_9/5/stat - /// size[4] Rwstat tag[2] - Wstat => Rwstat {} -} - -#[cfg(test)] -mod tests { - use super::*; - use simple_test_case::test_case; - use std::{cmp::PartialEq, io::Cursor}; - - #[test] - fn uint_n_bytes_is_correct() { - assert_eq!(0u8.n_bytes(), 1, "u8"); - assert_eq!(0u16.n_bytes(), 2, "u16"); - assert_eq!(0u32.n_bytes(), 4, "u32"); - assert_eq!(0u64.n_bytes(), 8, "u64"); - } - - #[test_case("test", 2 + 4; "single byte chars only")] - #[test_case("", 2; "empty string")] - #[test_case("Hello, 世界", 2 + 7 + 3 + 3; "including multi-byte chars")] - #[test] - fn string_n_bytes_is_correct(s: &str, expected: usize) { - assert_eq!(s.to_string().n_bytes(), expected); - } - - #[test] - fn uint_decode() { - let buf: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]; - let mut cur = Cursor::new(&buf); - - assert_eq!(0x01, u8::read_from(&mut cur).unwrap()); - cur.set_position(0); - assert_eq!(0x2301, u16::read_from(&mut cur).unwrap()); - cur.set_position(0); - assert_eq!(0x67452301, u32::read_from(&mut cur).unwrap()); - cur.set_position(0); - assert_eq!(0xefcdab8967452301, u64::read_from(&mut cur).unwrap()); - } - - #[test_case("test", &[0x04, 0x00, 0x74, 0x65, 0x73, 0x74]; "single byte chars only")] - #[test_case("", &[0x00, 0x00]; "empty string")] - #[test_case( - "Hello, 世界", - &[0x0d, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c]; - "including multi-byte chars" - )] - #[test] - fn string_encode(s: &str, bytes: &[u8]) { - let mut buf: Vec = vec![]; - s.to_string().write_to(&mut buf).unwrap(); - assert_eq!(&buf, bytes); - } - - enum F9 { - U8(u8), - U16(u16), - U32(u32), - U64(u64), - S(&'static str), - V(Vec), - D(Vec), - Clunk, - Walk, - } - - // simple_test_case doesn't handle generic args for parameterised - // tests so I'm wrapping things up in the above enum and destructuring - // into the inner types before calling this instead. - fn round_trip_inner(t1: T) - where - T: Format9p + PartialEq + fmt::Debug, - { - let mut buf = Cursor::new(Vec::new()); - t1.write_to(&mut buf).unwrap(); - buf.set_position(0); - - let t2 = T::read_from(&mut buf).unwrap(); - - assert_eq!(t1, t2); - } - - #[test_case(F9::U8(42); "u8_")] - #[test_case(F9::U16(17); "u16_")] - #[test_case(F9::U32(773); "u32_")] - #[test_case(F9::U64(123456); "u64_")] - #[test_case(F9::S("testing"); "single-byte char string")] - #[test_case(F9::S("Hello, 世界"); "multi-byte char string")] - #[test_case(F9::V(vec!["foo".to_string(), "bar".to_string()]); "vec String")] - #[test_case(F9::D(vec![5, 6, 7, 8, u8::MAX]); "data")] - #[test_case(F9::Clunk; "clunk")] - #[test_case(F9::Walk; "walk")] - #[test] - fn round_trip_is_fine(data: F9) { - match data { - F9::U8(t) => round_trip_inner(t), - F9::U16(t) => round_trip_inner(t), - F9::U32(t) => round_trip_inner(t), - F9::U64(t) => round_trip_inner(t), - F9::S(t) => round_trip_inner(t.to_string()), - F9::V(t) => round_trip_inner(t), - F9::D(t) => round_trip_inner(Data(t)), - F9::Clunk => round_trip_inner(Rmessage { - tag: 0, - content: Rdata::Clunk {}, - }), - F9::Walk => round_trip_inner(Tmessage { - tag: 0, - content: Tdata::Walk { - fid: 0, - new_fid: 2, - wnames: vec!["bar".to_string()], - }, - }), - } - } -} diff --git a/crates/ninep/src/sansio/mod.rs b/crates/ninep/src/sansio/mod.rs new file mode 100644 index 0000000..0c7fb48 --- /dev/null +++ b/crates/ninep/src/sansio/mod.rs @@ -0,0 +1,28 @@ +//! Sans-IO layer for working with the 9p protocol to write clients and servers +//! +//! See https://sans-io.readthedocs.io/how-to-sans-io.html for information on sans-io +use crate::{ + sansio::protocol::{Rdata, Rmessage}, + Result, +}; + +pub mod protocol; +pub mod server; + +impl From<(u16, Result)> for Rmessage { + fn from((tag, content): (u16, Result)) -> Self { + Rmessage { + tag, + content: content.unwrap_or_else(|ename| Rdata::Error { ename }), + } + } +} + +// TODO: pull up as much as possible into this trait + +/// An underlying stream over which we can handle 9p connections +pub trait Stream: Send + Sized + 'static { + /// The underlying try_clone implementations for file descriptors can fail at the libc level so + /// we need to account for that here. + fn try_clone(&self) -> Result; +} diff --git a/crates/ninep/src/sansio.rs b/crates/ninep/src/sansio/protocol.rs similarity index 96% rename from crates/ninep/src/sansio.rs rename to crates/ninep/src/sansio/protocol.rs index ac41e68..24c9fd6 100644 --- a/crates/ninep/src/sansio.rs +++ b/crates/ninep/src/sansio/protocol.rs @@ -71,12 +71,12 @@ pub trait Read9p: Sized { fn accept_bytes(self, bytes: &[u8]) -> io::Result>; } -#[inline] -fn accept_bytes_final(r: R, bytes: &[u8]) -> io::Result { - match r.accept_bytes(bytes)? { - NinepReader::Complete(t) => Ok(t), - NinepReader::Pending(_) => unreachable!(), - } +/// wrapper around uX::from_le_bytes that accepts a slice rather than a fixed size array +macro_rules! from_le_bytes { + ($ty:ty, $bytes:expr) => { + // SAFETY: we know we are setting the correct array length + unsafe { <$ty>::from_le_bytes($bytes[0..size_of::<$ty>()].try_into().unwrap_unchecked()) } + }; } /// Attempt to read a [NineP] value from a byte buffer that contains sufficient data without @@ -147,9 +147,7 @@ macro_rules! impl_u { } fn accept_bytes(self, bytes: &[u8]) -> io::Result> { - let buf = bytes[0..size_of::<$ty>()].try_into().unwrap(); - - Ok(NinepReader::Complete(<$ty>::from_le_bytes(buf))) + Ok(NinepReader::Complete(from_le_bytes!($ty, bytes))) } } )+ @@ -212,7 +210,7 @@ impl Read9p for StringReader { fn accept_bytes(self, mut bytes: &[u8]) -> io::Result> { match self { Self::Start => { - let len = accept_bytes_final(0u16, bytes)? as usize; + let len = from_le_bytes!(u16, bytes) as usize; Ok(NinepReader::Pending(Self::WithLen(len))) } @@ -291,7 +289,7 @@ impl Read9p for VecReader { fn accept_bytes(self, bytes: &[u8]) -> io::Result>> { match self { Self::Start => { - let len = accept_bytes_final(0u16, bytes)? as usize; + let len = from_le_bytes!(u16, bytes) as usize; let buf = Vec::with_capacity(len); let r = T::reader(); @@ -329,7 +327,7 @@ impl Read9p for VecReader { /// size[4] Rwrite tag[2] count[4] /// ``` #[derive(Clone, PartialEq, Eq)] -pub struct Data(pub(super) Vec); +pub struct Data(pub(crate) Vec); impl fmt::Debug for Data { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -410,7 +408,7 @@ impl Read9p for DataReader { fn accept_bytes(self, bytes: &[u8]) -> io::Result> { match self { DataReader::Start => { - let len = accept_bytes_final(0u32, bytes)? as usize; + let len = from_le_bytes!(u32, bytes) as usize; if len > MAX_DATA_LEN { return Err(io::Error::new( ErrorKind::InvalidData, @@ -441,29 +439,29 @@ impl Read9p for DataReader { #[derive(Debug, Clone, PartialEq, Eq)] pub struct RawStat { /// size[2] total byte count of the following data - size: u16, + pub size: u16, /// type[2] for kernel use - ty: u16, + pub ty: u16, /// dev[4] for kernel use - dev: u32, + pub dev: u32, /// Qid type, version and path - qid: Qid, + pub qid: Qid, /// mode[4] permissions and flags - mode: u32, + pub mode: u32, /// atime[4] last access time - atime: u32, + pub atime: u32, /// mtime[4] last modification time - mtime: u32, + pub mtime: u32, /// length[8] length of file in bytes - length: u64, + pub length: u64, /// name[ s ] file name; must be / if the file is the root directory of the server - name: String, + pub name: String, /// uid[ s ] owner name - uid: String, + pub uid: String, /// gid[ s ] group name - gid: String, + pub gid: String, /// muid[ s ] name of the user who last modified the file - muid: String, + pub muid: String, } macro_rules! write_fields { @@ -526,14 +524,14 @@ impl Read9p for RawStatReader { fn accept_bytes(self, bytes: &[u8]) -> io::Result> { match self { Self::Start => { - let size = accept_bytes_final(0u16, bytes)?; - let ty = accept_bytes_final(0u16, &bytes[2..])?; - let dev = accept_bytes_final(0u32, &bytes[4..])?; + let size = from_le_bytes!(u16, bytes); + let ty = from_le_bytes!(u16, &bytes[2..]); + let dev = from_le_bytes!(u32, &bytes[4..]); let qid: Qid = try_read_9p_bytes(&bytes[8..])?; - let mode = accept_bytes_final(0u32, &bytes[21..])?; - let atime = accept_bytes_final(0u32, &bytes[25..])?; - let mtime = accept_bytes_final(0u32, &bytes[29..])?; - let length = accept_bytes_final(0u64, &bytes[33..])?; + let mode = from_le_bytes!(u32, &bytes[21..]); + let atime = from_le_bytes!(u32, &bytes[25..]); + let mtime = from_le_bytes!(u32, &bytes[29..]); + let length = from_le_bytes!(u64, &bytes[33..]); let rs = RawStat { size, ty, @@ -968,12 +966,12 @@ macro_rules! impl_message_format { fn accept_bytes(self, bytes: &[u8]) -> io::Result> { match self { Self::Start => { - let size = accept_bytes_final(0u32, bytes)? as usize; + let size = from_le_bytes!(u32, bytes) as usize; Ok(NinepReader::Pending($reader::WithSize(size))) } Self::WithSize(_) => { - let ty = accept_bytes_final(0u8, bytes)?; - let tag = accept_bytes_final(0u16, &bytes[1..])?; + let ty = from_le_bytes!(u8, bytes); + let tag = from_le_bytes!(u16, &bytes[1..]); let mut offset = 3; let content = match MessageType(ty) { $( @@ -1333,9 +1331,8 @@ impl_rdata! { #[cfg(test)] mod tests { use super::*; - use crate::protocol::Format9p as _; use simple_test_case::test_case; - use std::{cmp::PartialEq, io::Cursor}; + use std::cmp::PartialEq; #[test] fn uint_decode() { @@ -1347,16 +1344,6 @@ mod tests { assert_eq!(0xefcdab8967452301, try_read_9p_bytes::(&buf).unwrap()); } - #[test] - fn reading_a_string_works() { - let s = "Hello, world!".to_owned(); - let mut buf = Cursor::new(Vec::new()); - s.write_to(&mut buf).unwrap(); - - let res = try_read_9p_bytes::(&buf.into_inner()).unwrap(); - assert_eq!(res, "Hello, world!"); - } - #[test_case("test", &[0x04, 0x00, 0x74, 0x65, 0x73, 0x74]; "single byte chars only")] #[test_case("", &[0x00, 0x00]; "empty string")] #[test_case( diff --git a/crates/ninep/src/sansio/server.rs b/crates/ninep/src/sansio/server.rs new file mode 100644 index 0000000..475adc6 --- /dev/null +++ b/crates/ninep/src/sansio/server.rs @@ -0,0 +1,372 @@ +//! Traits and structs for implementing a 9p fileserver +use crate::{ + fs::{FileMeta, FileType, QID_ROOT}, + sansio::{ + protocol::{Qid, Rdata, MAX_DATA_LEN}, + Stream, + }, + Result, +}; +use std::{ + cmp::min, + collections::btree_map::BTreeMap, + env, + sync::{mpsc::Receiver, Arc, Mutex, RwLock}, +}; + +/// Marker afid to denode that auth is not required for establishing connections +pub const AFID_NO_AUTH: u32 = u32::MAX; + +// Error messages +pub(crate) const E_NO_VERSION_MESSAGE: &str = "first message must be Tversion"; +pub(crate) const E_UNATTACHED: &str = "session is not attached"; +pub(crate) const E_AUTH_NOT_REQUIRED: &str = "authentication not required"; +pub(crate) const E_DUPLICATE_FID: &str = "duplicate fid"; +pub(crate) const E_UNKNOWN_FID: &str = "unknown fid"; +pub(crate) const E_UNKNOWN_ROOT: &str = "unknown root directory"; +pub(crate) const E_WALK_NON_DIR: &str = "walk in non-directory"; +pub(crate) const E_CREATE_NON_DIR: &str = "create in non-directory"; +pub(crate) const E_INVALID_OFFSET: &str = "invalid offset for read on directory"; + +pub(crate) const UNKNOWN_VERSION: &str = "unknown"; +pub(crate) const SUPPORTED_VERSION: &str = "9P2000"; + +const DEFAULT_DISPLAY_VALUE: &str = ":0"; + +/// Determine the 9p socket directory based on the USER and DISPLAY environment variables +pub fn socket_dir() -> String { + let uname = env::var("USER").unwrap(); + let display = env::var("DISPLAY").unwrap_or(String::from(DEFAULT_DISPLAY_VALUE)); + format!("/tmp/ns.{uname}.{display}") +} + +/// The unix socket path that will be used for a given server name. +pub fn socket_path(name: &str) -> String { + let socket_dir = socket_dir(); + format!("{socket_dir}/{name}") +} + +/// An opaque client ID that can be used by server implementations to determine which client a +/// request originated from by comparing equality. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ClientId(pub(crate) u64); + +/// The outcome of a client attempting to [read](Serve9p::read) a given file. +#[derive(Debug)] +pub enum ReadOutcome { + /// The data is immediately available. + Immediate(Vec), + /// No response should be sent until data is received on the provided channel + Blocked(Receiver>), +} + +/// A 9p server wrapping an `S` that must implement an IO specific handler trait to provide the +/// actual filesystem implementation. +#[derive(Debug)] +pub struct Server { + pub(crate) s: Arc>, + pub(crate) msize: u32, + pub(crate) roots: BTreeMap, + pub(crate) qids: Arc>>, + pub(crate) next_client_id: u64, +} + +impl Server { + /// Create a new file server with a single anonymous root (name will be "") and + /// qid of [QID_ROOT]. + pub fn new(s: S) -> Self { + Self::new_with_roots(s, [("".to_string(), QID_ROOT)].into_iter().collect()) + } + + /// Create a new file server with the given roots for clients to attach to. + pub fn new_with_roots(s: S, roots: BTreeMap) -> Self { + let qids = roots + .iter() + .map(|(p, &qid)| (qid, FileMeta::dir(p.clone(), qid))) + .collect(); + + Self { + s: Arc::new(Mutex::new(s)), + msize: MAX_DATA_LEN as u32, + roots, + qids: Arc::new(RwLock::new(qids)), + next_client_id: 0, + } + } + + /// Construct a new unattached [Session] over the provided [Stream] + pub(crate) fn new_session(&mut self, stream: U) -> Session + where + U: Stream, + { + let session = Session::new_unattached( + ClientId(self.next_client_id), + self.msize, + self.roots.clone(), + self.s.clone(), + self.qids.clone(), + stream, + ); + self.next_client_id += 1; + + session + } +} + +/// Marker trait for implementing a type state for Session +pub(crate) trait SessionType: Send {} + +#[derive(Debug, Default)] +pub(crate) struct Unattached { + pub(crate) seen_version: bool, +} + +impl SessionType for Unattached {} + +#[derive(Debug)] +pub(crate) struct Attached { + pub(crate) uname: String, + pub(crate) fids: BTreeMap, +} +impl SessionType for Attached {} + +impl Attached { + fn new(uname: String, root_fid: u32, root_qid: u64) -> Self { + Self { + uname, + fids: [(root_fid, root_qid)].into_iter().collect(), + } + } +} + +/// A connected client session over a given stream +#[derive(Debug)] +pub(crate) struct Session +where + T: SessionType, + U: Stream, +{ + pub(crate) client_id: ClientId, + pub(crate) state: T, + pub(crate) msize: u32, + pub(crate) roots: BTreeMap, + pub(crate) s: Arc>, + pub(crate) qids: Arc>>, + pub(crate) stream: U, +} + +impl Session +where + T: SessionType, + U: Stream, +{ + pub(crate) fn qid(&self, qid: u64) -> Option { + self.qids.read().unwrap().get(&qid).map(|fm| fm.as_qid()) + } + + /// The version request negotiates the protocol version and message size to be used on the + /// connection and initializes the connection for I/O. Tversion must be the first message sent + /// on the 9P connection, and the client cannot issue any further requests until it has + /// received the Rversion reply. The tag should be NOTAG (value (ushort)~0) for a version + /// message. + /// The client suggests a maximum message size, msize, that is the maximum length, in bytes, it + /// will ever generate or expect to receive in a single 9P message. This count includes all 9P + /// protocol data, starting from the size field and extending through the message, but excludes + /// enveloping transport protocols. The server responds with its own maximum, msize, which must + /// be less than or equal to the client’s value. Thenceforth, both sides of the connection must + /// honor this limit. + /// The version string identifies the level of the protocol. The string must always begin with + /// the two characters “9P”. If the server does not understand the client’s version string, it + /// should respond with an Rversion message (not Rerror) with the version string the 7 + /// characters “unknown”. + /// The server may respond with the client’s version string, or a version string identifying an + /// earlier defined protocol version. Currently, the only defined version is the 6 characters + /// “9P2000”. Version strings are defined such that, if the client string contains one or more + /// period characters, the initial substring up to but not including any single period in the + /// version string defines a version of the protocol. After stripping any such period-separated + /// suffix, the server is allowed to respond with a string of the form 9Pnnnn, where nnnn is + /// less than or equal to the digits sent by the client. + /// The client and server will use the protocol version defined by the server’s response for + /// all subsequent communication on the connection. + /// A successful version request initializes the connection. All outstanding I/O on the + /// connection is aborted; all active fids are freed (‘clunked’) automatically. The set of + /// messages between version requests is called a session. + pub(crate) fn handle_version(&mut self, msize: u32, version: String) -> Result { + let server_version = if version != SUPPORTED_VERSION { + UNKNOWN_VERSION + } else { + SUPPORTED_VERSION + }; + + Ok(Rdata::Version { + msize: min(self.msize, msize), + version: server_version.to_string(), + }) + } + + /// If the client does wish to authenticate, it must acquire and validate an afid using an auth + /// message before doing the attach. + /// The auth message contains afid, a new fid to be established for authentication, and the + /// uname and aname that will be those of the following attach message. If the server does not + /// require authentication, it returns Rerror to the Tauth message. + /// If the server does require authentication, it returns aqid defining a file of type QTAUTH + /// (see intro(9P)) that may be read and written (using read and write messages in the usual + /// way) to execute an authentication protocol. That protocol’s definition is not part of 9P + /// itself. + /// Once the protocol is complete, the same afid is presented in the attach message for the + /// user, granting entry. The same validated afid may be used for multiple attach messages with + /// the same uname and aname. + #[allow(unused_variables)] + pub(crate) fn handle_auth(&mut self, afid: u32, uname: String, aname: String) -> Result { + // TODO: handle auth + // let aqid = self.s.lock().unwrap().auth(afid, &uname, &aname)?; + // Ok(Rdata::Auth { aqid }) + + Err(E_AUTH_NOT_REQUIRED.to_string()) + } +} + +impl Session +where + U: Stream, +{ + fn new_unattached( + client_id: ClientId, + msize: u32, + roots: BTreeMap, + s: Arc>, + qids: Arc>>, + stream: U, + ) -> Self { + Self { + client_id, + state: Unattached::default(), + msize, + roots, + s, + qids, + stream, + } + } + + pub(crate) fn into_attached(self, ty: Attached) -> Session { + let Self { + client_id, + msize, + roots, + s, + qids, + stream, + .. + } = self; + + Session::new_attached(client_id, ty, msize, roots, s, qids, stream) + } + + /// The attach message serves as a fresh introduction from a user on the client machine to the + /// server. The message identifies the user (uname) and may select the file tree to access + /// (aname). The afid argument specifies a fid previously established by an auth message. + /// As a result of the attach transaction, the client will have a connection to the root + /// directory of the desired file tree, represented by fid. An error is returned if fid is + /// already in use. The server’s idea of the root of the file tree is represented by the + /// returned qid. + /// + /// 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, defined as + /// (u32int)~0 in . + pub(crate) fn handle_attach( + &mut self, + root_fid: u32, + _afid: u32, + uname: String, + aname: String, + ) -> Result<(Attached, Qid)> { + let root_qid = match self.roots.get(&aname) { + Some(qid) => *qid, + None => return Err(E_UNKNOWN_ROOT.to_string()), + }; + + // TODO: handle checking afids (AFID_NO_AUTH should be accepted if there is no auth) + + let st = Attached::new(uname, root_fid, root_qid); + let aqid = self.qid(root_qid).expect("to have root qid"); + + Ok((st, aqid)) + } +} + +#[derive(Debug)] +pub(crate) enum Either { + L(L), + R(R), +} + +impl Session +where + U: Stream, +{ + fn new_attached( + client_id: ClientId, + state: Attached, + msize: u32, + roots: BTreeMap, + s: Arc>, + qids: Arc>>, + stream: U, + ) -> Self { + Self { + client_id, + state, + msize, + roots, + s, + qids, + stream, + } + } + + pub(crate) fn try_file_meta(&self, fid: u32) -> Result { + let opt = match self.state.fids.get(&fid) { + Some(&qid) => self.qids.read().unwrap().get(&qid).cloned(), + None => None, + }; + + opt.ok_or_else(|| E_UNKNOWN_FID.to_string()) + } + + pub(crate) fn prep_walk( + &mut self, + fid: u32, + new_fid: u32, + wnames: &[String], + ) -> Result> { + if new_fid != fid && self.state.fids.contains_key(&new_fid) { + return Err(E_DUPLICATE_FID.to_string()); + } + + let fm = self.try_file_meta(fid)?; + + if wnames.is_empty() { + self.state.fids.insert(new_fid, fm.qid); + Ok(Either::L(Rdata::Walk { wqids: vec![] })) + } else if matches!(fm.ty, FileType::Regular) { + Err(E_WALK_NON_DIR.to_string()) + } else { + Ok(Either::R(fm)) + } + } + + pub(crate) fn complete_walk( + &mut self, + new_fid: u32, + wqids: Vec, + n_wnames: usize, + ) -> Rdata { + if wqids.len() == n_wnames { + let qid = wqids.last().expect("empty was handled in prep_walk").path; + self.state.fids.insert(new_fid, qid); + } + + Rdata::Walk { wqids } + } +} diff --git a/crates/ninep/src/client.rs b/crates/ninep/src/sync/client.rs similarity index 98% rename from crates/ninep/src/client.rs rename to crates/ninep/src/sync/client.rs index 43e6331..7d083d2 100644 --- a/crates/ninep/src/client.rs +++ b/crates/ninep/src/sync/client.rs @@ -1,8 +1,8 @@ //! A simple 9p client for building out application specific client applications. use crate::{ fs::{Mode, Perm, Stat}, - protocol::{Data, Format9p, RawStat, Rdata, Rmessage, Tdata, Tmessage}, - Stream, + sansio::protocol::{Data, RawStat, Rdata, Rmessage, Tdata, Tmessage}, + sync::{SyncNineP, SyncStream}, }; use std::{ cmp::min, @@ -61,7 +61,7 @@ pub type TcpClient = Client; #[derive(Debug)] pub struct Client where - S: Stream, + S: SyncStream, { /// The shared inner client holding our connection to the server /// @@ -72,7 +72,7 @@ where impl Clone for Client where - S: Stream, + S: SyncStream, { fn clone(&self) -> Self { Self { @@ -85,7 +85,7 @@ where #[derive(Debug)] struct ClientInner where - S: Stream, + S: SyncStream, { stream: S, uname: String, @@ -96,7 +96,7 @@ where impl Drop for ClientInner where - S: Stream, + S: SyncStream, { fn drop(&mut self) { let fids = std::mem::take(&mut self.fids); @@ -108,7 +108,7 @@ where impl ClientInner where - S: Stream, + S: SyncStream, { fn send(&mut self, tag: u16, content: Tdata) -> io::Result { let t = Tmessage { tag, content }; @@ -202,7 +202,7 @@ impl Client { impl Client where - S: Stream, + S: SyncStream, { #[inline] fn inner(&mut self) -> MutexGuard<'_, ClientInner> { @@ -488,7 +488,7 @@ where #[derive(Debug)] pub struct ChunkIter where - S: Stream, + S: SyncStream, { client: Client, fid: u32, @@ -498,7 +498,7 @@ where impl Iterator for ChunkIter where - S: Stream, + S: SyncStream, { type Item = Vec; @@ -523,7 +523,7 @@ where #[derive(Debug)] pub struct ReadLineIter where - S: Stream, + S: SyncStream, { client: Client, buf: Vec, @@ -535,7 +535,7 @@ where impl Iterator for ReadLineIter where - S: Stream, + S: SyncStream, { type Item = String; diff --git a/crates/ninep/src/sync/mod.rs b/crates/ninep/src/sync/mod.rs new file mode 100644 index 0000000..8b0b543 --- /dev/null +++ b/crates/ninep/src/sync/mod.rs @@ -0,0 +1,78 @@ +//! A synchronous implementation of 9p Servers and Clients +use crate::{ + sansio::{ + protocol::{NineP, NinepReader, Rdata, Read9p, Rmessage}, + Stream, + }, + Result, +}; +use std::{ + io::{self, Read, Write}, + net::TcpStream, + os::unix::net::UnixStream, +}; + +pub mod client; +pub mod server; + +/// Synchronous IO support for reading and writing 9p messages +pub trait SyncNineP: NineP { + /// Encode self as bytes for the 9p protocol and write to the given [SyncStream]. + fn write_to(&self, w: &mut W) -> io::Result<()> { + let mut buf = vec![0; self.n_bytes()]; + self.write_bytes(&mut buf) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + + w.write_all(&buf) + } + + /// Decode self from 9p protocol bytes coming from the given [SyncStream]. + #[allow(clippy::uninit_vec)] + fn read_from(r: &mut R) -> io::Result { + let mut nr = NinepReader::Pending(Self::reader()); + let mut buf = Vec::new(); + + loop { + match nr { + NinepReader::Pending(r9) => { + let n = Self::Reader::needs_bytes(&r9); + buf.reserve(n.saturating_sub(buf.len())); + // SAFETY: we've just reserved sufficient capacity + unsafe { buf.set_len(n) }; + r.read_exact(&mut buf)?; + nr = r9.accept_bytes(&buf[0..n])?; + } + + NinepReader::Complete(t) => return Ok(t), + } + } + } +} + +impl SyncNineP for T where T: NineP {} + +/// A [Stream] that makes use of the standard library [Read] and [Write] traits to perform IO +pub trait SyncStream: Stream + Read + Write { + /// Reply to the specified tag with a given Result. Err's will be converted to 9p error + /// messages automatically. + fn reply(&mut self, tag: u16, resp: Result) { + let r: Rmessage = (tag, resp).into(); + let _ = r.write_to(self); + } +} + +impl Stream for UnixStream { + fn try_clone(&self) -> Result { + self.try_clone().map_err(|e| e.to_string()) + } +} + +impl SyncStream for UnixStream {} + +impl Stream for TcpStream { + fn try_clone(&self) -> Result { + self.try_clone().map_err(|e| e.to_string()) + } +} + +impl SyncStream for TcpStream {} diff --git a/crates/ninep/src/server.rs b/crates/ninep/src/sync/server.rs similarity index 59% rename from crates/ninep/src/server.rs rename to crates/ninep/src/sync/server.rs index 24e2eaf..29dd2ec 100644 --- a/crates/ninep/src/server.rs +++ b/crates/ninep/src/sync/server.rs @@ -1,22 +1,30 @@ -//! Traits for implementing a 9p fileserver +//! Synchronous [Server] implementation using [Read][0] and [Write][1]. +//! +//! [0]: std::io::Read +//! [1]: std::io::Write use crate::{ - fs::{FileMeta, FileType, IoUnit, Mode, Perm, Stat, QID_ROOT}, - protocol::{Data, Format9p, Qid, RawStat, Rdata, Tdata, Tmessage, MAX_DATA_LEN}, - Result, Stream, + fs::{FileMeta, FileType, IoUnit, Mode, Perm, Stat}, + sansio::{ + protocol::{Data, RawStat, Rdata, Tdata, Tmessage}, + server::{ + Attached, Either, Session, SessionType, Unattached, E_CREATE_NON_DIR, E_INVALID_OFFSET, + E_NO_VERSION_MESSAGE, E_UNATTACHED, E_UNKNOWN_FID, SUPPORTED_VERSION, + }, + }, + sync::{SyncNineP, SyncStream}, + Result, }; use std::{ - cmp::min, - collections::btree_map::{BTreeMap, Entry}, - env, fs, + collections::btree_map::Entry, + fs, mem::size_of, net::TcpListener, os::unix::net::UnixListener, - sync::{mpsc::Receiver, Arc, Mutex, RwLock}, thread::{spawn, JoinHandle}, }; -/// Marker afid to denode that auth is not required for establishing connections -pub const AFID_NO_AUTH: u32 = u32::MAX; +// re-exports +pub use crate::sansio::server::{socket_dir, socket_path, ClientId, ReadOutcome, Server}; #[derive(Debug)] struct Socket { @@ -30,20 +38,6 @@ impl Drop for Socket { } } -const DEFAULT_DISPLAY_VALUE: &str = ":0"; - -fn socket_dir() -> String { - let uname = env::var("USER").unwrap(); - let display = env::var("DISPLAY").unwrap_or(String::from(DEFAULT_DISPLAY_VALUE)); - format!("/tmp/ns.{uname}.{display}") -} - -/// The unix socket path that will be used for a given server name. -pub fn socket_path(name: &str) -> String { - let socket_dir = socket_dir(); - format!("{socket_dir}/{name}") -} - fn unix_socket(name: &str) -> Socket { let socket_dir = socket_dir(); let _ = fs::create_dir_all(&socket_dir); @@ -64,33 +58,6 @@ fn tcp_socket(port: u16) -> TcpListener { TcpListener::bind(addr).unwrap() } -// Error messages -const E_NO_VERSION_MESSAGE: &str = "first message must be Tversion"; -const E_AUTH_NOT_REQUIRED: &str = "authentication not required"; -const E_DUPLICATE_FID: &str = "duplicate fid"; -const E_UNKNOWN_FID: &str = "unknown fid"; -const E_UNKNOWN_ROOT: &str = "unknown root directory"; -const E_WALK_NON_DIR: &str = "walk in non-directory"; -const E_CREATE_NON_DIR: &str = "create in non-directory"; -const E_INVALID_OFFSET: &str = "invalid offset for read on directory"; - -const UNKNOWN_VERSION: &str = "unknown"; -const SUPPORTED_VERSION: &str = "9P2000"; - -/// An opaque client ID that can be used by server implementations to determine which client a -/// request originated from by comparing equality. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ClientId(u64); - -/// The outcome of a client attempting to [read](Serve9p::read) a given file. -#[derive(Debug)] -pub enum ReadOutcome { - /// The data is immediately available. - Immediate(Vec), - /// No response should be sent until data is received on the provided channel - Blocked(Receiver>), -} - /// A type capable of handling [9p](http://9p.cat-v.org/) requests in order to implement a /// 9p virtual filesystem. The [Server] struct is used to handle the lower level protocal and /// underlying connection, allowing implementers of this trait to focus on the semantics of the @@ -186,45 +153,10 @@ pub trait Serve9p: Send + 'static { fn write_stat(&mut self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()>; } -/// A threaded `9p` server capable of listening on either a TCP socket or UNIX domain socket. -#[derive(Debug)] -pub struct Server -where - S: Serve9p, -{ - s: Arc>, - msize: u32, - roots: BTreeMap, - qids: Arc>>, - next_client_id: u64, -} - impl Server where S: Serve9p, { - /// Create a new file server with a single anonymous root (name will be "") and - /// qid of [QID_ROOT]. - pub fn new(s: S) -> Self { - Self::new_with_roots(s, [("".to_string(), QID_ROOT)].into_iter().collect()) - } - - /// Create a new file server with the given roots for clients to attach to. - pub fn new_with_roots(s: S, roots: BTreeMap) -> Self { - let qids = roots - .iter() - .map(|(p, &qid)| (qid, FileMeta::dir(p.clone(), qid))) - .collect(); - - Self { - s: Arc::new(Mutex::new(s)), - msize: MAX_DATA_LEN as u32, - roots, - qids: Arc::new(RwLock::new(qids)), - next_client_id: 0, - } - } - /// Bind this server to the specified port and serve over a tcp socket. pub fn serve_tcp(mut self, port: u16) -> JoinHandle<()> { spawn(move || { @@ -232,16 +164,7 @@ where for stream in listener.incoming() { let stream = stream.unwrap(); - let session = Session::new_unattached( - ClientId(self.next_client_id), - self.msize, - self.roots.clone(), - self.s.clone(), - self.qids.clone(), - stream, - ); - - self.next_client_id += 1; + let session = self.new_session(stream); spawn(move || session.handle_connection()); } }) @@ -256,178 +179,29 @@ where for stream in sock.listener.incoming() { let stream = stream.unwrap(); - let session = Session::new_unattached( - ClientId(self.next_client_id), - self.msize, - self.roots.clone(), - self.s.clone(), - self.qids.clone(), - stream, - ); - - self.next_client_id += 1; + let session = self.new_session(stream); spawn(move || session.handle_connection()); } }) } } -/// Marker trait for implementing a type state for Session -trait SessionType: Send {} - -#[derive(Debug, Default)] -struct Unattached { - seen_version: bool, -} - -impl SessionType for Unattached {} - -#[derive(Debug)] -struct Attached { - uname: String, - fids: BTreeMap, -} -impl SessionType for Attached {} - -impl Attached { - fn new(uname: String, root_fid: u32, root_qid: u64) -> Self { - Self { - uname, - fids: [(root_fid, root_qid)].into_iter().collect(), - } - } -} - -#[derive(Debug)] -struct Session -where - T: SessionType, - S: Serve9p, - U: Stream, -{ - client_id: ClientId, - state: T, - msize: u32, - roots: BTreeMap, - s: Arc>, - qids: Arc>>, - stream: U, -} - impl Session where T: SessionType, S: Serve9p, - U: Stream, + U: SyncStream, { - fn qid(&self, qid: u64) -> Option { - self.qids.read().unwrap().get(&qid).map(|fm| fm.as_qid()) - } - fn reply(&mut self, tag: u16, resp: Result) { self.stream.reply(tag, resp); } - - /// The version request negotiates the protocol version and message size to be used on the - /// connection and initializes the connection for I/O. Tversion must be the first message sent - /// on the 9P connection, and the client cannot issue any further requests until it has - /// received the Rversion reply. The tag should be NOTAG (value (ushort)~0) for a version - /// message. - /// The client suggests a maximum message size, msize, that is the maximum length, in bytes, it - /// will ever generate or expect to receive in a single 9P message. This count includes all 9P - /// protocol data, starting from the size field and extending through the message, but excludes - /// enveloping transport protocols. The server responds with its own maximum, msize, which must - /// be less than or equal to the client’s value. Thenceforth, both sides of the connection must - /// honor this limit. - /// The version string identifies the level of the protocol. The string must always begin with - /// the two characters “9P”. If the server does not understand the client’s version string, it - /// should respond with an Rversion message (not Rerror) with the version string the 7 - /// characters “unknown”. - /// The server may respond with the client’s version string, or a version string identifying an - /// earlier defined protocol version. Currently, the only defined version is the 6 characters - /// “9P2000”. Version strings are defined such that, if the client string contains one or more - /// period characters, the initial substring up to but not including any single period in the - /// version string defines a version of the protocol. After stripping any such period-separated - /// suffix, the server is allowed to respond with a string of the form 9Pnnnn, where nnnn is - /// less than or equal to the digits sent by the client. - /// The client and server will use the protocol version defined by the server’s response for - /// all subsequent communication on the connection. - /// A successful version request initializes the connection. All outstanding I/O on the - /// connection is aborted; all active fids are freed (‘clunked’) automatically. The set of - /// messages between version requests is called a session. - fn handle_version(&mut self, msize: u32, version: String) -> Result { - let server_version = if version != SUPPORTED_VERSION { - UNKNOWN_VERSION - } else { - SUPPORTED_VERSION - }; - - Ok(Rdata::Version { - msize: min(self.msize, msize), - version: server_version.to_string(), - }) - } - - /// If the client does wish to authenticate, it must acquire and validate an afid using an auth - /// message before doing the attach. - /// The auth message contains afid, a new fid to be established for authentication, and the - /// uname and aname that will be those of the following attach message. If the server does not - /// require authentication, it returns Rerror to the Tauth message. - /// If the server does require authentication, it returns aqid defining a file of type QTAUTH - /// (see intro(9P)) that may be read and written (using read and write messages in the usual - /// way) to execute an authentication protocol. That protocol’s definition is not part of 9P - /// itself. - /// Once the protocol is complete, the same afid is presented in the attach message for the - /// user, granting entry. The same validated afid may be used for multiple attach messages with - /// the same uname and aname. - #[allow(unused_variables)] - fn handle_auth(&mut self, afid: u32, uname: String, aname: String) -> Result { - // TODO: handle auth - // let aqid = self.s.lock().unwrap().auth(afid, &uname, &aname)?; - // Ok(Rdata::Auth { aqid }) - - Err(E_AUTH_NOT_REQUIRED.to_string()) - } } impl Session where S: Serve9p, - U: Stream, + U: SyncStream, { - fn new_unattached( - client_id: ClientId, - msize: u32, - roots: BTreeMap, - s: Arc>, - qids: Arc>>, - stream: U, - ) -> Self { - Self { - client_id, - state: Unattached::default(), - msize, - roots, - s, - qids, - stream, - } - } - - fn into_attached(self, ty: Attached) -> Session { - let Self { - client_id, - msize, - roots, - s, - qids, - stream, - .. - } = self; - - Session::new_attached(client_id, ty, msize, roots, s, qids, stream) - } - fn handle_connection(mut self) { use Tdata::*; @@ -478,79 +252,19 @@ where return self.into_attached(st).handle_connection(); } - _ => Err("session is unattached".into()), + _ => Err(E_UNATTACHED.into()), }; self.reply(tag, resp); } } - - /// The attach message serves as a fresh introduction from a user on the client machine to the - /// server. The message identifies the user (uname) and may select the file tree to access - /// (aname). The afid argument specifies a fid previously established by an auth message. - /// As a result of the attach transaction, the client will have a connection to the root - /// directory of the desired file tree, represented by fid. An error is returned if fid is - /// already in use. The server’s idea of the root of the file tree is represented by the - /// returned qid. - /// - /// 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, defined as - /// (u32int)~0 in . - fn handle_attach( - &mut self, - root_fid: u32, - _afid: u32, - uname: String, - aname: String, - ) -> Result<(Attached, Qid)> { - let root_qid = match self.roots.get(&aname) { - Some(qid) => *qid, - None => return Err(E_UNKNOWN_ROOT.to_string()), - }; - - // TODO: handle checking afids (AFID_NO_AUTH should be accepted if there is no auth) - - let st = Attached::new(uname, root_fid, root_qid); - let aqid = self.qid(root_qid).expect("to have root qid"); - - Ok((st, aqid)) - } -} - -macro_rules! file_meta { - ($self:expr, $fid:expr) => { - match $self.state.fids.get(&$fid) { - Some(&qid) => $self.qids.read().unwrap().get(&qid).cloned(), - None => None, - } - }; } impl Session where S: Serve9p, - U: Stream, + U: SyncStream, { - fn new_attached( - client_id: ClientId, - state: Attached, - msize: u32, - roots: BTreeMap, - s: Arc>, - qids: Arc>>, - stream: U, - ) -> Self { - Self { - client_id, - state, - msize, - roots, - s, - qids, - stream, - } - } - /// Explicitly clunk all fn clunk_and_clear(&mut self) { let mut guard = self.s.lock().unwrap(); @@ -656,22 +370,11 @@ where /// this restriction, the system imposes no limit on the number of elements in a file name, /// only the number that may be transmitted in a single message. fn handle_walk(&mut self, fid: u32, new_fid: u32, wnames: Vec) -> Result { - if new_fid != fid && self.state.fids.contains_key(&new_fid) { - return Err(E_DUPLICATE_FID.to_string()); - } - - let fm = match file_meta!(self, fid) { - Some(fm) => fm, - None => return Err(E_UNKNOWN_FID.to_string()), + let fm = match self.prep_walk(fid, new_fid, &wnames)? { + Either::L(rdata) => return Ok(rdata), + Either::R(fm) => fm, }; - if wnames.is_empty() { - self.state.fids.insert(new_fid, fm.qid); - return Ok(Rdata::Walk { wqids: vec![] }); - } else if matches!(fm.ty, FileType::Regular) { - return Err(E_WALK_NON_DIR.to_string()); - } - let mut wqids = Vec::with_capacity(wnames.len()); let mut s = self.s.lock().unwrap(); let mut qids = self.qids.write().unwrap(); @@ -687,16 +390,9 @@ where drop(s); drop(qids); - if wqids.len() == wnames.len() { - let qid = wqids.last().expect("empty was handled above").path; - self.state.fids.insert(new_fid, qid); - } - - Ok(Rdata::Walk { wqids }) + Ok(self.complete_walk(new_fid, wqids, wnames.len())) } - /// If a client clunks a fid we simply remove it from the list of fids - /// we have for them fn handle_clunk(&mut self, fid: u32) -> Result { match self.state.fids.entry(fid) { Entry::Occupied(ent) => { @@ -710,10 +406,7 @@ where } fn handle_stat(&mut self, fid: u32) -> Result { - let fm = match file_meta!(self, fid) { - Some(fm) => fm, - None => return Err(E_UNKNOWN_FID.to_string()), - }; + let fm = self.try_file_meta(fid)?; let s = self .s .lock() @@ -727,11 +420,7 @@ where fn handle_wstat(&mut self, fid: u32, raw_stat: RawStat) -> Result { let stat: Stat = raw_stat.try_into()?; - let fm = match file_meta!(self, fid) { - Some(fm) => fm, - None => return Err(E_UNKNOWN_FID.to_string()), - }; - + let fm = self.try_file_meta(fid)?; self.s .lock() .unwrap() @@ -741,10 +430,7 @@ where } fn handle_open(&mut self, fid: u32, mode: Mode) -> Result { - let fm = match file_meta!(self, fid) { - Some(fm) => fm, - None => return Err(E_UNKNOWN_FID.to_string()), - }; + let fm = self.try_file_meta(fid)?; let iounit = self.s .lock() @@ -758,10 +444,7 @@ where } fn handle_create(&mut self, fid: u32, name: String, perm: Perm, mode: Mode) -> Result { - let fm = match file_meta!(self, fid) { - Some(fm) => fm, - None => return Err(E_UNKNOWN_FID.to_string()), - }; + let fm = self.try_file_meta(fid)?; if fm.ty != FileType::Directory { return Err(E_CREATE_NON_DIR.to_string()); } @@ -803,11 +486,7 @@ where ) -> Result> { use FileType::*; - let fm = match file_meta!(self, fid) { - Some(fm) => fm, - None => return Err(E_UNKNOWN_FID.to_string()), - }; - + let fm = self.try_file_meta(fid)?; if offset > u32::MAX as u64 { return Err(format!("offset too large: {offset} > {}", u32::MAX)); } @@ -882,11 +561,7 @@ where } fn handle_write(&mut self, fid: u32, offset: u64, data: Vec) -> Result { - let fm = match file_meta!(self, fid) { - Some(fm) => fm, - None => return Err(E_UNKNOWN_FID.to_string()), - }; - + let fm = self.try_file_meta(fid)?; if offset > u32::MAX as u64 { return Err(format!("offset too large: {offset} > {}", u32::MAX)); } @@ -903,10 +578,7 @@ where } fn handle_remove(&mut self, fid: u32) -> Result { - let fm = match file_meta!(self, fid) { - Some(fm) => fm, - None => return Err(E_UNKNOWN_FID.to_string()), - }; + let fm = self.try_file_meta(fid)?; self.s .lock() diff --git a/src/fsys/buffer.rs b/src/fsys/buffer.rs index 6dfaeee..745822e 100644 --- a/src/fsys/buffer.rs +++ b/src/fsys/buffer.rs @@ -9,7 +9,7 @@ use crate::{ }, input::Event, }; -use ninep::{fs::Stat, server::ReadOutcome}; +use ninep::{fs::Stat, sansio::server::ReadOutcome}; use std::{ collections::BTreeMap, sync::mpsc::{channel, Receiver, Sender}, diff --git a/src/fsys/event.rs b/src/fsys/event.rs index 964433b..bffe745 100644 --- a/src/fsys/event.rs +++ b/src/fsys/event.rs @@ -8,7 +8,7 @@ use crate::{ input::Event, }; use ad_event::{FsysEvent, Kind, Source}; -use ninep::server::ReadOutcome; +use ninep::sansio::server::ReadOutcome; use std::{ sync::mpsc::{channel, Receiver, Sender}, thread::spawn, diff --git a/src/fsys/log.rs b/src/fsys/log.rs index 4b75e7b..5e39b90 100644 --- a/src/fsys/log.rs +++ b/src/fsys/log.rs @@ -1,4 +1,4 @@ -use ninep::server::{ClientId, ReadOutcome}; +use ninep::sansio::server::{ClientId, ReadOutcome}; use std::{ collections::HashMap, mem::swap, diff --git a/src/fsys/mod.rs b/src/fsys/mod.rs index 7f63d58..89336c3 100644 --- a/src/fsys/mod.rs +++ b/src/fsys/mod.rs @@ -30,7 +30,7 @@ use crate::{config_handle, editor::Action, input::Event}; use ninep::{ fs::{FileMeta, IoUnit, Mode, Perm, Stat}, - server::{socket_path, ClientId, ReadOutcome, Serve9p, Server}, + sync::server::{socket_path, ClientId, ReadOutcome, Serve9p, Server}, Result, }; use std::{ From 96a53d200bb63897c6581a71e342fff80131dfa4 Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Mon, 24 Feb 2025 14:13:58 +0000 Subject: [PATCH 03/12] getting the async client and server working --- Cargo.lock | 382 ++++++++++++---- crates/ninep/Cargo.toml | 6 + crates/ninep/examples/async_client.rs | 33 ++ crates/ninep/examples/async_server.rs | 318 +++++++++++++ crates/ninep/examples/client.rs | 2 +- crates/ninep/examples/server.rs | 63 +-- crates/ninep/src/lib.rs | 4 +- crates/ninep/src/sansio/mod.rs | 9 - crates/ninep/src/sansio/protocol.rs | 8 +- crates/ninep/src/sansio/server.rs | 54 +-- crates/ninep/src/sync/mod.rs | 18 +- crates/ninep/src/sync/server.rs | 97 ++-- crates/ninep/src/tokio/client.rs | 599 +++++++++++++++++++++++++ crates/ninep/src/tokio/mod.rs | 64 +++ crates/ninep/src/tokio/server.rs | 618 ++++++++++++++++++++++++++ src/fsys/mod.rs | 268 +++++------ 16 files changed, 2172 insertions(+), 371 deletions(-) create mode 100644 crates/ninep/examples/async_client.rs create mode 100644 crates/ninep/examples/async_server.rs create mode 100644 crates/ninep/src/tokio/client.rs create mode 100644 crates/ninep/src/tokio/mod.rs create mode 100644 crates/ninep/src/tokio/server.rs diff --git a/Cargo.lock b/Cargo.lock index 05d0ee2..1ded306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,21 @@ dependencies = [ "subprocess", ] +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -67,9 +82,20 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "async-trait" +version = "0.1.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "autocfg" @@ -77,6 +103,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -85,15 +126,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "cast" @@ -103,9 +150,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.2" +version = "1.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" +checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" dependencies = [ "shlex", ] @@ -145,18 +192,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" dependencies = [ "anstyle", "clap_lex", @@ -164,9 +211,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "criterion" @@ -206,9 +253,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -225,15 +272,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "either" @@ -243,9 +290,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "fluent-uri" @@ -256,6 +303,12 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "half" version = "2.4.1" @@ -280,9 +333,9 @@ checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", @@ -290,13 +343,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -310,16 +363,17 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -331,9 +385,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.162" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libloading" @@ -345,11 +399,21 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" -version = "0.4.22" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "lsp-types" @@ -379,12 +443,34 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "ninep" version = "0.3.0" dependencies = [ - "bitflags 2.6.0", + "async-trait", + "bitflags 2.8.0", "simple_test_case", + "tokio", ] [[package]] @@ -406,11 +492,20 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "oorandom" @@ -424,11 +519,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "plotters" @@ -460,18 +578,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -496,11 +614,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -510,9 +637,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -531,11 +658,23 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e33e4fb37ba46888052c763e4ec2acfedd8f00f62897b630cadb6298b833675e" +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -546,20 +685,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" -version = "1.0.215" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", @@ -568,9 +713,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" dependencies = [ "itoa", "memchr", @@ -626,9 +771,19 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] [[package]] name = "streaming-iterator" @@ -648,9 +803,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -677,11 +832,39 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", @@ -700,9 +883,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", @@ -713,9 +896,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -724,9 +907,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -735,9 +918,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -756,9 +939,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -770,9 +953,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.24.4" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67baf55e7e1b6806063b1e51041069c90afff16afcbbccd278d899f9d84bca4" +checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75" dependencies = [ "cc", "regex", @@ -783,9 +966,9 @@ dependencies = [ [[package]] name = "tree-sitter-language" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ddffe35a0e5eeeadf13ff7350af564c6e73993a24db62caee1822b185c2600" +checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" [[package]] name = "tree-sitter-python" @@ -809,9 +992,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "unicode-width" @@ -821,9 +1004,9 @@ checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "walkdir" @@ -835,26 +1018,32 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -863,9 +1052,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -873,9 +1062,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -886,15 +1075,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -1015,9 +1207,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" dependencies = [ "memchr", ] diff --git a/crates/ninep/Cargo.toml b/crates/ninep/Cargo.toml index 38180da..7937177 100644 --- a/crates/ninep/Cargo.toml +++ b/crates/ninep/Cargo.toml @@ -13,8 +13,14 @@ include = [ "README.md" ] +[features] +default = ["tokio"] +tokio = ["dep:tokio", "dep:async-trait"] + [dependencies] +async-trait = { version = "0.1.86", optional = true } bitflags = "2.6" +tokio = { version = "1.43.0", features = ["time", "macros", "parking_lot", "rt-multi-thread", "net", "io-util", "sync"], optional = true } [dev-dependencies] simple_test_case = "1" diff --git a/crates/ninep/examples/async_client.rs b/crates/ninep/examples/async_client.rs new file mode 100644 index 0000000..9ef270b --- /dev/null +++ b/crates/ninep/examples/async_client.rs @@ -0,0 +1,33 @@ +//! A simple demo of the 9p client interface +use ninep::{fs::FileType, tokio::client::UnixClient}; +use std::io; + +#[tokio::main] +async fn main() -> io::Result<()> { + let mut client = UnixClient::new_unix("ninep-server", "").await?; + tree(&mut client, "", 0).await?; + + let mut stream = client.stream_lines("blocking").await?; + while let Some(line) = stream.next().await { + println!("{line}"); + } + + Ok(()) +} + +async fn tree(client: &mut UnixClient, path: &str, depth: usize) -> io::Result<()> { + for stat in client.read_dir(path).await? { + let name = stat.fm.name; + println!("{:indent$}{name}", "", indent = depth * 2); + if stat.fm.ty == FileType::Directory { + let child = if path.is_empty() { + name + } else { + format!("{path}/{name}") + }; + Box::pin(tree(client, &child, depth + 1)).await?; + } + } + + Ok(()) +} diff --git a/crates/ninep/examples/async_server.rs b/crates/ninep/examples/async_server.rs new file mode 100644 index 0000000..b7e5683 --- /dev/null +++ b/crates/ninep/examples/async_server.rs @@ -0,0 +1,318 @@ +//! A simple demo of the 9p server interface +//! +//! You can use the `9p` command from https://github.com/9fans/plan9port to interact +//! with the server and test it out. +//! +//! https://9fans.github.io/plan9port/man/man1/9p.html +//! +//! ```sh +//! # Let 9p know where to find the socket we have opened +//! $ export NAMESPACE="/tmp/ns.$USER.$DISPLAY" +//! +//! # List the contents of the filesystem and read the contents of a file +//! $ 9p ls ninep-server +//! $ 9p read ninep-server/foo +//! +//! # List the contents of a subdirectory and a file in that subdirectory +//! $ 9p ls ninep-server/bar +//! $ 9p read ninep-server/bar/baz +//! +//! # Read and then update the contents of a file +//! $ 9p read ninep-server/rw +//! $ echo "updated" | 9p write ninep-server/rw +//! $ 9p read ninep-server/rw +//! ``` +use ninep::{ + fs::{FileMeta, IoUnit, Mode, Perm, Stat}, + tokio::server::{AsyncServe9p, ClientId, ReadOutcome, Server}, + Result, +}; +use std::{ + sync::{Arc, RwLock}, + time::{Duration, SystemTime}, +}; +use tokio::{spawn, sync::mpsc::channel, time::sleep}; + +#[tokio::main] +async fn main() { + let s = Server::new(EchoServer { + state: Arc::new(RwLock::new(State { + rw: "initial".to_string(), + blocking: "0\n".to_string(), + n: 0, + })), + }); + println!("starting server"); + _ = s.serve_socket_async("ninep-server").await; +} + +struct State { + rw: String, + blocking: String, + n: usize, +} + +struct EchoServer { + state: Arc>, +} + +const ROOT: u64 = 0; +const BAR: u64 = 1; +const FOO: u64 = 2; +const BAZ: u64 = 3; +const RW: u64 = 4; +const BLOCKING: u64 = 5; + +#[async_trait::async_trait] +impl AsyncServe9p for EchoServer { + async fn write( + &self, + _cid: ClientId, + qid: u64, + offset: usize, + data: Vec, + _uname: &str, + ) -> Result { + if qid != RW { + return Err(format!("write not supported for {qid} @ {offset}")); + } + + println!("writing data to rw file"); + let s = String::from_utf8(data).unwrap(); + let n = s.len(); + self.state.write().unwrap().rw = s; + + Ok(n) + } + + #[allow(unused_variables)] + async fn create( + &self, + cid: ClientId, + parent: u64, + name: &str, + perm: Perm, + mode: Mode, + uname: &str, + ) -> Result<(FileMeta, IoUnit)> { + Err("create not supported".to_string()) + } + + #[allow(unused_variables)] + async fn remove(&self, cid: ClientId, qid: u64, uname: &str) -> Result<()> { + Err("remove not supported".to_string()) + } + + #[allow(unused_variables)] + async fn write_stat(&self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()> { + Err("write_stat not supported".to_string()) + } + + async fn walk( + &self, + _cid: ClientId, + parent_qid: u64, + child: &str, + _uname: &str, + ) -> Result { + println!("handling walk request: parent={parent_qid} child={child}"); + match (parent_qid, child) { + (ROOT, "bar") => Ok(FileMeta::dir("bar", BAR)), + (ROOT, "foo") => Ok(FileMeta::file("foo", FOO)), + (ROOT, "rw") => Ok(FileMeta::file("rw", RW)), + (ROOT, "blocking") => Ok(FileMeta::file("blocking", BLOCKING)), + (BAR, "baz") => Ok(FileMeta::file("baz", BAZ)), + (qid, child) => Err(format!("unknown child: qid={qid}, child={child}")), + } + } + + async fn stat(&self, _cid: ClientId, qid: u64, uname: &str) -> Result { + println!("handling stat request: qid={qid} uname={uname}"); + match qid { + ROOT => Ok(Stat { + fm: FileMeta::dir("/", ROOT), + perms: Perm::OWNER_READ | Perm::OWNER_EXEC, + n_bytes: 0, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }), + + BAR => Ok(Stat { + fm: FileMeta::dir("bar", BAR), + perms: Perm::OWNER_READ | Perm::OWNER_EXEC, + n_bytes: 0, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }), + + FOO => Ok(Stat { + fm: FileMeta::file("foo", FOO), + perms: Perm::OWNER_READ, + n_bytes: 0, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }), + + BAZ => Ok(Stat { + fm: FileMeta::file("baz", BAZ), + perms: Perm::OWNER_READ, + n_bytes: 0, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }), + + RW => Ok(Stat { + fm: FileMeta::file("rw", BAZ), + perms: Perm::OWNER_READ | Perm::OWNER_WRITE, + n_bytes: self.state.read().unwrap().rw.len() as u64, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }), + + BLOCKING => Ok(Stat { + fm: FileMeta::file("blocking", BLOCKING), + perms: Perm::OWNER_READ, + n_bytes: 0, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }), + + qid => Err(format!("stat for qid={qid}")), + } + } + + async fn open(&self, _cid: ClientId, qid: u64, mode: Mode, uname: &str) -> Result { + println!("handling open request: qid={qid} mode={mode:?} uname={uname}"); + match (qid, mode) { + (FOO | BAZ | RW | BLOCKING, Mode::FILE) => Ok(8168), + (ROOT | BAR, Mode::DIR) => Ok(8168), + (RW, _) => Ok(8168), + (qid, mode) => Err(format!("{qid} is not a known qid (mode={mode:?})")), + } + } + + async fn read( + &self, + _cid: ClientId, + qid: u64, + offset: usize, + count: usize, + uname: &str, + ) -> Result { + println!("handling read request: qid={qid} offset={offset} count={count} uname={uname}"); + let chunk = |s: &str| { + s.as_bytes() + .iter() + .skip(offset) + .take(count) + .copied() + .collect::>() + }; + + let mut s = self.state.write().unwrap(); + + let data = match qid { + FOO => chunk("foo contents\n"), + BAZ => chunk("contents of baz\n"), + RW => chunk(&format!("server state is currently: '{}'", s.rw)), + BLOCKING => { + let (tx, rx) = channel(1); + let data = chunk(&s.blocking); + s.n += 1; + let n_str = s.n.to_string(); + s.blocking.push_str(&n_str); + s.blocking.push('\n'); + + spawn(async move { + sleep(Duration::from_secs(1)).await; + _ = tx.send(data).await; + }); + + return Ok(ReadOutcome::Blocked(rx)); + } + + _ => Vec::new(), + }; + + Ok(ReadOutcome::Immediate(data)) + } + + async fn read_dir(&self, _cid: ClientId, qid: u64, uname: &str) -> Result> { + println!("handling read_dir request: qid={qid} uname={uname}"); + match qid { + ROOT => Ok(vec![ + Stat { + fm: FileMeta::dir("bar", BAR), + perms: Perm::OWNER_READ | Perm::OWNER_EXEC, + n_bytes: 0, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }, + Stat { + fm: FileMeta::file("foo", FOO), + perms: Perm::OWNER_READ, + n_bytes: 42, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }, + Stat { + fm: FileMeta::file("rw", RW), + perms: Perm::OWNER_READ | Perm::OWNER_WRITE, + n_bytes: self.state.read().unwrap().rw.len() as u64, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }, + Stat { + fm: FileMeta::file("blocking", BLOCKING), + perms: Perm::OWNER_READ, + n_bytes: 0, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }, + ]), + + BAR => Ok(vec![Stat { + fm: FileMeta::file("baz", BAZ), + perms: Perm::OWNER_READ | Perm::OWNER_WRITE, + n_bytes: 0, + last_accesses: SystemTime::now(), + last_modified: SystemTime::now(), + owner: uname.into(), + group: uname.into(), + last_modified_by: uname.into(), + }]), + + s => Err(format!("unknown dir: '{s}'")), + } + } +} diff --git a/crates/ninep/examples/client.rs b/crates/ninep/examples/client.rs index 042036c..c88a70a 100644 --- a/crates/ninep/examples/client.rs +++ b/crates/ninep/examples/client.rs @@ -7,7 +7,7 @@ fn main() -> io::Result<()> { tree(&mut client, "", 0)?; for line in client.iter_lines("blocking")? { - print!("{line}"); + println!("{line}"); } Ok(()) diff --git a/crates/ninep/examples/server.rs b/crates/ninep/examples/server.rs index 1fc56ac..71f7ed9 100644 --- a/crates/ninep/examples/server.rs +++ b/crates/ninep/examples/server.rs @@ -28,27 +28,33 @@ use ninep::{ Result, }; use std::{ - sync::mpsc::channel, + sync::{mpsc::channel, Arc, RwLock}, thread::{sleep, spawn}, time::{Duration, SystemTime}, }; fn main() { let s = Server::new(EchoServer { - rw: "initial".to_string(), - blocking: "0\n".to_string(), - n: 0, + state: Arc::new(RwLock::new(State { + rw: "initial".to_string(), + blocking: "0\n".to_string(), + n: 0, + })), }); println!("starting server"); _ = s.serve_socket("ninep-server").join(); } -struct EchoServer { +struct State { rw: String, blocking: String, n: usize, } +struct EchoServer { + state: Arc>, +} + const ROOT: u64 = 0; const BAR: u64 = 1; const FOO: u64 = 2; @@ -58,7 +64,7 @@ const BLOCKING: u64 = 5; impl Serve9p for EchoServer { fn write( - &mut self, + &self, _cid: ClientId, qid: u64, offset: usize, @@ -70,14 +76,16 @@ impl Serve9p for EchoServer { } println!("writing data to rw file"); - self.rw = String::from_utf8(data).unwrap(); + let s = String::from_utf8(data).unwrap(); + let n = s.len(); + self.state.write().unwrap().rw = s; - Ok(self.rw.len()) + Ok(n) } #[allow(unused_variables)] fn create( - &mut self, + &self, cid: ClientId, parent: u64, name: &str, @@ -89,22 +97,16 @@ impl Serve9p for EchoServer { } #[allow(unused_variables)] - fn remove(&mut self, cid: ClientId, qid: u64, uname: &str) -> Result<()> { + fn remove(&self, cid: ClientId, qid: u64, uname: &str) -> Result<()> { Err("remove not supported".to_string()) } #[allow(unused_variables)] - fn write_stat(&mut self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()> { + fn write_stat(&self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()> { Err("write_stat not supported".to_string()) } - fn walk( - &mut self, - _cid: ClientId, - parent_qid: u64, - child: &str, - _uname: &str, - ) -> Result { + fn walk(&self, _cid: ClientId, parent_qid: u64, child: &str, _uname: &str) -> Result { println!("handling walk request: parent={parent_qid} child={child}"); match (parent_qid, child) { (ROOT, "bar") => Ok(FileMeta::dir("bar", BAR)), @@ -116,7 +118,7 @@ impl Serve9p for EchoServer { } } - fn stat(&mut self, _cid: ClientId, qid: u64, uname: &str) -> Result { + fn stat(&self, _cid: ClientId, qid: u64, uname: &str) -> Result { println!("handling stat request: qid={qid} uname={uname}"); match qid { ROOT => Ok(Stat { @@ -166,7 +168,7 @@ impl Serve9p for EchoServer { RW => Ok(Stat { fm: FileMeta::file("rw", BAZ), perms: Perm::OWNER_READ | Perm::OWNER_WRITE, - n_bytes: self.rw.len() as u64, + n_bytes: self.state.read().unwrap().rw.len() as u64, last_accesses: SystemTime::now(), last_modified: SystemTime::now(), owner: uname.into(), @@ -189,7 +191,7 @@ impl Serve9p for EchoServer { } } - fn open(&mut self, _cid: ClientId, qid: u64, mode: Mode, uname: &str) -> Result { + fn open(&self, _cid: ClientId, qid: u64, mode: Mode, uname: &str) -> Result { println!("handling open request: qid={qid} mode={mode:?} uname={uname}"); match (qid, mode) { (FOO | BAZ | RW | BLOCKING, Mode::FILE) => Ok(8168), @@ -200,7 +202,7 @@ impl Serve9p for EchoServer { } fn read( - &mut self, + &self, _cid: ClientId, qid: u64, offset: usize, @@ -217,16 +219,19 @@ impl Serve9p for EchoServer { .collect::>() }; + let mut s = self.state.write().unwrap(); + let data = match qid { FOO => chunk("foo contents\n"), BAZ => chunk("contents of baz\n"), - RW => chunk(&format!("server state is currently: '{}'", self.rw)), + RW => chunk(&format!("server state is currently: '{}'", s.rw)), BLOCKING => { let (tx, rx) = channel(); - let data = chunk(&self.blocking); - self.n += 1; - self.blocking.push_str(&self.n.to_string()); - self.blocking.push('\n'); + let data = chunk(&s.blocking); + s.n += 1; + let n_str = s.n.to_string(); + s.blocking.push_str(&n_str); + s.blocking.push('\n'); spawn(move || { sleep(Duration::from_secs(1)); @@ -242,7 +247,7 @@ impl Serve9p for EchoServer { Ok(ReadOutcome::Immediate(data)) } - fn read_dir(&mut self, _cid: ClientId, qid: u64, uname: &str) -> Result> { + fn read_dir(&self, _cid: ClientId, qid: u64, uname: &str) -> Result> { println!("handling read_dir request: qid={qid} uname={uname}"); match qid { ROOT => Ok(vec![ @@ -269,7 +274,7 @@ impl Serve9p for EchoServer { Stat { fm: FileMeta::file("rw", RW), perms: Perm::OWNER_READ | Perm::OWNER_WRITE, - n_bytes: self.rw.len() as u64, + n_bytes: self.state.read().unwrap().rw.len() as u64, last_accesses: SystemTime::now(), last_modified: SystemTime::now(), owner: uname.into(), diff --git a/crates/ninep/src/lib.rs b/crates/ninep/src/lib.rs index 8826117..751cc11 100644 --- a/crates/ninep/src/lib.rs +++ b/crates/ninep/src/lib.rs @@ -14,9 +14,7 @@ pub mod fs; pub mod sansio; pub mod sync; -// pub mod client; -// pub mod protocol; -// pub mod server; +pub mod tokio; /// A simple result type for errors returned from this crate pub type Result = std::result::Result; diff --git a/crates/ninep/src/sansio/mod.rs b/crates/ninep/src/sansio/mod.rs index 0c7fb48..e71a6aa 100644 --- a/crates/ninep/src/sansio/mod.rs +++ b/crates/ninep/src/sansio/mod.rs @@ -17,12 +17,3 @@ impl From<(u16, Result)> for Rmessage { } } } - -// TODO: pull up as much as possible into this trait - -/// An underlying stream over which we can handle 9p connections -pub trait Stream: Send + Sized + 'static { - /// The underlying try_clone implementations for file descriptors can fail at the libc level so - /// we need to account for that here. - fn try_clone(&self) -> Result; -} diff --git a/crates/ninep/src/sansio/protocol.rs b/crates/ninep/src/sansio/protocol.rs index 24c9fd6..1fa997a 100644 --- a/crates/ninep/src/sansio/protocol.rs +++ b/crates/ninep/src/sansio/protocol.rs @@ -60,7 +60,7 @@ pub trait NineP: Sized { } /// A paired helper type for decoding a [Format9p] type from a bytestream. -pub trait Read9p: Sized { +pub trait Read9p: Sized + Send { /// The parent [Format9p] type being decoded into type T: NineP; @@ -237,7 +237,7 @@ impl Read9p for StringReader { // From [INTRO(5)](http://man.cat-v.org/plan_9/5/intro): // Data items of larger or variable lengths are represented by a two-byte field specifying // a count, n, followed by n bytes of data. -impl NineP for Vec { +impl NineP for Vec { type Reader = VecReader; fn n_bytes(&self) -> usize { @@ -270,13 +270,13 @@ impl NineP for Vec { #[allow(missing_docs)] pub enum VecReader where - T: NineP, + T: NineP + fmt::Debug + Send, { Start, Reading(usize, T::Reader, Vec), } -impl Read9p for VecReader { +impl Read9p for VecReader { type T = Vec; fn needs_bytes(&self) -> usize { diff --git a/crates/ninep/src/sansio/server.rs b/crates/ninep/src/sansio/server.rs index 475adc6..78a1601 100644 --- a/crates/ninep/src/sansio/server.rs +++ b/crates/ninep/src/sansio/server.rs @@ -1,17 +1,14 @@ //! Traits and structs for implementing a 9p fileserver use crate::{ fs::{FileMeta, FileType, QID_ROOT}, - sansio::{ - protocol::{Qid, Rdata, MAX_DATA_LEN}, - Stream, - }, + sansio::protocol::{Qid, Rdata, MAX_DATA_LEN}, Result, }; use std::{ cmp::min, collections::btree_map::BTreeMap, env, - sync::{mpsc::Receiver, Arc, Mutex, RwLock}, + sync::{mpsc::Receiver, Arc}, }; /// Marker afid to denode that auth is not required for establishing connections @@ -63,15 +60,21 @@ pub enum ReadOutcome { /// A 9p server wrapping an `S` that must implement an IO specific handler trait to provide the /// actual filesystem implementation. #[derive(Debug)] -pub struct Server { - pub(crate) s: Arc>, +pub struct Server +where + S: Send, +{ + pub(crate) s: Arc, pub(crate) msize: u32, pub(crate) roots: BTreeMap, - pub(crate) qids: Arc>>, + pub(crate) qids: BTreeMap, pub(crate) next_client_id: u64, } -impl Server { +impl Server +where + S: Send, +{ /// Create a new file server with a single anonymous root (name will be "") and /// qid of [QID_ROOT]. pub fn new(s: S) -> Self { @@ -86,19 +89,16 @@ impl Server { .collect(); Self { - s: Arc::new(Mutex::new(s)), + s: Arc::new(s), msize: MAX_DATA_LEN as u32, roots, - qids: Arc::new(RwLock::new(qids)), + qids, next_client_id: 0, } } /// Construct a new unattached [Session] over the provided [Stream] - pub(crate) fn new_session(&mut self, stream: U) -> Session - where - U: Stream, - { + pub(crate) fn new_session(&mut self, stream: U) -> Session { let session = Session::new_unattached( ClientId(self.next_client_id), self.msize, @@ -144,24 +144,24 @@ impl Attached { pub(crate) struct Session where T: SessionType, - U: Stream, + S: Send, { pub(crate) client_id: ClientId, pub(crate) state: T, pub(crate) msize: u32, pub(crate) roots: BTreeMap, - pub(crate) s: Arc>, - pub(crate) qids: Arc>>, + pub(crate) s: Arc, + pub(crate) qids: BTreeMap, pub(crate) stream: U, } impl Session where T: SessionType, - U: Stream, + S: Send, { pub(crate) fn qid(&self, qid: u64) -> Option { - self.qids.read().unwrap().get(&qid).map(|fm| fm.as_qid()) + self.qids.get(&qid).map(|fm| fm.as_qid()) } /// The version request negotiates the protocol version and message size to be used on the @@ -228,14 +228,14 @@ where impl Session where - U: Stream, + S: Send, { fn new_unattached( client_id: ClientId, msize: u32, roots: BTreeMap, - s: Arc>, - qids: Arc>>, + s: Arc, + qids: BTreeMap, stream: U, ) -> Self { Self { @@ -303,15 +303,15 @@ pub(crate) enum Either { impl Session where - U: Stream, + S: Send, { fn new_attached( client_id: ClientId, state: Attached, msize: u32, roots: BTreeMap, - s: Arc>, - qids: Arc>>, + s: Arc, + qids: BTreeMap, stream: U, ) -> Self { Self { @@ -327,7 +327,7 @@ where pub(crate) fn try_file_meta(&self, fid: u32) -> Result { let opt = match self.state.fids.get(&fid) { - Some(&qid) => self.qids.read().unwrap().get(&qid).cloned(), + Some(&qid) => self.qids.get(&qid).cloned(), None => None, }; diff --git a/crates/ninep/src/sync/mod.rs b/crates/ninep/src/sync/mod.rs index 8b0b543..82c6b28 100644 --- a/crates/ninep/src/sync/mod.rs +++ b/crates/ninep/src/sync/mod.rs @@ -1,9 +1,6 @@ //! A synchronous implementation of 9p Servers and Clients use crate::{ - sansio::{ - protocol::{NineP, NinepReader, Rdata, Read9p, Rmessage}, - Stream, - }, + sansio::protocol::{NineP, NinepReader, Rdata, Read9p, Rmessage}, Result, }; use std::{ @@ -52,7 +49,10 @@ pub trait SyncNineP: NineP { impl SyncNineP for T where T: NineP {} /// A [Stream] that makes use of the standard library [Read] and [Write] traits to perform IO -pub trait SyncStream: Stream + Read + Write { +pub trait SyncStream: Read + Write + Send + Sized + 'static { + /// Clone this stream, accounting for operating system errors + fn try_clone(&self) -> Result; + /// Reply to the specified tag with a given Result. Err's will be converted to 9p error /// messages automatically. fn reply(&mut self, tag: u16, resp: Result) { @@ -61,18 +61,14 @@ pub trait SyncStream: Stream + Read + Write { } } -impl Stream for UnixStream { +impl SyncStream for UnixStream { fn try_clone(&self) -> Result { self.try_clone().map_err(|e| e.to_string()) } } -impl SyncStream for UnixStream {} - -impl Stream for TcpStream { +impl SyncStream for TcpStream { fn try_clone(&self) -> Result { self.try_clone().map_err(|e| e.to_string()) } } - -impl SyncStream for TcpStream {} diff --git a/crates/ninep/src/sync/server.rs b/crates/ninep/src/sync/server.rs index 29dd2ec..2472cef 100644 --- a/crates/ninep/src/sync/server.rs +++ b/crates/ninep/src/sync/server.rs @@ -76,9 +76,9 @@ fn tcp_socket(port: u16) -> TcpListener { /// as such, [Serve9p] only needs to worry about maintaining `qids` for resources. /// /// The source code of [Server] is a useful reference for those wanting to learn more. -pub trait Serve9p: Send + 'static { +pub trait Serve9p: Send + Sync + 'static { // #[allow(unused_variables)] - // fn auth(&mut self, afid: u32, uname: &str, aname: &str) -> Result { + // fn auth(&self, afid: u32, uname: &str, aname: &str) -> Result { // Err("authentication not required".to_string()) // } @@ -90,28 +90,22 @@ pub trait Serve9p: Send + 'static { /// /// [Server] will ensure that this method is only called for known parents who have previously /// been identified has having [FileType::Directory]. - fn walk( - &mut self, - cid: ClientId, - parent_qid: u64, - child: &str, - uname: &str, - ) -> Result; + fn walk(&self, cid: ClientId, parent_qid: u64, child: &str, uname: &str) -> Result; /// Open an existing file in the requested mode for subsequent I/O via [read](Serve9p::read) and /// [write](Serve9p::write) calls. /// /// The return of this method is an [IoUnit] used to inform the client of the maximum number of /// bytes that will be supported per read/write call on this resource. - fn open(&mut self, cid: ClientId, qid: u64, mode: Mode, uname: &str) -> Result; + fn open(&self, cid: ClientId, qid: u64, mode: Mode, uname: &str) -> Result; /// Clunk a currently open file. #[allow(unused_variables)] - fn clunk(&mut self, cid: ClientId, qid: u64) {} + fn clunk(&self, cid: ClientId, qid: u64) {} /// Create a new file in the given parent directory. fn create( - &mut self, + &self, cid: ClientId, parent: u64, name: &str, @@ -122,7 +116,7 @@ pub trait Serve9p: Send + 'static { /// Read `count` bytes from the requested file starting from the given `offset`. fn read( - &mut self, + &self, cid: ClientId, qid: u64, offset: usize, @@ -131,11 +125,11 @@ pub trait Serve9p: Send + 'static { ) -> Result; /// List the contents of the given directory. - fn read_dir(&mut self, cid: ClientId, qid: u64, uname: &str) -> Result>; + fn read_dir(&self, cid: ClientId, qid: u64, uname: &str) -> Result>; /// Write the given `data` to the requested file starting at `offset` fn write( - &mut self, + &self, cid: ClientId, qid: u64, offset: usize, @@ -144,13 +138,13 @@ pub trait Serve9p: Send + 'static { ) -> Result; /// Remove the requested file from the filesystem. - fn remove(&mut self, cid: ClientId, qid: u64, uname: &str) -> Result<()>; + fn remove(&self, cid: ClientId, qid: u64, uname: &str) -> Result<()>; /// Request a machine independent "directory entry" for the given resource. - fn stat(&mut self, cid: ClientId, qid: u64, uname: &str) -> Result; + fn stat(&self, cid: ClientId, qid: u64, uname: &str) -> Result; /// Attempt to set the machine independent "directory entry" for the given resource. - fn write_stat(&mut self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()>; + fn write_stat(&self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()>; } impl Server @@ -267,9 +261,8 @@ where { /// Explicitly clunk all fn clunk_and_clear(&mut self) { - let mut guard = self.s.lock().unwrap(); for &qid in self.state.fids.values() { - guard.clunk(self.client_id, qid); + self.s.clunk(self.client_id, qid); } self.state.fids.clear(); } @@ -376,20 +369,15 @@ where }; let mut wqids = Vec::with_capacity(wnames.len()); - let mut s = self.s.lock().unwrap(); - let mut qids = self.qids.write().unwrap(); let mut qid = fm.qid; for name in wnames.iter() { - let fm = s.walk(self.client_id, qid, name, &self.state.uname)?; + let fm = self.s.walk(self.client_id, qid, name, &self.state.uname)?; qid = fm.qid; wqids.push(fm.as_qid()); - qids.insert(qid, fm); + self.qids.insert(qid, fm); } - drop(s); - drop(qids); - Ok(self.complete_walk(new_fid, wqids, wnames.len())) } @@ -397,7 +385,7 @@ where match self.state.fids.entry(fid) { Entry::Occupied(ent) => { let qid = ent.remove(); - self.s.lock().unwrap().clunk(self.client_id, qid); + self.s.clunk(self.client_id, qid); Ok(Rdata::Clunk {}) } @@ -407,11 +395,7 @@ where fn handle_stat(&mut self, fid: u32) -> Result { let fm = self.try_file_meta(fid)?; - let s = self - .s - .lock() - .unwrap() - .stat(self.client_id, fm.qid, &self.state.uname)?; + let s = self.s.stat(self.client_id, fm.qid, &self.state.uname)?; let stat: RawStat = s.into(); let size = stat.size + size_of::() as u16; @@ -422,8 +406,6 @@ where let stat: Stat = raw_stat.try_into()?; let fm = self.try_file_meta(fid)?; self.s - .lock() - .unwrap() .write_stat(self.client_id, fm.qid, stat, &self.state.uname)?; Ok(Rdata::Wstat {}) @@ -431,11 +413,9 @@ where fn handle_open(&mut self, fid: u32, mode: Mode) -> Result { let fm = self.try_file_meta(fid)?; - let iounit = - self.s - .lock() - .unwrap() - .open(self.client_id, fm.qid, mode, &self.state.uname)?; + let iounit = self + .s + .open(self.client_id, fm.qid, mode, &self.state.uname)?; Ok(Rdata::Open { qid: fm.as_qid(), @@ -449,19 +429,14 @@ where return Err(E_CREATE_NON_DIR.to_string()); } - let (fm, iounit) = self.s.lock().unwrap().create( - self.client_id, - fm.qid, - &name, - perm, - mode, - &self.state.uname, - )?; + let (fm, iounit) = + self.s + .create(self.client_id, fm.qid, &name, perm, mode, &self.state.uname)?; // fid is now changed to point to the newly created file rather than the parent let qid = fm.as_qid(); self.state.fids.insert(fid, fm.qid); - self.qids.write().unwrap().entry(fm.qid).or_insert(fm); + self.qids.entry(fm.qid).or_insert(fm); Ok(Rdata::Create { qid, iounit }) } @@ -494,7 +469,7 @@ where let data = match fm.ty { Directory => self.read_dir(fm.qid, offset as usize, count as usize)?, Regular | AppendOnly | Exclusive => { - let outcome = self.s.lock().unwrap().read( + let outcome = self.s.read( self.client_id, fm.qid, offset as usize, @@ -522,22 +497,12 @@ where } fn read_dir(&mut self, qid: u64, offset: usize, count: usize) -> Result> { - let stats = self - .s - .lock() - .unwrap() - .read_dir(self.client_id, qid, &self.state.uname)?; - + let stats = self.s.read_dir(self.client_id, qid, &self.state.uname)?; let mut buf = Vec::with_capacity(count); let mut to_skip = offset; for stat in stats.into_iter() { - self.qids - .write() - .unwrap() - .entry(stat.fm.qid) - .or_insert(stat.fm.clone()); - + self.qids.entry(stat.fm.qid).or_insert(stat.fm.clone()); let rstat: RawStat = stat.into(); let mut tmp = Vec::new(); rstat.write_to(&mut tmp).unwrap(); @@ -566,7 +531,7 @@ where return Err(format!("offset too large: {offset} > {}", u32::MAX)); } - let count = self.s.lock().unwrap().write( + let count = self.s.write( self.client_id, fm.qid, offset as usize, @@ -579,11 +544,7 @@ where fn handle_remove(&mut self, fid: u32) -> Result { let fm = self.try_file_meta(fid)?; - - self.s - .lock() - .unwrap() - .remove(self.client_id, fm.qid, &self.state.uname)?; + self.s.remove(self.client_id, fm.qid, &self.state.uname)?; Ok(Rdata::Remove {}) } diff --git a/crates/ninep/src/tokio/client.rs b/crates/ninep/src/tokio/client.rs new file mode 100644 index 0000000..f5697b9 --- /dev/null +++ b/crates/ninep/src/tokio/client.rs @@ -0,0 +1,599 @@ +//! A simple async 9p client for building out application specific client applications. +use crate::{ + fs::{Mode, Perm, Stat}, + sansio::protocol::{Data, RawStat, Rdata, Rmessage, Tdata, Tmessage}, + tokio::{AsyncNineP, AsyncStream}, +}; +use std::{ + cmp::min, + collections::HashMap, + env, + io::{self, Cursor, ErrorKind}, + mem, + sync::Arc, +}; +use tokio::{ + net::{TcpStream, ToSocketAddrs, UnixStream}, + sync::Mutex, +}; + +// TODO: +// - need a proper error enum rather than just using io::Error + +macro_rules! expect_rmessage { + ($resp:expr, $variant:ident { $($field:ident),+, .. }) => { + match $resp.content { + Rdata::$variant { $($field),+, .. } => ($($field),+), + Rdata::Error { ename } => return err(ename), + m => return err(format!("unexpected response: {m:?}")), + } + + }; + + ($resp:expr, $variant:ident { $($field:ident),+ }) => { + match $resp.content { + Rdata::$variant { $($field),+ } => ($($field),+), + Rdata::Error { ename } => return err(ename), + m => return err(format!("unexpected response: {m:?}")), + } + + }; +} + +const MSIZE: u32 = u16::MAX as u32; +const VERSION: &str = "9P2000"; + +fn err(e: E) -> io::Result +where + E: Into>, +{ + Err(io::Error::new(io::ErrorKind::Other, e)) +} + +/// A client that operates over an underlying [UnixStream]. +pub type UnixClient = Client; + +/// A client that operates over an underlying [TcpStream]. +pub type TcpClient = Client; + +/// A 9p client. +/// +/// Support for each of the operations exposed by this client is determined by the server +/// implementation that it is connected to. +#[derive(Debug)] +pub struct Client +where + S: AsyncStream, +{ + /// The shared inner client holding our connection to the server + /// + /// Shared between clones + inner: Arc>>, + msize: u32, +} + +impl Clone for Client +where + S: AsyncStream, +{ + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + msize: self.msize, + } + } +} + +#[derive(Debug)] +struct ClientInner +where + S: AsyncStream, +{ + stream: S, + uname: String, + msize: u32, + fids: HashMap, + next_fid: u32, +} + +impl ClientInner +where + S: AsyncStream, +{ + async fn send(&mut self, tag: u16, content: Tdata) -> io::Result { + let t = Tmessage { tag, content }; + t.write_to(&mut self.stream).await?; + + match Rmessage::read_from(&mut self.stream).await? { + Rmessage { + content: Rdata::Error { ename }, + .. + } => err(ename), + msg => Ok(msg), + } + } + + fn next_fid(&mut self) -> u32 { + let fid = self.next_fid; + self.next_fid += 1; + + fid + } +} + +impl Client { + /// Create a new [Client] connected to a unix socket at the specified path. + pub async fn new_unix_with_explicit_path( + uname: String, + path: String, + aname: impl Into, + ) -> io::Result { + let stream = UnixStream::connect(path).await?; + let mut fids = HashMap::new(); + fids.insert(String::new(), 0); + + let mut client = Self { + inner: Arc::new(Mutex::new(ClientInner { + stream, + uname, + msize: MSIZE, + fids, + next_fid: 1, + })), + msize: MSIZE, + }; + client.connect(aname).await?; + + Ok(client) + } + + /// Create a new [Client] connected to a unix socket at the given aname under the default + /// namespace. + /// + /// The default namespace is located in /tmp/ns.$USER.$DISPLAY/ + pub async fn new_unix(ns: impl Into, aname: impl Into) -> io::Result { + let ns = ns.into(); + let uname = match env::var("USER") { + Ok(s) => s, + Err(_) => return err("USER env var not set"), + }; + let display = env::var("DISPLAY").unwrap_or(":0".to_string()); + let path = format!("/tmp/ns.{uname}.{display}/{ns}"); + + Self::new_unix_with_explicit_path(uname, path, aname).await + } +} + +impl Client { + /// Create a new [Client] connected to a tcp socket at the specified address. + pub async fn new_tcp(uname: String, addr: T, aname: impl Into) -> io::Result + where + T: ToSocketAddrs, + { + let stream = TcpStream::connect(addr).await?; + let mut fids = HashMap::new(); + fids.insert(String::new(), 0); + + let mut client = Self { + inner: Arc::new(Mutex::new(ClientInner { + stream, + uname, + msize: MSIZE, + fids, + next_fid: 1, + })), + msize: MSIZE, + }; + client.connect(aname).await?; + + Ok(client) + } +} + +impl Client +where + S: AsyncStream, +{ + /// Establish our connection to the target 9p server and begin the session. + async fn connect(&mut self, aname: impl Into) -> io::Result<()> { + let mut inner = self.inner.lock().await; + let resp = inner + .send( + u16::MAX, + Tdata::Version { + msize: MSIZE, + version: VERSION.to_string(), + }, + ) + .await?; + + let (msize, version) = expect_rmessage!(resp, Version { msize, version }); + if version != VERSION { + return err("server version not supported"); + } + inner.msize = msize; + let uname = inner.uname.clone(); + + inner + .send( + 0, + Tdata::Attach { + fid: 0, + afid: u32::MAX, // no auth + uname, + aname: aname.into(), + }, + ) + .await?; + + drop(inner); + self.msize = msize; + + Ok(()) + } + + /// Associate the given path with a new fid. + pub async fn walk(&mut self, path: impl Into) -> io::Result { + let mut inner = self.inner.lock().await; + let path = path.into(); + if let Some(fid) = inner.fids.get(&path) { + return Ok(*fid); + } + + let new_fid = inner.next_fid(); + + inner + .send( + 0, + Tdata::Walk { + fid: 0, + new_fid, + wnames: path.split('/').map(Into::into).collect(), + }, + ) + .await?; + + inner.fids.insert(path, new_fid); + + Ok(new_fid) + } + + /// Free server side state for the given fid. + /// + /// Clunks of the root fid (0) will be ignored + pub async fn clunk(&mut self, fid: u32) -> io::Result<()> { + let mut inner = self.inner.lock().await; + if fid != 0 { + inner.send(0, Tdata::Clunk { fid }).await?; + inner.fids.retain(|_, v| *v != fid); + } + + Ok(()) + } + + /// Free server side state for the given path. + pub async fn clunk_path(&mut self, path: impl Into) -> io::Result<()> { + let fid = match self.inner.lock().await.fids.get(&path.into()) { + Some(fid) => *fid, + None => return Ok(()), + }; + + self.clunk(fid).await + } + + /// Request the current [Stat] of the file or directory identified by the given path. + pub async fn stat(&mut self, path: impl Into) -> io::Result { + let fid = self.walk(path).await?; + let mut inner = self.inner.lock().await; + let resp = inner.send(0, Tdata::Stat { fid }).await?; + let raw_stat = expect_rmessage!(resp, Stat { stat, .. }); + + match raw_stat.try_into() { + Ok(s) => Ok(s), + Err(e) => err(e), + } + } + + async fn _read_count(&mut self, fid: u32, offset: u64, count: u32) -> io::Result> { + let resp = self + .inner + .lock() + .await + .send(0, Tdata::Read { fid, offset, count }) + .await?; + let Data(data) = expect_rmessage!(resp, Read { data }); + + Ok(data) + } + + async fn _read_all(&mut self, path: impl Into, mode: Mode) -> io::Result> { + let fid = self.walk(path).await?; + let mode = mode.bits(); + self.inner + .lock() + .await + .send(0, Tdata::Open { fid, mode }) + .await?; + + let count = self.msize; + let mut bytes = Vec::new(); + let mut offset = 0; + loop { + let data = self._read_count(fid, offset, count).await?; + if data.is_empty() { + break; + } + offset += data.len() as u64; + bytes.extend(data); + } + + Ok(bytes) + } + + /// Read the full contents of the file at `path` as bytes. + pub async fn read(&mut self, path: impl Into) -> io::Result> { + self._read_all(path, Mode::FILE).await + } + + /// Read the full contents of the file at `path` as utf-8 encoded text. + pub async fn read_str(&mut self, path: impl Into) -> io::Result { + let bytes = self._read_all(path, Mode::FILE).await?; + let s = match String::from_utf8(bytes) { + Ok(s) => s, + Err(_) => return err("invalid utf8"), + }; + + Ok(s) + } + + /// Read the directory listing of the directory at `path`. + pub async fn read_dir(&mut self, path: impl Into) -> io::Result> { + let bytes = self._read_all(path, Mode::DIR).await?; + let mut buf = Cursor::new(bytes); + let mut stats: Vec = Vec::new(); + + loop { + match RawStat::read_from(&mut buf).await { + Ok(rs) => match rs.try_into() { + Ok(s) => stats.push(s), + Err(e) => return err(e), + }, + Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, + Err(e) => return Err(e), + } + } + + Ok(stats) + } + + /// Asynchronously iterate over Vec's of bytes from the file at `path`. + /// + /// The size of each chunk is determined by the supported message size of the server replying + /// to the requests and provides no guarantees over the structure of the content of each chunk. + /// + /// The [ChunkStream] returned by this method provides an asynchronous `next` method that can + /// be called to await the next chunk. + pub async fn stream_chunks(&mut self, path: impl Into) -> io::Result> { + let fid = self.walk(path).await?; + let mode = Mode::FILE.bits(); + let count = self.msize; + self.inner + .lock() + .await + .send(0, Tdata::Open { fid, mode }) + .await?; + + Ok(ChunkStream { + client: self.clone(), + fid, + offset: 0, + count, + }) + } + + /// Asynchronously iterate over newline delimited lines of utf-8 encoded text from the file at `path`. + /// + /// The [ReadLineStream] returned by this method provides an asynchronous `next` method that can + /// be called to await the next chunk. + pub async fn stream_lines(&mut self, path: impl Into) -> io::Result> { + let fid = self.walk(path).await?; + let mode = Mode::FILE.bits(); + let count = self.msize; + self.inner + .lock() + .await + .send(0, Tdata::Open { fid, mode }) + .await?; + + Ok(ReadLineStream { + client: self.clone(), + buf: Vec::new(), + fid, + offset: 0, + count, + at_eof: false, + }) + } + + /// Write the provided data to the file at `path` at the given offset. + pub async fn write( + &mut self, + path: impl Into, + mut offset: u64, + content: &[u8], + ) -> io::Result { + let fid = self.walk(path).await?; + let len = content.len(); + let mut cur = 0; + let header_size = 4 + 8 + 4; // fid + offset + data len + let chunk_size = (self.msize - header_size) as usize; + let mut inner = self.inner.lock().await; + + while cur < len { + let end = min(cur + chunk_size, len); + let resp = inner + .send( + 0, + Tdata::Write { + fid, + offset, + data: Data(content[cur..end].to_vec()), + }, + ) + .await?; + let n = expect_rmessage!(resp, Write { count }); + if n == 0 { + break; + } + cur += n as usize; + offset += n as u64; + } + + if cur != len { + return err(format!("partial write: {cur} < {len}")); + } + + Ok(cur) + } + + /// Write the provided string data to the file at `path` at the given offset. + pub async fn write_str( + &mut self, + path: impl Into, + offset: u64, + content: &str, + ) -> io::Result { + self.write(path, offset, content.as_bytes()).await + } + + /// Attempt to create a new file within the connected filesystem. + pub async fn create( + &mut self, + dir: impl Into, + name: impl Into, + perms: Perm, + mode: Mode, + ) -> io::Result<()> { + let fid = self.walk(dir).await?; + self.inner + .lock() + .await + .send( + 0, + Tdata::Create { + fid, + name: name.into(), + perm: perms.bits(), + mode: mode.bits(), + }, + ) + .await?; + + Ok(()) + } + + /// Attempt to remove a file from the connected filesystem. + pub async fn remove(&mut self, path: impl Into) -> io::Result<()> { + let fid = self.walk(path).await?; + self.inner + .lock() + .await + .send(0, Tdata::Remove { fid }) + .await?; + + Ok(()) + } +} + +/// An asynchronous stream of [Vec] chunks out of a given file. +#[derive(Debug)] +pub struct ChunkStream +where + S: AsyncStream, +{ + client: Client, + fid: u32, + offset: u64, + count: u32, +} + +impl ChunkStream +where + S: AsyncStream, +{ + /// Await the next chunk of data out of a file. + pub async fn next(&mut self) -> Option> { + let data = self + .client + ._read_count(self.fid, self.offset, self.count) + .await + .ok()?; + + if data.is_empty() { + _ = self.client.clunk(self.fid); + return None; + } + + self.offset += data.len() as u64; + + Some(data) + } +} + +/// An asynchronous stream of [String] lines out of a given file. +#[derive(Debug)] +pub struct ReadLineStream +where + S: AsyncStream, +{ + client: Client, + buf: Vec, + fid: u32, + offset: u64, + count: u32, + at_eof: bool, +} + +impl ReadLineStream +where + S: AsyncStream, +{ + /// Await the next newline delimited line out of a file. + pub async fn next(&mut self) -> Option { + if self.at_eof { + return None; + } + + loop { + match self.buf.iter().position(|&b| b == b'\n') { + Some(pos) => { + let (raw_line, remaining) = self.buf.split_at(pos + 1); + let mut line = raw_line.to_vec(); + line.pop(); + let s = String::from_utf8(line).ok(); + self.buf = remaining.to_vec(); + return s; + } + + _ => { + let data = self + .client + ._read_count(self.fid, self.offset, self.count) + .await + .ok()?; + + if data.is_empty() { + self.at_eof = true; + if self.buf.is_empty() { + return None; + } + return String::from_utf8(mem::take(&mut self.buf)).ok(); + } + + self.offset += data.len() as u64; + self.buf.extend(data); + } + } + } + } +} diff --git a/crates/ninep/src/tokio/mod.rs b/crates/ninep/src/tokio/mod.rs new file mode 100644 index 0000000..16d2c53 --- /dev/null +++ b/crates/ninep/src/tokio/mod.rs @@ -0,0 +1,64 @@ +//! Tokio based asynchronous implementation of 9p Servers and Clients +use crate::{ + sansio::protocol::{NineP, NinepReader, Rdata, Read9p, Rmessage}, + Result, +}; +use std::{io, marker::Unpin}; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + net::{TcpStream, UnixStream}, +}; + +pub mod client; +pub mod server; + +/// Synchronous IO support for reading and writing 9p messages +#[async_trait::async_trait] +pub trait AsyncNineP: NineP { + /// Encode self as bytes for the 9p protocol and write to the given [SyncStream]. + async fn write_to(&self, w: &mut W) -> io::Result<()> { + let mut buf = vec![0; self.n_bytes()]; + self.write_bytes(&mut buf) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + + w.write_all(&buf).await + } + + /// Decode self from 9p protocol bytes coming from the given [SyncStream]. + #[allow(clippy::uninit_vec)] + async fn read_from(r: &mut R) -> io::Result { + let mut nr = NinepReader::Pending(Self::reader()); + let mut buf = Vec::new(); + + loop { + match nr { + NinepReader::Pending(r9) => { + let n = Self::Reader::needs_bytes(&r9); + buf.reserve(n.saturating_sub(buf.len())); + // SAFETY: we've just reserved sufficient capacity + unsafe { buf.set_len(n) }; + r.read_exact(&mut buf).await?; + nr = r9.accept_bytes(&buf[0..n])?; + } + + NinepReader::Complete(t) => return Ok(t), + } + } + } +} + +impl AsyncNineP for T where T: NineP {} + +/// A [Stream] that makes use of the standard library [Read] and [Write] traits to perform IO +#[allow(async_fn_in_trait)] +pub trait AsyncStream: AsyncRead + AsyncWrite + Unpin + Send + Sized + 'static { + /// Reply to the specified tag with a given Result. Err's will be converted to 9p error + /// messages automatically. + async fn reply(&mut self, tag: u16, resp: Result) { + let r: Rmessage = (tag, resp).into(); + let _ = r.write_to(self).await; + } +} + +impl AsyncStream for UnixStream {} +impl AsyncStream for TcpStream {} diff --git a/crates/ninep/src/tokio/server.rs b/crates/ninep/src/tokio/server.rs new file mode 100644 index 0000000..568ac32 --- /dev/null +++ b/crates/ninep/src/tokio/server.rs @@ -0,0 +1,618 @@ +//! Asynchronous [Server] implementation using tokio's [AsyncRead][0] and [AsyncWrite][1]. +//! +//! [0]: tokio::io::AsyncRead +//! [1]: tokio::io::AsyncWrite +use crate::{ + fs::{FileMeta, FileType, IoUnit, Mode, Perm, Stat}, + sansio::{ + protocol::{Data, RawStat, Rdata, Tdata, Tmessage}, + server::{ + Attached, Either, Session, SessionType, Unattached, E_CREATE_NON_DIR, E_INVALID_OFFSET, + E_NO_VERSION_MESSAGE, E_UNATTACHED, E_UNKNOWN_FID, SUPPORTED_VERSION, + }, + }, + tokio::{AsyncNineP, AsyncStream}, + Result, +}; +use std::{collections::btree_map::Entry, fs, mem::size_of}; +use tokio::{ + net::{TcpListener, UnixListener}, + sync::mpsc::{unbounded_channel, Receiver, UnboundedSender}, + task::{spawn, JoinHandle}, +}; + +// re-exports +pub use crate::sansio::server::{socket_dir, socket_path, ClientId, Server}; + +/// The outcome of a client attempting to [read](Serve9p::read) a given file. +#[derive(Debug)] +pub enum ReadOutcome { + /// The data is immediately available. + Immediate(Vec), + /// No response should be sent until data is received on the provided channel + Blocked(Receiver>), +} + +#[derive(Debug)] +struct Socket { + path: String, + listener: UnixListener, +} + +impl Drop for Socket { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + } +} + +fn unix_socket(name: &str) -> Socket { + let socket_dir = socket_dir(); + let _ = fs::create_dir_all(&socket_dir); + let path = format!("{socket_dir}/{name}"); + + // FIXME: really we should be handling this on exit but we'll need to catch + // ctrl-c to do that properly. For now this works but it means that if you + // start a second file server with the same name then it'll remove the socket + // for the first. + let _ = fs::remove_file(&path); + let listener = UnixListener::bind(&path).unwrap(); + + Socket { path, listener } +} + +async fn tcp_socket(port: u16) -> TcpListener { + let addr = format!("127.0.0.1:{port}"); + TcpListener::bind(addr).await.unwrap() +} + +/// A type capable of handling [9p](http://9p.cat-v.org/) requests in order to implement a +/// 9p virtual filesystem. The [Server] struct is used to handle the lower level protocal and +/// underlying connection, allowing implementers of this trait to focus on the semantics of the +/// virtual filesystem itself. +/// +/// # The 9p protocol +/// Please see [the documentation page on cat-v](http://9p.cat-v.org/documentation/) for an +/// overview of how the protocol works along with various papers covering the original implementation +/// from Bell Labs. For simple filesystems you should be able to get away with referring to the +/// docs on each of the methods for this trait, but you are advised to read through the semantics +/// around permissions and file creation as this is something handled by the trait implementer, not +/// [Server]. +/// +/// ## Client fids and server-side qids +/// [Server] handles establishing and maintaining per-client sessions along with all of their `fids`, +/// as such, [Serve9p] only needs to worry about maintaining `qids` for resources. +/// +/// The source code of [Server] is a useful reference for those wanting to learn more. +#[async_trait::async_trait] +pub trait AsyncServe9p: Send + Sync + 'static { + // #[allow(unused_variables)] + // async fn auth(&self, afid: u32, uname: &str, aname: &str) -> Result { + // Err("authentication not required".to_string()) + // } + + /// Lookup a child node under a known parent directory by name. + /// + /// `9p` Twalk messages received by the server will specify a full path from a known parent + /// to a target child. This method is called for each element of that path in sequence in order, + /// stopping either when the target is reached or some element of the path returns an error. + /// + /// [Server] will ensure that this method is only called for known parents who have previously + /// been identified has having [FileType::Directory]. + async fn walk( + &self, + cid: ClientId, + parent_qid: u64, + child: &str, + uname: &str, + ) -> Result; + + /// Open an existing file in the requested mode for subsequent I/O via [read](Serve9p::read) and + /// [write](Serve9p::write) calls. + /// + /// The return of this method is an [IoUnit] used to inform the client of the maximum number of + /// bytes that will be supported per read/write call on this resource. + async fn open(&self, cid: ClientId, qid: u64, mode: Mode, uname: &str) -> Result; + + /// Clunk a currently open file. + #[allow(unused_variables)] + async fn clunk(&self, cid: ClientId, qid: u64) {} + + /// Create a new file in the given parent directory. + async fn create( + &self, + cid: ClientId, + parent: u64, + name: &str, + perm: Perm, + mode: Mode, + uname: &str, + ) -> Result<(FileMeta, IoUnit)>; + + /// Read `count` bytes from the requested file starting from the given `offset`. + async fn read( + &self, + cid: ClientId, + qid: u64, + offset: usize, + count: usize, + uname: &str, + ) -> Result; + + /// List the contents of the given directory. + async fn read_dir(&self, cid: ClientId, qid: u64, uname: &str) -> Result>; + + /// Write the given `data` to the requested file starting at `offset` + async fn write( + &self, + cid: ClientId, + qid: u64, + offset: usize, + data: Vec, + uname: &str, + ) -> Result; + + /// Remove the requested file from the filesystem. + async fn remove(&self, cid: ClientId, qid: u64, uname: &str) -> Result<()>; + + /// Request a machine independent "directory entry" for the given resource. + async fn stat(&self, cid: ClientId, qid: u64, uname: &str) -> Result; + + /// Attempt to set the machine independent "directory entry" for the given resource. + async fn write_stat(&self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()>; +} + +impl Server +where + S: AsyncServe9p, +{ + /// Bind this server to the specified port and serve over a tcp socket. + pub fn serve_tcp_async(mut self, port: u16) -> JoinHandle<()> { + spawn(async move { + let listener = tcp_socket(port).await; + loop { + if let Ok((stream, _addr)) = listener.accept().await { + let session = self.new_session(stream); + spawn(session.handle_connection_async()); + } + } + }) + } + + /// Bind this server to the specified path and serve over a unix socket. + pub fn serve_socket_async(mut self, socket_name: impl Into) -> JoinHandle<()> { + let socket_name = socket_name.into(); + + spawn(async move { + let sock = unix_socket(&socket_name); + loop { + if let Ok((stream, _addr)) = sock.listener.accept().await { + let session = self.new_session(stream); + spawn(session.handle_connection_async()); + } + } + }) + } +} + +impl Session +where + T: SessionType, + S: AsyncServe9p, + U: AsyncStream, +{ + async fn reply_async(&mut self, tag: u16, resp: Result) { + self.stream.reply(tag, resp).await + } +} + +impl Session +where + S: AsyncServe9p, + U: AsyncStream, +{ + async fn handle_connection_async(mut self) { + use Tdata::*; + + loop { + let t = match Tmessage::read_from(&mut self.stream).await { + Ok(t) => t, + Err(_) => return, + }; + + let Tmessage { tag, content } = t; + + let resp = match content { + Version { msize, version } => { + self.state.seen_version = version == SUPPORTED_VERSION; + self.handle_version(msize, version) + } + + Auth { afid, uname, aname } => { + if !self.state.seen_version { + self.reply_async(tag, Err(E_NO_VERSION_MESSAGE.to_string())) + .await; + continue; + } + + self.handle_auth(afid, uname, aname) + } + + Attach { + fid, + afid, + uname, + aname, + } => { + if !self.state.seen_version { + self.reply_async(tag, Err(E_NO_VERSION_MESSAGE.to_string())) + .await; + continue; + } + + let (st, aqid) = match self.handle_attach(fid, afid, uname, aname) { + Err(e) => { + self.reply_async(tag, Err(e)).await; + continue; + } + + Ok((st, aqid)) => (st, aqid), + }; + + self.reply_async(tag, Ok(Rdata::Attach { aqid })).await; + return self.into_attached(st).handle_connection_async().await; + } + + _ => Err(E_UNATTACHED.into()), + }; + + self.reply_async(tag, resp).await; + } + } +} + +impl Session +where + S: AsyncServe9p, + U: AsyncStream, +{ + /// Explicitly clunk all + async fn clunk_and_clear_async(&mut self) { + for &qid in self.state.fids.values() { + self.s.clunk(self.client_id, qid).await; + } + self.state.fids.clear(); + } + + async fn handle_connection_async(mut self) { + use Tdata::*; + let (tx, mut rx) = unbounded_channel(); + + loop { + let Tmessage { tag, content } = tokio::select! { + // Blocked read came through so send it to the client + Some((tag, data)) = rx.recv() => { + self.stream + .reply(tag, Ok(Rdata::Read { data: Data(data) })) + .await; + continue; + }, + res = Tmessage::read_from(&mut self.stream) => match res { + Ok(t) => t, + Err(_) => return self.clunk_and_clear_async().await, + }, + else => continue, + }; + + let resp = match content { + Version { msize, version } => { + let res = self.handle_version(msize, version); + if res.is_ok() { + self.clunk_and_clear_async().await; + } + + res + } + Auth { .. } | Attach { .. } => Err("session is already attached".into()), + Flush { .. } => Ok(Rdata::Flush {}), + + Walk { + fid, + new_fid, + wnames, + } => self.handle_walk_async(fid, new_fid, wnames).await, + Clunk { fid } => self.handle_clunk_async(fid).await, + Stat { fid } => self.handle_stat_async(fid).await, + Open { fid, mode } => self.handle_open_async(fid, Mode::new(mode)).await, + Create { + fid, + name, + perm, + mode, + } => { + self.handle_create_async(fid, name, Perm::new(perm), Mode::new(mode)) + .await + } + Read { fid, offset, count } => { + match self.handle_read_async(tag, fid, offset, count, &tx).await { + Ok(Some(resp)) => Ok(resp), + Err(err) => Err(err), + Ok(None) => continue, + } + } + Write { fid, offset, data } => self.handle_write_async(fid, offset, data.0).await, + Remove { fid } => self.handle_remove_async(fid).await, + Wstat { fid, stat, .. } => self.handle_wstat_async(fid, stat).await, + }; + + self.reply_async(tag, resp).await; + } + } + + /// The walk request carries as arguments an existing fid and a proposed newfid (which must not + /// be in use unless it is the same as fid) that the client wishes to associate with the result + /// of traversing the directory hierarchy by ‘walking’ the hierarchy using the successive path + /// name elements wname. + /// + /// The fid must represent a directory unless zero path name elements are specified. + /// + /// The fid must be valid in the current session and must not have been opened for I/O by an + /// open or create message. If the full sequence of nwname elements is walked successfully, + /// newfid will represent the file that results. If not, newfid (and fid) will be unaffected. + /// However, if newfid is in use or otherwise illegal, an Rerror is returned. + /// + /// The name “..” (dot-dot) represents the parent directory. The name “.” (dot), meaning the + /// current directory, is not used in the protocol. + /// + /// It is legal for nwname to be zero, in which case newfid will represent the same file as fid + /// and the walk will usually succeed; this is equivalent to walking to dot. The rest of this + /// discussion assumes nwname is greater than zero. + /// + /// The nwname path name elements wname are walked in order, “elementwise”. For the first + /// elementwise walk to succeed, the file identified by fid must be a directory, and the + /// implied user of the request must have permission to search the directory (see intro(9P)). + /// Subsequent elementwise walks have equivalent restrictions applied to the implicit fid that + /// results from the preceding elementwise walk. + /// + /// If the first element cannot be walked for any reason, Rerror is returned. Otherwise, the + /// walk will return an Rwalk message containing nwqid qids corresponding, in order, to the + /// files that are visited by the nwqid successful elementwise walks; nwqid is therefore either + /// nwname or the index of the first elementwise walk that failed. The value of nwqid cannot be + /// zero unless nwname is zero. Also, nwqid will always be less than or equal to nwname. Only + /// if it is equal, however, will newfid be affected, in which case newfid will represent the + /// file reached by the final elementwise walk requested in the message. + /// + /// A walk of the name “..” in the root directory of a server is equivalent to a walk with no + /// name elements. + /// + /// If newfid is the same as fid, the above discussion applies, with the obvious difference + /// that if the walk changes the state of newfid, it also changes the state of fid; and if + /// newfid is unaffected, then fid is also unaffected. + /// + /// To simplify the implementation of the servers, a maximum of sixteen name elements or qids + /// may be packed in a single message. This constant is called MAXWELEM in fcall(3). Despite + /// this restriction, the system imposes no limit on the number of elements in a file name, + /// only the number that may be transmitted in a single message. + async fn handle_walk_async( + &mut self, + fid: u32, + new_fid: u32, + wnames: Vec, + ) -> Result { + let fm = match self.prep_walk(fid, new_fid, &wnames)? { + Either::L(rdata) => return Ok(rdata), + Either::R(fm) => fm, + }; + + let mut wqids = Vec::with_capacity(wnames.len()); + let mut qid = fm.qid; + + for name in wnames.iter() { + let fm = self + .s + .walk(self.client_id, qid, name, &self.state.uname) + .await?; + qid = fm.qid; + wqids.push(fm.as_qid()); + self.qids.insert(qid, fm); + } + + Ok(self.complete_walk(new_fid, wqids, wnames.len())) + } + + async fn handle_clunk_async(&mut self, fid: u32) -> Result { + match self.state.fids.entry(fid) { + Entry::Occupied(ent) => { + let qid = ent.remove(); + self.s.clunk(self.client_id, qid).await; + + Ok(Rdata::Clunk {}) + } + Entry::Vacant(_) => Err(E_UNKNOWN_FID.to_string()), + } + } + + async fn handle_stat_async(&mut self, fid: u32) -> Result { + let fm = self.try_file_meta(fid)?; + let s = self + .s + .stat(self.client_id, fm.qid, &self.state.uname) + .await?; + let stat: RawStat = s.into(); + let size = stat.size + size_of::() as u16; + + Ok(Rdata::Stat { size, stat }) + } + + async fn handle_wstat_async(&mut self, fid: u32, raw_stat: RawStat) -> Result { + let stat: Stat = raw_stat.try_into()?; + let fm = self.try_file_meta(fid)?; + self.s + .write_stat(self.client_id, fm.qid, stat, &self.state.uname) + .await?; + + Ok(Rdata::Wstat {}) + } + + async fn handle_open_async(&mut self, fid: u32, mode: Mode) -> Result { + let fm = self.try_file_meta(fid)?; + let iounit = self + .s + .open(self.client_id, fm.qid, mode, &self.state.uname) + .await?; + + Ok(Rdata::Open { + qid: fm.as_qid(), + iounit, + }) + } + + async fn handle_create_async( + &mut self, + fid: u32, + name: String, + perm: Perm, + mode: Mode, + ) -> Result { + let fm = self.try_file_meta(fid)?; + if fm.ty != FileType::Directory { + return Err(E_CREATE_NON_DIR.to_string()); + } + + let (fm, iounit) = self + .s + .create(self.client_id, fm.qid, &name, perm, mode, &self.state.uname) + .await?; + + // fid is now changed to point to the newly created file rather than the parent + let qid = fm.as_qid(); + self.state.fids.insert(fid, fm.qid); + self.qids.entry(fm.qid).or_insert(fm); + + Ok(Rdata::Create { qid, iounit }) + } + + // The read request asks for count bytes of data from the file identified by fid, which must be + // opened for reading, starting offset bytes after the beginning of the file. The bytes are + // returned with the read reply message. + // The count field in the reply indicates the number of bytes returned. This may be less than + // the requested amount. If the offset field is greater than or equal to the number of bytes in + // the file, a count of zero will be returned. + // For directories, read returns an integral number of directory entries exactly as in stat + // (see stat(9P)), one for each member of the directory. The read request message must have + // offset equal to zero or the value of offset in the previous read on the directory, plus the + // number of bytes returned in the previous read. In other words, seeking other than to the + // beginning is illegal in a directory. + async fn handle_read_async( + &mut self, + tag: u16, + fid: u32, + offset: u64, + count: u32, + tx: &UnboundedSender<(u16, Vec)>, + ) -> Result> { + use FileType::*; + + let fm = self.try_file_meta(fid)?; + if offset > u32::MAX as u64 { + return Err(format!("offset too large: {offset} > {}", u32::MAX)); + } + + let data = match fm.ty { + Directory => { + self.read_dir_async(fm.qid, offset as usize, count as usize) + .await? + } + Regular | AppendOnly | Exclusive => { + let outcome = self + .s + .read( + self.client_id, + fm.qid, + offset as usize, + count as usize, + &self.state.uname, + ) + .await?; + + match outcome { + ReadOutcome::Immediate(data) => data, + ReadOutcome::Blocked(mut chan) => { + let tx = tx.clone(); + spawn(async move { + let data = chan.recv().await.unwrap_or_default(); + tx.send((tag, data)) + }); + + return Ok(None); + } + } + } + }; + + Ok(Some(Rdata::Read { data: Data(data) })) + } + + async fn read_dir_async(&mut self, qid: u64, offset: usize, count: usize) -> Result> { + let stats = self + .s + .read_dir(self.client_id, qid, &self.state.uname) + .await?; + + let mut buf = Vec::with_capacity(count); + let mut to_skip = offset; + + for stat in stats.into_iter() { + self.qids.entry(stat.fm.qid).or_insert(stat.fm.clone()); + + let rstat: RawStat = stat.into(); + let mut tmp = Vec::new(); + rstat.write_to(&mut tmp).await.unwrap(); + + if to_skip != 0 { + if tmp.len() > to_skip { + return Err(E_INVALID_OFFSET.to_string()); + } else { + to_skip -= tmp.len(); + continue; + } + } + + if buf.len() + tmp.len() > count { + break; + } + buf.extend(tmp); + } + + Ok(buf) + } + + async fn handle_write_async(&mut self, fid: u32, offset: u64, data: Vec) -> Result { + let fm = self.try_file_meta(fid)?; + if offset > u32::MAX as u64 { + return Err(format!("offset too large: {offset} > {}", u32::MAX)); + } + + let count = self + .s + .write( + self.client_id, + fm.qid, + offset as usize, + data, + &self.state.uname, + ) + .await? as u32; + + Ok(Rdata::Write { count }) + } + + async fn handle_remove_async(&mut self, fid: u32) -> Result { + let fm = self.try_file_meta(fid)?; + + self.s + .remove(self.client_id, fm.qid, &self.state.uname) + .await?; + + Ok(Rdata::Remove {}) + } +} diff --git a/src/fsys/mod.rs b/src/fsys/mod.rs index 89336c3..fee8be9 100644 --- a/src/fsys/mod.rs +++ b/src/fsys/mod.rs @@ -40,7 +40,10 @@ use std::{ mem::take, path::Path, process::Command, - sync::mpsc::{channel, Receiver, Sender}, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, thread::{spawn, JoinHandle}, time::SystemTime, }; @@ -141,9 +144,12 @@ enum MiniBufferContent { Pending(Sender>>, Receiver>), } -/// The filesystem interface for ad +/// Mutable state for the ad filesystem. +/// +/// The parent [AdFs] holds onto this state inside of an Arc> which means that all +/// incoming requests will be processed sequentially. #[derive(Debug)] -pub(crate) struct AdFs { +struct State { tx: Sender, buffer_nodes: BufferNodes, minibuffer_content: MiniBufferContent, @@ -159,7 +165,7 @@ pub(crate) struct AdFs { auto_mount: bool, } -impl Drop for AdFs { +impl Drop for State { fn drop(&mut self) { if self.auto_mount { let res = Command::new("fusermount") @@ -173,60 +179,7 @@ impl Drop for AdFs { } } -impl AdFs { - /// Construct a new filesystem interface using channels held by the editor. - pub fn new(tx: Sender, brx: Receiver) -> Self { - let home = env::var("HOME").expect("$HOME to be set"); - let mount_path = format!("{home}/{MOUNT_DIR}"); - - if !Path::new(&mount_path).exists() { - create_dir_all(&mount_path).expect("to be able to create our mount point"); - } - - let (log_tx, log_rx) = channel(); - let (listener_tx, listener_rx) = channel(); - spawn_log_listener(brx, listener_tx, log_rx); - - let buffer_nodes = BufferNodes::new(tx.clone(), listener_rx, log_tx); - let auto_mount = config_handle!().auto_mount; - - Self { - tx, - buffer_nodes, - open_cids: HashMap::new(), - minibuffer_content: MiniBufferContent::Data(Vec::new()), - minibuffer_prompt: None, - mount_dir_stat: empty_dir_stat(MOUNT_ROOT_QID, "/"), - control_file_stat: empty_file_stat(CONTROL_FILE_QID, CONTROL_FILE), - minibuffer_stat: empty_file_stat(MINIBUFFER_QID, MINIBUFFER), - log_file_stat: empty_file_stat(LOG_FILE_QID, LOG_FILE), - mount_path, - auto_mount, - } - } - - /// Spawn a thread for running this filesystem and return a handle to it - pub fn run_threaded(self) -> FsHandle { - let auto_mount = self.auto_mount; - let mount_path = self.mount_path.clone(); - let socket_path = socket_path(DEFAULT_SOCKET_NAME); - - let s = Server::new(self); - let handle = FsHandle(s.serve_socket(DEFAULT_SOCKET_NAME.to_string())); - - if auto_mount { - let res = Command::new("9pfuse") - .args([socket_path, mount_path]) - .spawn(); - - if let Ok(mut child) = res { - _ = child.wait(); - } - } - - handle - } - +impl State { fn add_open_cid(&mut self, qid: u64, cid: ClientId) { self.open_cids.entry(qid).or_default().cids.push(cid); } @@ -346,6 +299,71 @@ impl AdFs { } } +/// The filesystem interface for ad +#[derive(Debug)] +pub(crate) struct AdFs { + state: Arc>, +} + +impl AdFs { + /// Construct a new filesystem interface using channels held by the editor. + pub fn new(tx: Sender, brx: Receiver) -> Self { + let home = env::var("HOME").expect("$HOME to be set"); + let mount_path = format!("{home}/{MOUNT_DIR}"); + + if !Path::new(&mount_path).exists() { + create_dir_all(&mount_path).expect("to be able to create our mount point"); + } + + let (log_tx, log_rx) = channel(); + let (listener_tx, listener_rx) = channel(); + spawn_log_listener(brx, listener_tx, log_rx); + + let buffer_nodes = BufferNodes::new(tx.clone(), listener_rx, log_tx); + let auto_mount = config_handle!().auto_mount; + + Self { + state: Arc::new(Mutex::new(State { + tx, + buffer_nodes, + open_cids: HashMap::new(), + minibuffer_content: MiniBufferContent::Data(Vec::new()), + minibuffer_prompt: None, + mount_dir_stat: empty_dir_stat(MOUNT_ROOT_QID, "/"), + control_file_stat: empty_file_stat(CONTROL_FILE_QID, CONTROL_FILE), + minibuffer_stat: empty_file_stat(MINIBUFFER_QID, MINIBUFFER), + log_file_stat: empty_file_stat(LOG_FILE_QID, LOG_FILE), + mount_path, + auto_mount, + })), + } + } + + /// Spawn a thread for running this filesystem and return a handle to it + pub fn run_threaded(self) -> FsHandle { + let s = self.state.lock().unwrap(); + let auto_mount = s.auto_mount; + let mount_path = s.mount_path.clone(); + let socket_path = socket_path(DEFAULT_SOCKET_NAME); + drop(s); + + let s = Server::new(self); + let handle = FsHandle(s.serve_socket(DEFAULT_SOCKET_NAME.to_string())); + + if auto_mount { + let res = Command::new("9pfuse") + .args([socket_path, mount_path]) + .spawn(); + + if let Ok(mut child) = res { + _ = child.wait(); + } + } + + handle + } +} + /// Spawn a listener to wait for a reply from the editor for our minibuffer selection fn spawn_minibuffer_listener( data_rx: Receiver, @@ -372,62 +390,59 @@ fn spawn_minibuffer_listener( } impl Serve9p for AdFs { - fn stat(&mut self, cid: ClientId, qid: u64, uname: &str) -> Result { + fn stat(&self, cid: ClientId, qid: u64, uname: &str) -> Result { trace!(?cid, %qid, %uname, "handling stat request"); - self.buffer_nodes.update(); + let mut s = self.state.lock().unwrap(); + s.buffer_nodes.update(); match qid { - MOUNT_ROOT_QID => Ok(self.mount_dir_stat.clone()), - CONTROL_FILE_QID => Ok(self.control_file_stat.clone()), - MINIBUFFER_QID => Ok(self.minibuffer_stat.clone()), - LOG_FILE_QID => Ok(self.log_file_stat.clone()), - BUFFERS_QID => Ok(self.buffer_nodes.stat().clone()), - qid => match self.buffer_nodes.get_stat_for_qid(qid) { + MOUNT_ROOT_QID => Ok(s.mount_dir_stat.clone()), + CONTROL_FILE_QID => Ok(s.control_file_stat.clone()), + MINIBUFFER_QID => Ok(s.minibuffer_stat.clone()), + LOG_FILE_QID => Ok(s.log_file_stat.clone()), + BUFFERS_QID => Ok(s.buffer_nodes.stat().clone()), + qid => match s.buffer_nodes.get_stat_for_qid(qid) { Some(stat) => Ok(stat.clone()), None => Err(E_UNKNOWN_FILE.to_string()), }, } } - fn write_stat(&mut self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()> { + fn write_stat(&self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()> { trace!(?cid, %qid, %uname, "handling write stat request"); - self.buffer_nodes.update(); + let mut s = self.state.lock().unwrap(); + s.buffer_nodes.update(); if stat.n_bytes == 0 { trace!(%qid, %uname, "stat n_bytes=0, truncating file"); match qid { MOUNT_ROOT_QID | CONTROL_FILE_QID | MINIBUFFER_QID | LOG_FILE_QID => (), - qid => self.buffer_nodes.truncate(qid), + qid => s.buffer_nodes.truncate(qid), } } Ok(()) } - fn walk( - &mut self, - cid: ClientId, - parent_qid: u64, - child: &str, - uname: &str, - ) -> Result { + fn walk(&self, cid: ClientId, parent_qid: u64, child: &str, uname: &str) -> Result { trace!(?cid, %parent_qid, %child, %uname, "handling walk request"); - self.buffer_nodes.update(); + let mut s = self.state.lock().unwrap(); + s.buffer_nodes.update(); match parent_qid { MOUNT_ROOT_QID => match child { - CONTROL_FILE => Ok(self.control_file_stat.fm.clone()), - MINIBUFFER => Ok(self.minibuffer_stat.fm.clone()), - LOG_FILE => Ok(self.log_file_stat.fm.clone()), - BUFFERS_DIR => Ok(self.buffer_nodes.stat().fm.clone()), - _ => match self.buffer_nodes.lookup_file_stat(parent_qid, child) { + CONTROL_FILE => Ok(s.control_file_stat.fm.clone()), + MINIBUFFER => Ok(s.minibuffer_stat.fm.clone()), + LOG_FILE => Ok(s.log_file_stat.fm.clone()), + BUFFERS_DIR => Ok(s.buffer_nodes.stat().fm.clone()), + _ => match s.buffer_nodes.lookup_file_stat(parent_qid, child) { Some(stat) => Ok(stat.fm.clone()), None => Err(format!("{E_UNKNOWN_FILE}: {parent_qid} {child}")), }, }, - qid if qid == BUFFERS_QID || self.buffer_nodes.is_known_buffer_qid(qid) => { - match self.buffer_nodes.lookup_file_stat(qid, child) { + qid if qid == BUFFERS_QID || s.buffer_nodes.is_known_buffer_qid(qid) => { + match s.buffer_nodes.lookup_file_stat(qid, child) { Some(stat) => Ok(stat.fm.clone()), None => Err(format!("{E_UNKNOWN_FILE}: {parent_qid} {child}")), } @@ -437,38 +452,40 @@ impl Serve9p for AdFs { } } - fn open(&mut self, cid: ClientId, qid: u64, mode: Mode, uname: &str) -> Result { + fn open(&self, cid: ClientId, qid: u64, mode: Mode, uname: &str) -> Result { trace!(?cid, %qid, %uname, ?mode, "handling open request"); - self.buffer_nodes.update(); + let mut s = self.state.lock().unwrap(); + s.buffer_nodes.update(); if qid == LOG_FILE_QID { - self.buffer_nodes.log.add_client(cid); + s.buffer_nodes.log.add_client(cid); } else if !TOP_LEVEL_QIDS.contains(&qid) { - if let QidCheck::Unknown = self.buffer_nodes.check_if_known_qid(qid) { + if let QidCheck::Unknown = s.buffer_nodes.check_if_known_qid(qid) { return Err(format!("{E_UNKNOWN_FILE}: {qid}")); } } - self.add_open_cid(qid, cid); + s.add_open_cid(qid, cid); Ok(IO_UNIT) } - fn clunk(&mut self, cid: ClientId, qid: u64) { + fn clunk(&self, cid: ClientId, qid: u64) { trace!(?cid, %qid, "handling clunk request"); + let mut s = self.state.lock().unwrap(); if qid == LOG_FILE_QID { - self.buffer_nodes.log.remove_client(cid); - } else if let QidCheck::EventFile { buf_qid } = self.buffer_nodes.check_if_known_qid(qid) { - if self.readlocked_cid(qid) == Some(cid) { - self.buffer_nodes.clear_input_filter(buf_qid); + s.buffer_nodes.log.remove_client(cid); + } else if let QidCheck::EventFile { buf_qid } = s.buffer_nodes.check_if_known_qid(qid) { + if s.readlocked_cid(qid) == Some(cid) { + s.buffer_nodes.clear_input_filter(buf_qid); } } - self.remove_open_cid(qid, cid); // also handles clearing the read lock + s.remove_open_cid(qid, cid); // also handles clearing the read lock } fn read( - &mut self, + &self, cid: ClientId, qid: u64, offset: usize, @@ -476,48 +493,50 @@ impl Serve9p for AdFs { uname: &str, ) -> Result { trace!(?cid, %qid, %offset, %count, %uname, "handling read request"); - self.buffer_nodes.update(); + let mut s = self.state.lock().unwrap(); + s.buffer_nodes.update(); if qid == CONTROL_FILE_QID { return Ok(ReadOutcome::Immediate(Vec::new())); } else if qid == MINIBUFFER_QID { - return Ok(self.minibuffer_read(offset, count)); + return Ok(s.minibuffer_read(offset, count)); } else if qid == LOG_FILE_QID { - return Ok(self.buffer_nodes.log.events_since_last_read(cid)); + return Ok(s.buffer_nodes.log.events_since_last_read(cid)); } - if let QidCheck::EventFile { buf_qid } = self.buffer_nodes.check_if_known_qid(qid) { - match self.readlocked_cid(qid) { + if let QidCheck::EventFile { buf_qid } = s.buffer_nodes.check_if_known_qid(qid) { + match s.readlocked_cid(qid) { Some(id) if id == cid => (), Some(_) => return Ok(ReadOutcome::Immediate(Vec::new())), None => { trace!("attaching filter qid={qid} cid={cid:?}"); - self.buffer_nodes.attach_input_filter(buf_qid)?; - self.lock_qid_for_reading(qid, cid)?; + s.buffer_nodes.attach_input_filter(buf_qid)?; + s.lock_qid_for_reading(qid, cid)?; } } } - match self.buffer_nodes.get_file_content(qid, offset, count) { + match s.buffer_nodes.get_file_content(qid, offset, count) { InternalRead::Unknown => Err(format!("{E_UNKNOWN_FILE}: {qid}")), InternalRead::Immediate(content) => Ok(ReadOutcome::Immediate(content)), InternalRead::Blocked(tx) => Ok(ReadOutcome::Blocked(tx)), } } - fn read_dir(&mut self, cid: ClientId, qid: u64, uname: &str) -> Result> { + fn read_dir(&self, cid: ClientId, qid: u64, uname: &str) -> Result> { trace!(?cid, %qid, %uname, "handling read dir request"); - self.buffer_nodes.update(); + let mut s = self.state.lock().unwrap(); + s.buffer_nodes.update(); match qid { MOUNT_ROOT_QID => Ok(vec![ - self.log_file_stat.clone(), - self.minibuffer_stat.clone(), - self.control_file_stat.clone(), - self.buffer_nodes.stat().clone(), + s.log_file_stat.clone(), + s.minibuffer_stat.clone(), + s.control_file_stat.clone(), + s.buffer_nodes.stat().clone(), ]), - BUFFERS_QID => Ok(self.buffer_nodes.top_level_stats()), - qid => self + BUFFERS_QID => Ok(s.buffer_nodes.top_level_stats()), + qid => s .buffer_nodes .buffer_level_stats(qid) .ok_or_else(|| E_UNKNOWN_FILE.to_string()), @@ -525,7 +544,7 @@ impl Serve9p for AdFs { } fn write( - &mut self, + &self, cid: ClientId, qid: u64, offset: usize, @@ -533,46 +552,47 @@ impl Serve9p for AdFs { uname: &str, ) -> Result { trace!(?cid, %qid, %offset, n_bytes=%data.len(), %uname, "handling write request"); - self.buffer_nodes.update(); + let mut s = self.state.lock().unwrap(); + s.buffer_nodes.update(); let n_bytes = data.len(); - let s = match String::from_utf8(data.to_vec()) { + let str = match String::from_utf8(data.to_vec()) { Ok(s) => s, Err(e) => return Err(format!("Invalid data: {e}")), }; match qid { - CONTROL_FILE_QID => match s.strip_prefix("minibuffer-prompt ") { + CONTROL_FILE_QID => match str.strip_prefix("minibuffer-prompt ") { Some(prompt) => { - self.minibuffer_prompt = Some(prompt.to_string()); + s.minibuffer_prompt = Some(prompt.to_string()); Ok(n_bytes) } None => { - self.control_file_stat.last_modified = SystemTime::now(); - match Message::send(Req::ControlMessage { msg: s }, &self.tx) { + s.control_file_stat.last_modified = SystemTime::now(); + match Message::send(Req::ControlMessage { msg: str }, &s.tx) { Ok(_) => Ok(n_bytes), Err(e) => Err(format!("unable to execute control message: {e}")), } } }, - MINIBUFFER_QID => self.minibuffer_write(s), - CURRENT_BUFFER_QID => self.set_active_buffer(s), + MINIBUFFER_QID => s.minibuffer_write(str), + CURRENT_BUFFER_QID => s.set_active_buffer(str), LOG_FILE_QID | INDEX_BUFFER_QID => Err(E_NOT_ALLOWED.to_string()), - qid => self.buffer_nodes.write(qid, s, offset), + qid => s.buffer_nodes.write(qid, str, offset), } } // TODO: allow remove of a buffer to close the buffer - fn remove(&mut self, cid: ClientId, qid: u64, uname: &str) -> Result<()> { + fn remove(&self, cid: ClientId, qid: u64, uname: &str) -> Result<()> { trace!(?cid, %qid, %uname, "handling remove request"); Err("remove not allowed".to_string()) } fn create( - &mut self, + &self, cid: ClientId, parent: u64, name: &str, From 4180f461b55f9db7cfc05675bc10a455107c62eb Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Mon, 24 Feb 2025 15:53:50 +0000 Subject: [PATCH 04/12] exploring questionable ideas involving async/await --- Cargo.lock | 105 ++++++++++++++++++++++++ crates/ninep/Cargo.toml | 1 + crates/ninep/examples/async_fsm.rs | 126 +++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 crates/ninep/examples/async_fsm.rs diff --git a/Cargo.lock b/Cargo.lock index 1ded306..96cdd06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,6 +303,95 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gimli" version = "0.31.1" @@ -469,6 +558,7 @@ version = "0.3.0" dependencies = [ "async-trait", "bitflags 2.8.0", + "futures", "simple_test_case", "tokio", ] @@ -548,6 +638,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "plotters" version = "0.3.7" @@ -769,6 +865,15 @@ dependencies = [ "syn", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.14.0" diff --git a/crates/ninep/Cargo.toml b/crates/ninep/Cargo.toml index 7937177..449f28d 100644 --- a/crates/ninep/Cargo.toml +++ b/crates/ninep/Cargo.toml @@ -20,6 +20,7 @@ tokio = ["dep:tokio", "dep:async-trait"] [dependencies] async-trait = { version = "0.1.86", optional = true } bitflags = "2.6" +futures = "0.3.31" tokio = { version = "1.43.0", features = ["time", "macros", "parking_lot", "rt-multi-thread", "net", "io-util", "sync"], optional = true } [dev-dependencies] diff --git a/crates/ninep/examples/async_fsm.rs b/crates/ninep/examples/async_fsm.rs new file mode 100644 index 0000000..fceaf7f --- /dev/null +++ b/crates/ninep/examples/async_fsm.rs @@ -0,0 +1,126 @@ +//! This is a little exploration of using async/await + a dummy Waker to simplify writing sans-io +//! state machine code. +use futures::pending; +use std::{ + cell::RefCell, + future::{Future, IntoFuture}, + io::Read, + pin::pin, + rc::Rc, + sync::Arc, + task::{Context, Poll, Wake, Waker}, +}; + +fn main() { + // "Hello, 世界" with a u16 length header + // + // From [INTRO(5)](http://man.cat-v.org/plan_9/5/intro): + // Data items of larger or variable lengths are represented by a two-byte field specifying + // a count, n, followed by n bytes of data. Text strings are represented this way, with + // the text itself stored as a UTF-8 encoded sequence of Unicode charac- ters (see utf(6)). + // + // Text strings in 9P messages are not NUL- terminated: n counts the bytes of UTF-8 data, + // which include no final zero byte. The NUL character is illegal in all text strings + // in 9P, and is therefore excluded from file names, user names, and so on. + let data = &[ + 0x0d, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c, + ]; + let r = StringReader::default(); + let val = read_9p_sync_from_bytes(r, data); + + println!("got val: {val:?}"); +} + +/// A no-op waker that is just used to create a [Context] in order to poll the [Read9p] future. +struct StubWaker; +impl Wake for StubWaker { + fn wake(self: Arc) {} + fn wake_by_ref(self: &Arc) {} +} + +/// A real impl of a reader function would source the bytes here from IO rather than reading from a Vec. +fn read_9p_sync_from_bytes(r: R, data: &[u8]) -> R::T { + let waker = Waker::from(Arc::new(StubWaker)); + let mut context = Context::from_waker(&waker); + let mut offset = 0; + + let mut s = r.state(); + let mut fut = pin!(r.read().into_future()); + + loop { + match fut.as_mut().poll(&mut context) { + Poll::Ready(val) => { + return val; + } + Poll::Pending => { + let n = s.bytes_needed(); + println!("{n} bytes requested"); + s.set_buf(data[offset..offset + n].to_vec()); + offset += n; + } + } + } +} + +#[derive(Default, Debug, Clone)] +struct State(Rc>); +impl State { + fn request_bytes(&mut self, n: usize) { + self.0.borrow_mut().n = n; + } + + fn bytes_needed(&self) -> usize { + self.0.borrow().n + } + + fn set_buf(&mut self, buf: Vec) { + self.0.borrow_mut().buf = buf; + } + + fn buf(&self) -> Vec { + self.0.borrow().buf.clone() + } +} + +#[derive(Default, Debug)] +struct StateInner { + n: usize, + buf: Vec, +} + +#[allow(async_fn_in_trait)] +trait Read9p { + type T; + + fn state(&self) -> State; + async fn read(self) -> Self::T; +} + +#[derive(Default)] +struct StringReader { + s: State, +} + +impl Read9p for StringReader { + type T = String; + + fn state(&self) -> State { + self.s.clone() + } + + async fn read(mut self) -> String { + let n = size_of::(); + self.s.request_bytes(n); + pending!(); + + let data = self.s.buf()[0..n].try_into().unwrap(); + let len = u16::from_le_bytes(data) as usize; + self.s.request_bytes(len); + pending!(); + + let mut s = String::with_capacity(len); + self.s.buf().as_slice().read_to_string(&mut s).unwrap(); + + s + } +} From a778457afea548a283d9eb9e7fba65c1353bd102 Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Mon, 24 Feb 2025 19:55:42 +0000 Subject: [PATCH 05/12] making the async/await state machine stuff look semi-respectable --- Cargo.lock | 154 ------------------------- crates/ninep/Cargo.toml | 4 +- crates/ninep/examples/async_fsm.rs | 175 ++++++++++++++++------------- 3 files changed, 101 insertions(+), 232 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96cdd06..c00f715 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,95 +303,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - [[package]] name = "gimli" version = "0.31.1" @@ -488,16 +399,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.26" @@ -558,7 +459,6 @@ version = "0.3.0" dependencies = [ "async-trait", "bitflags 2.8.0", - "futures", "simple_test_case", "tokio", ] @@ -609,41 +509,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "plotters" version = "0.3.7" @@ -710,15 +581,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" -dependencies = [ - "bitflags 2.8.0", -] - [[package]] name = "regex" version = "1.11.1" @@ -781,12 +643,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "serde" version = "1.0.218" @@ -865,15 +721,6 @@ dependencies = [ "syn", ] -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "smallvec" version = "1.14.0" @@ -947,7 +794,6 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", "socket2", "tokio-macros", diff --git a/crates/ninep/Cargo.toml b/crates/ninep/Cargo.toml index 449f28d..c71a440 100644 --- a/crates/ninep/Cargo.toml +++ b/crates/ninep/Cargo.toml @@ -20,8 +20,8 @@ tokio = ["dep:tokio", "dep:async-trait"] [dependencies] async-trait = { version = "0.1.86", optional = true } bitflags = "2.6" -futures = "0.3.31" -tokio = { version = "1.43.0", features = ["time", "macros", "parking_lot", "rt-multi-thread", "net", "io-util", "sync"], optional = true } +tokio = { version = "1.43.0", features = ["net", "io-util", "sync"], optional = true } [dev-dependencies] simple_test_case = "1" +tokio = { version = "1.43.0", features = ["time", "macros", "rt-multi-thread", "net", "io-util", "sync"] } diff --git a/crates/ninep/examples/async_fsm.rs b/crates/ninep/examples/async_fsm.rs index fceaf7f..9179f03 100644 --- a/crates/ninep/examples/async_fsm.rs +++ b/crates/ninep/examples/async_fsm.rs @@ -1,126 +1,149 @@ //! This is a little exploration of using async/await + a dummy Waker to simplify writing sans-io //! state machine code. -use futures::pending; use std::{ cell::RefCell, future::{Future, IntoFuture}, - io::Read, - pin::pin, + io::{Cursor, Read}, + pin::{pin, Pin}, rc::Rc, sync::Arc, task::{Context, Poll, Wake, Waker}, }; +use tokio::io::{AsyncRead, AsyncReadExt}; -fn main() { +#[tokio::main] +async fn main() { // "Hello, 世界" with a u16 length header // - // From [INTRO(5)](http://man.cat-v.org/plan_9/5/intro): - // Data items of larger or variable lengths are represented by a two-byte field specifying - // a count, n, followed by n bytes of data. Text strings are represented this way, with - // the text itself stored as a UTF-8 encoded sequence of Unicode charac- ters (see utf(6)). - // - // Text strings in 9P messages are not NUL- terminated: n counts the bytes of UTF-8 data, - // which include no final zero byte. The NUL character is illegal in all text strings - // in 9P, and is therefore excluded from file names, user names, and so on. - let data = &[ + // In 9p, data items of larger or variable lengths are represented by a two-byte field + // specifying a count, n, followed by n bytes of data. Text strings are represented this way, + // with the text itself stored as a UTF-8 encoded sequence of Unicode characters without a + // trailing null byte. + let mut cur = Cursor::new(vec![ 0x0d, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c, - ]; - let r = StringReader::default(); - let val = read_9p_sync_from_bytes(r, data); + ]); - println!("got val: {val:?}"); -} + println!(">> reading using std::io::Read"); + let s: String = read_9p_sync_from_bytes(&mut cur); + println!(" got val: {s:?}\n"); -/// A no-op waker that is just used to create a [Context] in order to poll the [Read9p] future. -struct StubWaker; -impl Wake for StubWaker { - fn wake(self: Arc) {} - fn wake_by_ref(self: &Arc) {} + cur.set_position(0); + + println!(">> reading using tokio::io::AsyncRead"); + let s: String = read_9p_async_from_bytes(&mut cur).await; + println!(" got val: {s:?}"); } -/// A real impl of a reader function would source the bytes here from IO rather than reading from a Vec. -fn read_9p_sync_from_bytes(r: R, data: &[u8]) -> R::T { +fn read_9p_sync_from_bytes(r: &mut R) -> T +where + T: Read9p, + R: Read, +{ let waker = Waker::from(Arc::new(StubWaker)); let mut context = Context::from_waker(&waker); - let mut offset = 0; - - let mut s = r.state(); - let mut fut = pin!(r.read().into_future()); + let s = State::default(); + // SAFETY: assumes the impl of Read9p is a valid future for us to poll + let mut fut = unsafe { pin!(T::read(s.clone()).into_future()) }; loop { match fut.as_mut().poll(&mut context) { - Poll::Ready(val) => { - return val; - } + Poll::Ready(val) => return val, Poll::Pending => { - let n = s.bytes_needed(); + let n = s.0.borrow().n; println!("{n} bytes requested"); - s.set_buf(data[offset..offset + n].to_vec()); - offset += n; + let mut buf = vec![0; n]; + r.read_exact(&mut buf).unwrap(); + s.0.borrow_mut().buf = Some(buf); } } } } -#[derive(Default, Debug, Clone)] -struct State(Rc>); -impl State { - fn request_bytes(&mut self, n: usize) { - self.0.borrow_mut().n = n; - } +async fn read_9p_async_from_bytes(r: &mut R) -> T +where + T: Read9p, + R: AsyncRead + Unpin, +{ + let waker = Waker::from(Arc::new(StubWaker)); + let mut context = Context::from_waker(&waker); + let s = State::default(); - fn bytes_needed(&self) -> usize { - self.0.borrow().n + // SAFETY: assumes the impl of Read9p is a valid future for us to poll + let mut fut = unsafe { pin!(T::read(s.clone()).into_future()) }; + loop { + match fut.as_mut().poll(&mut context) { + Poll::Ready(val) => return val, + Poll::Pending => { + let n = s.0.borrow().n; + println!("{n} bytes requested"); + let mut buf = vec![0; n]; + r.read_exact(&mut buf).await.unwrap(); + s.0.borrow_mut().buf = Some(buf); + } + } } +} - fn set_buf(&mut self, buf: Vec) { - self.0.borrow_mut().buf = buf; - } +/// A no-op waker that is just used to create a [Context] in order to poll the [Read9p] future. +struct StubWaker; +impl Wake for StubWaker { + fn wake(self: Arc) {} + fn wake_by_ref(self: &Arc) {} +} - fn buf(&self) -> Vec { - self.0.borrow().buf.clone() +/// Helper struct for awaiting a Future that returns pending once so we can return control to the +/// poll loop and perform IO. +struct Yield(bool); +impl Future for Yield { + type Output = (); + fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<()> { + if self.0 { + Poll::Ready(()) + } else { + self.0 = true; + Poll::Pending + } } } +#[derive(Default, Debug, Clone)] +struct State(Rc>); + #[derive(Default, Debug)] struct StateInner { n: usize, - buf: Vec, + buf: Option>, } -#[allow(async_fn_in_trait)] -trait Read9p { - type T; - - fn state(&self) -> State; - async fn read(self) -> Self::T; +/// Request a specific number of bytes from the parent poll loop and then yield to that poll loop +/// so it can perform IO and provide the requested data. +macro_rules! request_bytes { + ($s:expr, $n:expr) => {{ + $s.0.borrow_mut().n = $n; + Yield(false).await; + $s.0.borrow_mut().buf.take().unwrap() + }}; } -#[derive(Default)] -struct StringReader { - s: State, +/// # Safety +/// The read method of this trait requires that you only yield view the [request_bytes] macro. +#[allow(async_fn_in_trait)] +unsafe trait Read9p { + /// # Safety + /// Implementations of `read` need to ensure that the only await points they contain are + /// from calls to the [request_bytes] macro. + async unsafe fn read(state: State) -> Self; } -impl Read9p for StringReader { - type T = String; - - fn state(&self) -> State { - self.s.clone() - } - - async fn read(mut self) -> String { +#[allow(async_fn_in_trait)] +unsafe impl Read9p for String { + async unsafe fn read(state: State) -> Self { let n = size_of::(); - self.s.request_bytes(n); - pending!(); - - let data = self.s.buf()[0..n].try_into().unwrap(); + let buf = request_bytes!(state, n); + let data = buf[0..n].try_into().unwrap(); let len = u16::from_le_bytes(data) as usize; - self.s.request_bytes(len); - pending!(); - - let mut s = String::with_capacity(len); - self.s.buf().as_slice().read_to_string(&mut s).unwrap(); + let buf = request_bytes!(state, len); - s + String::from_utf8(buf).unwrap() } } From 2889a8e2e0c7b5141a5c65ddb7411b032b952f07 Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Tue, 25 Feb 2025 08:13:54 +0000 Subject: [PATCH 06/12] getting the fsm approach to work without async-trait --- Cargo.lock | 16 +- Cargo.toml | 2 +- crates/ad_client/Cargo.toml | 4 +- crates/ninep/Cargo.toml | 9 +- crates/ninep/examples/async_fsm.rs | 42 +- crates/ninep/examples/async_server.rs | 1 - crates/ninep/src/lib.rs | 2 + crates/ninep/src/sansio/mod.rs | 15 + crates/ninep/src/sansio/protocol.rs | 719 ++++++-------------------- crates/ninep/src/sync/mod.rs | 34 +- crates/ninep/src/tokio/mod.rs | 93 ++-- crates/ninep/src/tokio/server.rs | 64 ++- 12 files changed, 337 insertions(+), 664 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c00f715..f97e0f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,7 +27,7 @@ dependencies = [ [[package]] name = "ad_client" -version = "0.3.0" +version = "0.3.1" dependencies = [ "ad_event", "ninep", @@ -86,17 +86,6 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" -[[package]] -name = "async-trait" -version = "0.1.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "autocfg" version = "1.4.0" @@ -455,9 +444,8 @@ dependencies = [ [[package]] name = "ninep" -version = "0.3.0" +version = "0.4.0" dependencies = [ - "async-trait", "bitflags 2.8.0", "simple_test_case", "tokio", diff --git a/Cargo.toml b/Cargo.toml index c8918c5..5b4e19d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ harness = false [dependencies] ad_event = { version = "0.3", path = "crates/ad_event" } -ninep = { version = "0.3", path = "crates/ninep" } +ninep = { version = "0.4", path = "crates/ninep" } libc = "0.2.159" lsp-types = "0.97.0" serde = "1.0.215" diff --git a/crates/ad_client/Cargo.toml b/crates/ad_client/Cargo.toml index 9c76b96..7e594c2 100644 --- a/crates/ad_client/Cargo.toml +++ b/crates/ad_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ad_client" -version = "0.3.0" +version = "0.3.1" edition = "2021" authors = ["sminez "] license = "MIT" @@ -19,4 +19,4 @@ categories = [ "development-tools", "text-editors", "command-line-utilities" ] [dependencies] ad_event = { version = "0.3", path = "../ad_event" } -ninep = { version = "0.3", path = "../ninep" } +ninep = { version = "0.4", path = "../ninep" } diff --git a/crates/ninep/Cargo.toml b/crates/ninep/Cargo.toml index c71a440..262c2cc 100644 --- a/crates/ninep/Cargo.toml +++ b/crates/ninep/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ninep" -version = "0.3.0" +version = "0.4.0" edition = "2021" authors = ["sminez "] license = "MIT" @@ -15,13 +15,12 @@ include = [ [features] default = ["tokio"] -tokio = ["dep:tokio", "dep:async-trait"] +tokio = ["dep:tokio"] [dependencies] -async-trait = { version = "0.1.86", optional = true } bitflags = "2.6" -tokio = { version = "1.43.0", features = ["net", "io-util", "sync"], optional = true } +tokio = { version = "1.43.0", features = ["macros", "net", "io-util", "rt", "sync"], optional = true } [dev-dependencies] simple_test_case = "1" -tokio = { version = "1.43.0", features = ["time", "macros", "rt-multi-thread", "net", "io-util", "sync"] } +tokio = { version = "1.43.0", features = ["macros", "net", "io-util", "rt", "sync", "time", "rt-multi-thread", "net", "io-util"] } diff --git a/crates/ninep/examples/async_fsm.rs b/crates/ninep/examples/async_fsm.rs index 9179f03..ce0b76d 100644 --- a/crates/ninep/examples/async_fsm.rs +++ b/crates/ninep/examples/async_fsm.rs @@ -1,11 +1,10 @@ //! This is a little exploration of using async/await + a dummy Waker to simplify writing sans-io //! state machine code. use std::{ - cell::RefCell, + cell::UnsafeCell, future::{Future, IntoFuture}, io::{Cursor, Read}, pin::{pin, Pin}, - rc::Rc, sync::Arc, task::{Context, Poll, Wake, Waker}, }; @@ -24,17 +23,17 @@ async fn main() { ]); println!(">> reading using std::io::Read"); - let s: String = read_9p_sync_from_bytes(&mut cur); + let s: String = read_9p_sync(&mut cur); println!(" got val: {s:?}\n"); cur.set_position(0); println!(">> reading using tokio::io::AsyncRead"); - let s: String = read_9p_async_from_bytes(&mut cur).await; + let s: String = read_9p_async(&mut cur).await; println!(" got val: {s:?}"); } -fn read_9p_sync_from_bytes(r: &mut R) -> T +fn read_9p_sync(r: &mut R) -> T where T: Read9p, R: Read, @@ -48,18 +47,18 @@ where loop { match fut.as_mut().poll(&mut context) { Poll::Ready(val) => return val, - Poll::Pending => { - let n = s.0.borrow().n; + Poll::Pending => unsafe { + let n = (*s.0.get()).n; println!("{n} bytes requested"); let mut buf = vec![0; n]; r.read_exact(&mut buf).unwrap(); - s.0.borrow_mut().buf = Some(buf); - } + (*s.0.get()).buf = Some(buf); + }, } } } -async fn read_9p_async_from_bytes(r: &mut R) -> T +async fn read_9p_async(r: &mut R) -> T where T: Read9p, R: AsyncRead + Unpin, @@ -73,13 +72,13 @@ where loop { match fut.as_mut().poll(&mut context) { Poll::Ready(val) => return val, - Poll::Pending => { - let n = s.0.borrow().n; + Poll::Pending => unsafe { + let n = (*s.0.get()).n; println!("{n} bytes requested"); let mut buf = vec![0; n]; r.read_exact(&mut buf).await.unwrap(); - s.0.borrow_mut().buf = Some(buf); - } + (*s.0.get()).buf = Some(buf); + }, } } } @@ -106,8 +105,14 @@ impl Future for Yield { } } +/// Shared state between a [NineP] impl and a parent read loop that is performing IO. #[derive(Default, Debug, Clone)] -struct State(Rc>); +pub struct State(pub(crate) Arc>); + +// SAFETY: StateInner is only accessable in this crate +unsafe impl Send for State {} +// SAFETY: StateInner is only accessable in this crate +unsafe impl Sync for State {} #[derive(Default, Debug)] struct StateInner { @@ -119,20 +124,19 @@ struct StateInner { /// so it can perform IO and provide the requested data. macro_rules! request_bytes { ($s:expr, $n:expr) => {{ - $s.0.borrow_mut().n = $n; + (*$s.0.get()).n = $n; Yield(false).await; - $s.0.borrow_mut().buf.take().unwrap() + (*$s.0.get()).buf.take().unwrap() }}; } /// # Safety /// The read method of this trait requires that you only yield view the [request_bytes] macro. -#[allow(async_fn_in_trait)] unsafe trait Read9p { /// # Safety /// Implementations of `read` need to ensure that the only await points they contain are /// from calls to the [request_bytes] macro. - async unsafe fn read(state: State) -> Self; + unsafe fn read(state: State) -> impl Future + Send; } #[allow(async_fn_in_trait)] diff --git a/crates/ninep/examples/async_server.rs b/crates/ninep/examples/async_server.rs index b7e5683..a0ef6e6 100644 --- a/crates/ninep/examples/async_server.rs +++ b/crates/ninep/examples/async_server.rs @@ -63,7 +63,6 @@ const BAZ: u64 = 3; const RW: u64 = 4; const BLOCKING: u64 = 5; -#[async_trait::async_trait] impl AsyncServe9p for EchoServer { async fn write( &self, diff --git a/crates/ninep/src/lib.rs b/crates/ninep/src/lib.rs index 751cc11..07874ed 100644 --- a/crates/ninep/src/lib.rs +++ b/crates/ninep/src/lib.rs @@ -14,6 +14,8 @@ pub mod fs; pub mod sansio; pub mod sync; + +#[cfg(feature = "tokio")] pub mod tokio; /// A simple result type for errors returned from this crate diff --git a/crates/ninep/src/sansio/mod.rs b/crates/ninep/src/sansio/mod.rs index e71a6aa..1dd296f 100644 --- a/crates/ninep/src/sansio/mod.rs +++ b/crates/ninep/src/sansio/mod.rs @@ -5,6 +5,10 @@ use crate::{ sansio::protocol::{Rdata, Rmessage}, Result, }; +use std::{ + sync::Arc, + task::{Wake, Waker}, +}; pub mod protocol; pub mod server; @@ -17,3 +21,14 @@ impl From<(u16, Result)> for Rmessage { } } } + +struct StubWaker; +impl Wake for StubWaker { + fn wake(self: Arc) {} + fn wake_by_ref(self: &Arc) {} +} + +/// A no-op waker that is just used to create a context for driving a NineP read loop. +pub(crate) fn stub_waker() -> Waker { + Waker::from(Arc::new(StubWaker)) +} diff --git a/crates/ninep/src/sansio/protocol.rs b/crates/ninep/src/sansio/protocol.rs index 1fa997a..1419d77 100644 --- a/crates/ninep/src/sansio/protocol.rs +++ b/crates/ninep/src/sansio/protocol.rs @@ -1,10 +1,16 @@ //! Sans-io 9p protocol implementation //! //! http://man.cat-v.org/plan_9/5/ +use crate::sync::SyncNineP; use std::{ + cell::UnsafeCell, fmt, - io::{self, ErrorKind, Read}, + future::Future, + io::{self, Cursor, ErrorKind}, mem::size_of, + pin::Pin, + sync::Arc, + task::{Context, Poll}, }; /// The size of variable length data is denoted using a u16 so anything longer @@ -45,9 +51,6 @@ impl fmt::Display for WriteError { /// Each message consists of a sequence of bytes. Two-, four-, and eight-byte fields hold /// unsigned integers represented in little-endian order (least significant byte first). pub trait NineP: Sized { - /// The [Read9p] implementation used to decode an instance of this type from a bytestream - type Reader: Read9p; - /// Number of bytes required to encode fn n_bytes(&self) -> usize; @@ -55,20 +58,61 @@ pub trait NineP: Sized { /// ensure is sized to be at least [NineP::n_bytes]. fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError>; - /// Construct a new reader for parsing this type from a source of bytes - fn reader() -> Self::Reader; + /// Serialize into a byte buffer ready for transmission. + fn write_9p_bytes(&self) -> Result, WriteError> { + let mut buf = vec![0; self.n_bytes()]; + self.write_bytes(&mut buf)?; + + Ok(buf) + } + + /// This is not a normal async function. It is used to set up a sans-io state machine that + /// can be driven by a concrete implementation. + /// + /// # Safety + /// Implementations of `read` need to ensure that the only await points they contain are + /// from calls to the [request_bytes] macro. + unsafe fn read(state: &State) -> impl Future> + Send; } -/// A paired helper type for decoding a [Format9p] type from a bytestream. -pub trait Read9p: Sized + Send { - /// The parent [Format9p] type being decoded into - type T: NineP; +/// Helper struct for awaiting a Future that returns pending once so we can return control to the +/// poll loop and perform IO. +struct Yield(bool); +impl Future for Yield { + type Output = (); + fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<()> { + if self.0 { + Poll::Ready(()) + } else { + self.0 = true; + Poll::Pending + } + } +} + +/// Shared state between a [NineP] impl and a parent read loop that is performing IO. +#[derive(Default, Debug, Clone)] +pub struct State(pub(crate) Arc>); - /// The number of bytes that need to be passed to the next call to [Read9p::accept_bytes]. - fn needs_bytes(&self) -> usize; +// SAFETY: StateInner is only accessable in this crate +unsafe impl Send for State {} +// SAFETY: StateInner is only accessable in this crate +unsafe impl Sync for State {} + +#[derive(Default, Debug)] +pub(crate) struct StateInner { + pub(crate) n: usize, + pub(crate) buf: Option>, +} - /// Accept the requested number of bytes and return a new [NinepReader] state machine. - fn accept_bytes(self, bytes: &[u8]) -> io::Result>; +/// Request a specific number of bytes from the parent poll loop and then yield to that poll loop +/// so it can perform IO and provide the requested data. +macro_rules! request_bytes { + ($s:expr, $n:expr) => {{ + (*$s.0.get()).n = $n; + Yield(false).await; + (*$s.0.get()).buf.take().unwrap() + }}; } /// wrapper around uX::from_le_bytes that accepts a slice rather than a fixed size array @@ -79,52 +123,12 @@ macro_rules! from_le_bytes { }; } -/// Attempt to read a [NineP] value from a byte buffer that contains sufficient data without -/// requiring further IO. -pub fn try_read_9p_bytes(mut bytes: &[u8]) -> io::Result { - let mut nr = NinepReader::Pending(T::reader()); - - loop { - match nr { - NinepReader::Pending(r) => { - let n = T::Reader::needs_bytes(&r); - nr = r.accept_bytes(&bytes[0..n])?; - bytes = &bytes[n..]; - } - - NinepReader::Complete(t) => return Ok(t), - } - } -} - -/// Serialize `t` into a byte buffer ready for transmission. -pub fn write_9p_bytes(t: &T) -> Result, WriteError> { - let mut buf = vec![0; t.n_bytes()]; - t.write_bytes(&mut buf)?; - - Ok(buf) -} - -/// State machine enum for writing concrete readers from a [Format9p] type. -#[derive(Debug)] -pub enum NinepReader -where - T: NineP, -{ - /// A reader that requires more input to complete - Pending(T::Reader), - /// A reader that completed successfully - Complete(T), -} - // Unsigned integer types can all be treated the same way so we stamp them out using a macro. // They are written and read in their little-endian byte form. macro_rules! impl_u { ($($ty:ty),+) => { $( impl NineP for $ty { - type Reader = $ty; - fn n_bytes(&self) -> usize { size_of::<$ty>() } @@ -134,20 +138,9 @@ macro_rules! impl_u { Ok(()) } - fn reader() -> $ty { - 0 - } - } - - impl Read9p for $ty { - type T = $ty; - - fn needs_bytes(&self) -> usize { - size_of::<$ty>() - } - - fn accept_bytes(self, bytes: &[u8]) -> io::Result> { - Ok(NinepReader::Complete(from_le_bytes!($ty, bytes))) + async unsafe fn read(state: &State) -> io::Result<$ty> { + let buf = request_bytes!(state, size_of::<$ty>()); + Ok(from_le_bytes!($ty, buf)) } } )+ @@ -167,8 +160,6 @@ impl_u!(u8, u16, u32, u64); // which include no final zero byte. The NUL character is illegal in all text strings // in 9P, and is therefore excluded from file names, user names, and so on. impl NineP for String { - type Reader = StringReader; - fn n_bytes(&self) -> usize { size_of::() + self.len() } @@ -185,50 +176,12 @@ impl NineP for String { Ok(()) } - fn reader() -> StringReader { - StringReader::Start - } -} - -#[derive(Debug)] -#[allow(missing_docs)] -pub enum StringReader { - Start, - WithLen(usize), -} - -impl Read9p for StringReader { - type T = String; - - fn needs_bytes(&self) -> usize { - match self { - Self::Start => size_of::(), - Self::WithLen(len) => *len, - } - } - - fn accept_bytes(self, mut bytes: &[u8]) -> io::Result> { - match self { - Self::Start => { - let len = from_le_bytes!(u16, bytes) as usize; - Ok(NinepReader::Pending(Self::WithLen(len))) - } - - Self::WithLen(len) => { - let mut s = String::with_capacity(len); - bytes.read_to_string(&mut s)?; - let actual = s.len(); + async unsafe fn read(state: &State) -> io::Result { + let buf = request_bytes!(state, size_of::()); + let len = from_le_bytes!(u16, buf) as usize; + let buf = request_bytes!(state, len); - if actual < len { - return Err(io::Error::new( - ErrorKind::UnexpectedEof, - format!("unexpected end of string: wanted {len}, got {actual}"), - )); - } - - Ok(NinepReader::Complete(s)) - } - } + String::from_utf8(buf).map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string())) } } @@ -238,8 +191,6 @@ impl Read9p for StringReader { // Data items of larger or variable lengths are represented by a two-byte field specifying // a count, n, followed by n bytes of data. impl NineP for Vec { - type Reader = VecReader; - fn n_bytes(&self) -> usize { size_of::() + self.iter().map(|t| t.n_bytes()).sum::() } @@ -261,55 +212,16 @@ impl NineP for Vec { Ok(()) } - fn reader() -> VecReader { - VecReader::Start - } -} - -#[derive(Debug)] -#[allow(missing_docs)] -pub enum VecReader -where - T: NineP + fmt::Debug + Send, -{ - Start, - Reading(usize, T::Reader, Vec), -} + async unsafe fn read(state: &State) -> io::Result { + let buf = request_bytes!(state, size_of::()); + let len = from_le_bytes!(u16, buf) as usize; -impl Read9p for VecReader { - type T = Vec; - - fn needs_bytes(&self) -> usize { - match self { - Self::Start => size_of::(), - Self::Reading(_, r, _) => r.needs_bytes(), + let mut buf = Vec::with_capacity(len); + for _ in 0..len { + buf.push(T::read(state).await?); } - } - - fn accept_bytes(self, bytes: &[u8]) -> io::Result>> { - match self { - Self::Start => { - let len = from_le_bytes!(u16, bytes) as usize; - let buf = Vec::with_capacity(len); - let r = T::reader(); - Ok(NinepReader::Pending(VecReader::Reading(len, r, buf))) - } - - Self::Reading(n, r, mut buf) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(VecReader::Reading(n, r, buf))), - - NinepReader::Complete(t) => { - buf.push(t); - if n == 1 { - Ok(NinepReader::Complete(buf)) - } else { - let r = T::reader(); - Ok(NinepReader::Pending(VecReader::Reading(n - 1, r, buf))) - } - } - }, - } + Ok(buf) } } @@ -350,7 +262,7 @@ impl TryFrom for Vec { let n = size_of::(); loop { - match try_read_9p_bytes::(bytes) { + match RawStat::read_from(&mut bytes) { Ok(rs) => { buf.push(rs); bytes = &bytes[n..]; @@ -365,8 +277,6 @@ impl TryFrom for Vec { } impl NineP for Data { - type Reader = DataReader; - fn n_bytes(&self) -> usize { size_of::() + self.0.len() } @@ -383,54 +293,17 @@ impl NineP for Data { Ok(()) } - fn reader() -> DataReader { - DataReader::Start - } -} - -#[derive(Debug)] -#[allow(missing_docs)] -pub enum DataReader { - Start, - WithLen(usize), -} - -impl Read9p for DataReader { - type T = Data; - - fn needs_bytes(&self) -> usize { - match self { - Self::Start => size_of::(), - Self::WithLen(len) => *len, + async unsafe fn read(state: &State) -> io::Result { + let buf = request_bytes!(state, size_of::()); + let len = from_le_bytes!(u32, buf) as usize; + if len > MAX_DATA_LEN { + return Err(io::Error::new( + ErrorKind::InvalidData, + format!("data field too long: max={MAX_DATA_LEN} len={len}"), + )); } - } - - fn accept_bytes(self, bytes: &[u8]) -> io::Result> { - match self { - DataReader::Start => { - let len = from_le_bytes!(u32, bytes) as usize; - if len > MAX_DATA_LEN { - return Err(io::Error::new( - ErrorKind::InvalidData, - format!("data field too long: max={MAX_DATA_LEN} len={len}"), - )); - } - - Ok(NinepReader::Pending(DataReader::WithLen(len))) - } - - DataReader::WithLen(len) => { - let actual = bytes.len(); - if actual < len { - return Err(io::Error::new( - ErrorKind::UnexpectedEof, - format!("unexpected end of data: wanted {len}, got {actual}"), - )); - } - Ok(NinepReader::Complete(Data(bytes.to_vec()))) - } - } + Ok(Data(request_bytes!(state, len))) } } @@ -480,8 +353,6 @@ macro_rules! write_fields { } impl NineP for RawStat { - type Reader = RawStatReader; - fn n_bytes(&self) -> usize { // 2 2 4 13 4 4 4 8 -> 41 41 + self.name.n_bytes() + self.uid.n_bytes() + self.gid.n_bytes() + self.muid.n_bytes() @@ -493,95 +364,37 @@ impl NineP for RawStat { ) } - fn reader() -> RawStatReader { - RawStatReader::Start - } -} - -#[derive(Debug)] -#[allow(missing_docs)] -pub enum RawStatReader { - Start, - Name(RawStat, StringReader), - Uid(RawStat, StringReader), - Gid(RawStat, StringReader), - Muid(RawStat, StringReader), -} - -impl Read9p for RawStatReader { - type T = RawStat; - - fn needs_bytes(&self) -> usize { - match self { - Self::Start => 41, - Self::Name(_, r) => r.needs_bytes(), - Self::Uid(_, r) => r.needs_bytes(), - Self::Gid(_, r) => r.needs_bytes(), - Self::Muid(_, r) => r.needs_bytes(), - } - } - - fn accept_bytes(self, bytes: &[u8]) -> io::Result> { - match self { - Self::Start => { - let size = from_le_bytes!(u16, bytes); - let ty = from_le_bytes!(u16, &bytes[2..]); - let dev = from_le_bytes!(u32, &bytes[4..]); - let qid: Qid = try_read_9p_bytes(&bytes[8..])?; - let mode = from_le_bytes!(u32, &bytes[21..]); - let atime = from_le_bytes!(u32, &bytes[25..]); - let mtime = from_le_bytes!(u32, &bytes[29..]); - let length = from_le_bytes!(u64, &bytes[33..]); - let rs = RawStat { - size, - ty, - dev, - qid, - mode, - atime, - mtime, - length, - name: String::default(), - uid: String::default(), - gid: String::default(), - muid: String::default(), - }; - - Ok(NinepReader::Pending(Self::Name(rs, StringReader::Start))) - } - - Self::Name(mut rs, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::Name(rs, r))), - NinepReader::Complete(s) => { - rs.name = s; - Ok(NinepReader::Pending(Self::Uid(rs, StringReader::Start))) - } - }, - - Self::Uid(mut rs, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::Uid(rs, r))), - NinepReader::Complete(s) => { - rs.uid = s; - Ok(NinepReader::Pending(Self::Gid(rs, StringReader::Start))) - } - }, - - Self::Gid(mut rs, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::Gid(rs, r))), - NinepReader::Complete(s) => { - rs.gid = s; - Ok(NinepReader::Pending(Self::Muid(rs, StringReader::Start))) - } - }, - - Self::Muid(mut rs, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::Muid(rs, r))), - NinepReader::Complete(s) => { - rs.muid = s; - Ok(NinepReader::Complete(rs)) - } - }, - } + async unsafe fn read(state: &State) -> io::Result { + let buf = request_bytes!(state, 41); + let bytes = buf.as_slice(); + + let size = from_le_bytes!(u16, bytes); + let ty = from_le_bytes!(u16, &bytes[2..]); + let dev = from_le_bytes!(u32, &bytes[4..]); + let qid = Qid::read_from(&mut &bytes[8..])?; + let mode = from_le_bytes!(u32, &bytes[21..]); + let atime = from_le_bytes!(u32, &bytes[25..]); + let mtime = from_le_bytes!(u32, &bytes[29..]); + let length = from_le_bytes!(u64, &bytes[33..]); + let name = String::read(state).await?; + let uid = String::read(state).await?; + let gid = String::read(state).await?; + let muid = String::read(state).await?; + + Ok(RawStat { + size, + ty, + dev, + qid, + mode, + atime, + mtime, + length, + name, + uid, + gid, + muid, + }) } } @@ -606,201 +419,22 @@ macro_rules! impl_message_datatype { pub $field: $ty, )* } - impl_message_datatype!(@tuple $struct $reader $($field: $ty),*); - }; - - // No fields - (@tuple $struct:ident $reader:ident) => { - impl NineP for $struct { - type Reader = $reader; - fn n_bytes(&self) -> usize { 0 } - fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError> { Ok(()) } - fn reader() -> $reader { $reader($ty::reader()) } - } - - #[derive(Debug)] - #[allow(missing_docs)] - pub struct $reader; - impl Read9p for $reader { - type T = $struct; - fn needs_bytes(&self) -> usize { 0 } - fn accept_bytes(self, bytes: &[u8]) -> io::Result> { - debug_assert!(bytes.is_empty()); - Ok(NinepReader::Complete($struct { })) - } - } - }; - - // Single field - (@tuple $struct:ident $reader:ident $field:ident: $ty:ty) => { - impl NineP for $struct { - type Reader = $reader; - fn n_bytes(&self) -> usize { self.$field.n_bytes() } - fn write_bytes(&self, buf: &mut [u8]) -> Result<(), WriteError> { self.$field.write_bytes(buf) } - fn reader() -> $reader { $reader(<$ty as NineP>::reader()) } - } - - #[derive(Debug)] - #[allow(missing_docs)] - pub struct $reader($ty::Reader); - impl Read9p for $reader { - type T = $struct; - fn needs_bytes(&self) -> usize { self.0.needs_bytes() } - fn accept_bytes(self, bytes: &[u8]) -> io::Result> { - match self.0.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self(r))), - NinepReader::Complete($field) => Ok(NinepReader::Complete($struct { $field })) - } - } - } - }; - - // Two fields - (@tuple $struct:ident $reader:ident $f1:ident: $t1:ty, $f2:ident: $t2:ty) => { - impl NineP for $struct { - type Reader = $reader; - fn n_bytes(&self) -> usize { - self.$f1.n_bytes() + self.$f2.n_bytes() - } - fn write_bytes(&self, mut buf: &mut [u8]) -> Result<(), WriteError> { - write_fields!(buf, self, $f1, $f2) - } - fn reader() -> $reader { $reader::T1(<$t1 as NineP>::reader()) } - } - - #[derive(Debug)] - #[allow(missing_docs)] - pub enum $reader { - T1($t1::Reader), - T2($t1, $t2::Reader), - } - impl Read9p for $reader { - type T = $struct; - fn needs_bytes(&self) -> usize { - match self { - Self::T1(r) => r.needs_bytes(), - Self::T2(_, r) => r.needs_bytes(), - } - } - fn accept_bytes(self, bytes: &[u8]) -> io::Result { - match self { - Self::T1(r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T1(r))), - NinepReader::Complete(t1) => Ok(NinepReader::Pending(Self::T2(t1, <$t2 as NineP>::reader()))), - }, - Self::T2($f1, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T2($f1, r))), - NinepReader::Complete($f2) => Ok(NinepReader::Complete($struct { $f1, $f2 })), - }, - } - } - } - }; - // Three fields - (@tuple $struct:ident $reader:ident $f1:ident: $t1:ty, $f2:ident: $t2:ty, $f3:ident: $t3:ty) => { impl NineP for $struct { - type Reader = $reader; fn n_bytes(&self) -> usize { - self.$f1.n_bytes() + self.$f2.n_bytes() + self.$f3.n_bytes() - } - fn write_bytes(&self, mut buf: &mut [u8]) -> Result<(), WriteError> { - write_fields!(buf, self, $f1, $f2, $f3) - } - fn reader() -> $reader { $reader::T1(<$t1 as NineP>::reader()) } - } - - #[derive(Debug)] - #[allow(missing_docs)] - pub enum $reader { - T1(<$t1 as NineP>::Reader), - T2($t1, <$t2 as NineP>::Reader), - T3($t1, $t2, <$t3 as NineP>::Reader), - } - impl Read9p for $reader { - type T = $struct; - fn needs_bytes(&self) -> usize { - match self { - Self::T1(r) => r.needs_bytes(), - Self::T2(_, r) => r.needs_bytes(), - Self::T3(_, _, r) => r.needs_bytes(), - } - } - fn accept_bytes(self, bytes: &[u8]) -> io::Result> { - match self { - Self::T1(r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T1(r))), - NinepReader::Complete(t1) => Ok(NinepReader::Pending(Self::T2(t1, <$t2 as NineP>::reader()))), - }, - Self::T2(t1, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T2(t1, r))), - NinepReader::Complete(t2) => { - Ok(NinepReader::Pending(Self::T3(t1, t2, <$t3 as NineP>::reader()))) - } - }, - Self::T3($f1, $f2, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T3($f1, $f2, r))), - NinepReader::Complete($f3) => Ok(NinepReader::Complete($struct { $f1, $f2, $f3 })), - }, - } + #[allow(unused_mut)] + let mut n = 0; + $(n += self.$field.n_bytes();)* + n } - } - }; - // Four fields - (@tuple $struct:ident $reader:ident $f1:ident: $t1:ty, $f2:ident: $t2:ty, $f3:ident: $t3:ty, $f4:ident: $t4:ty) => { - impl NineP for $struct { - type Reader = $reader; - fn n_bytes(&self) -> usize { - self.$f1.n_bytes() + self.$f2.n_bytes() + self.$f3.n_bytes() + self.$f4.n_bytes() - } fn write_bytes(&self, mut buf: &mut [u8]) -> Result<(), WriteError> { - write_fields!(buf, self, $f1, $f2, $f3, $f4) + write_fields!(buf, self, $($field),*) } - fn reader() -> $reader { $reader::T1(<$t1 as NineP>::reader()) } - } - #[derive(Debug)] - #[allow(missing_docs)] - pub enum $reader { - T1(T1::Reader), - T2(T1, T2::Reader), - T3(T1, T2, T3::Reader), - T4(T1, T2, T3, T4::Reader), - } - impl Read9p for $reader { - type T = $struct; - fn needs_bytes(&self) -> usize { - match self { - Self::T1(r) => r.needs_bytes(), - Self::T2(_, r) => r.needs_bytes(), - Self::T3(_, _, r) => r.needs_bytes(), - Self::T4(_, _, _, r) => r.needs_bytes(), - } - } - fn accept_bytes(self, bytes: &[u8]) -> io::Result> { - match self { - Self::T1(r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T1(r))), - NinepReader::Complete(t1) => Ok(NinepReader::Pending(Self::T2(t1, <$t2 as NineP>::reader()))), - }, - Self::T2(t1, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T2(t1, r))), - NinepReader::Complete(t2) => { - Ok(NinepReader::Pending(Self::T3(t1, t2, <$t3 as NineP>::reader()))) - } - }, - Self::T3(t1, t2, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T3(t1, t2, r))), - NinepReader::Complete(t3) => { - Ok(NinepReader::Pending(Self::T4(t1, t2, t3, <$t4 as NineP>::reader()))) - } - }, - Self::T4($f1, $f2, $f3, r) => match r.accept_bytes(bytes)? { - NinepReader::Pending(r) => Ok(NinepReader::Pending(Self::T4($f1, $f2, $f3, r))), - NinepReader::Complete($f4) => Ok(NinepReader::Complete($struct { $f1, $f2, $f3, $f4 })), - }, - } + async unsafe fn read(state: &State) -> io::Result { + $(let $field = <$ty>::read(&state).await?;)* + Ok($struct { $($field),* }) } } }; @@ -883,14 +517,12 @@ impl MessageType { /// Helper for implementing Tmessage and Rmessage macro_rules! impl_message_format { ( - $message_ty:ident, $reader:ident, $enum_ty:ident, $err:expr; + $message_ty:ident, $enum_ty:ident, $err:expr; $($enum_variant:ident => $message_variant:ident { $($field:ident: $ty:ty,)* })+ ) => { impl NineP for $message_ty { - type Reader = $reader; - fn n_bytes(&self) -> usize { let content_size = match &self.content { $( @@ -932,69 +564,31 @@ macro_rules! impl_message_format { Ok(()) } - fn reader() -> $reader { - $reader::Start - } - } + #[allow(unused_assignments)] + async unsafe fn read(state: &State) -> io::Result { + let buf = request_bytes!(state, size_of::()); + let len = from_le_bytes!(u32, buf) as usize; -// pub trait Read9p: Sized { -// type T: NineP; -// fn needs_bytes(&self) -> usize; -// fn accept_bytes(self, bytes: &[u8]) -> io::Result>; -// } - - #[derive(Debug)] - #[allow(missing_docs)] - pub enum $reader { - Start, - WithSize(usize), - } + let bytes = request_bytes!(state, len-4); + let ty = from_le_bytes!(u8, &bytes); + let tag = from_le_bytes!(u16, &bytes[1..]); + let mut cur = Cursor::new(bytes); + cur.set_position(3); - impl Read9p for $reader { - type T = $message_ty; + let content = match MessageType(ty) { + $( + MessageType::$message_variant => $enum_ty::$enum_variant { + $($field: <$ty>::read_from(&mut cur)?),* + }, + )+ - fn needs_bytes(&self) -> usize { - match self { - Self::Start => size_of::(), - // the size field includes the number of bytes for the field itself so we - // trim that off before decoding the rest of the message - Self::WithSize(n) => n - 4, - } - } + MessageType(ty) => return Err(io::Error::new( + ErrorKind::InvalidData, + format!($err, ty), + )), + }; - #[allow(unused_assignments)] - fn accept_bytes(self, bytes: &[u8]) -> io::Result> { - match self { - Self::Start => { - let size = from_le_bytes!(u32, bytes) as usize; - Ok(NinepReader::Pending($reader::WithSize(size))) - } - Self::WithSize(_) => { - let ty = from_le_bytes!(u8, bytes); - let tag = from_le_bytes!(u16, &bytes[1..]); - let mut offset = 3; - let content = match MessageType(ty) { - $( - MessageType::$message_variant => $enum_ty::$enum_variant { - $( - $field: { - let val: $ty = try_read_9p_bytes(&bytes[offset..])?; - offset += val.n_bytes(); - val - }, - )* - }, - )+ - - MessageType(ty) => return Err(io::Error::new( - ErrorKind::InvalidData, - format!($err, ty), - )), - }; - - Ok(NinepReader::Complete($message_ty { tag, content })) - } - } + Ok($message_ty { tag, content }) } } }; @@ -1043,7 +637,7 @@ macro_rules! impl_tdata { } impl_message_format!( - Tmessage, TmessageReader, Tdata, "invalid message type for t-message: {}"; + Tmessage, Tdata, "invalid message type for t-message: {}"; $($enum_variant => $message_variant { $($field: $ty,)* })+ @@ -1224,7 +818,7 @@ macro_rules! impl_rdata { } impl_message_format!( - Rmessage, RmessageReader, Rdata, "invalid message type for r-message: {}"; + Rmessage, Rdata, "invalid message type for r-message: {}"; $($enum_variant => $message_variant { $($field: $ty,)* })+ @@ -1336,12 +930,15 @@ mod tests { #[test] fn uint_decode() { - let buf: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]; - - assert_eq!(0x01, try_read_9p_bytes::(&buf).unwrap()); - assert_eq!(0x2301, try_read_9p_bytes::(&buf).unwrap()); - assert_eq!(0x67452301, try_read_9p_bytes::(&buf).unwrap()); - assert_eq!(0xefcdab8967452301, try_read_9p_bytes::(&buf).unwrap()); + let buf: Vec = vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]; + + assert_eq!(0x01, u8::read_from(&mut buf.as_slice()).unwrap()); + assert_eq!(0x2301, u16::read_from(&mut buf.as_slice()).unwrap()); + assert_eq!(0x67452301, u32::read_from(&mut buf.as_slice()).unwrap()); + assert_eq!( + 0xefcdab8967452301, + u64::read_from(&mut buf.as_slice()).unwrap() + ); } #[test_case("test", &[0x04, 0x00, 0x74, 0x65, 0x73, 0x74]; "single byte chars only")] @@ -1354,7 +951,7 @@ mod tests { #[test] fn string_encode(s: &str, bytes: &[u8]) { let s = s.to_string(); - let buf = write_9p_bytes(&s).unwrap(); + let buf = s.write_9p_bytes().unwrap(); assert_eq!(&buf, bytes); } @@ -1378,8 +975,8 @@ mod tests { where T: NineP + PartialEq + fmt::Debug, { - let buf = write_9p_bytes(&t1).unwrap(); - let t2 = try_read_9p_bytes::(&buf).unwrap(); + let buf = t1.write_9p_bytes().unwrap(); + let t2 = T::read_from(&mut buf.as_slice()).unwrap(); assert_eq!(t1, t2); } diff --git a/crates/ninep/src/sync/mod.rs b/crates/ninep/src/sync/mod.rs index 82c6b28..8a3f40e 100644 --- a/crates/ninep/src/sync/mod.rs +++ b/crates/ninep/src/sync/mod.rs @@ -1,12 +1,18 @@ //! A synchronous implementation of 9p Servers and Clients use crate::{ - sansio::protocol::{NineP, NinepReader, Rdata, Read9p, Rmessage}, + sansio::{ + protocol::{NineP, Rdata, Rmessage, State}, + stub_waker, + }, Result, }; use std::{ + future::Future, io::{self, Read, Write}, net::TcpStream, os::unix::net::UnixStream, + pin::pin, + task::{Context, Poll}, }; pub mod client; @@ -24,23 +30,23 @@ pub trait SyncNineP: NineP { } /// Decode self from 9p protocol bytes coming from the given [SyncStream]. - #[allow(clippy::uninit_vec)] fn read_from(r: &mut R) -> io::Result { - let mut nr = NinepReader::Pending(Self::reader()); - let mut buf = Vec::new(); + let waker = stub_waker(); + let mut context = Context::from_waker(&waker); + let s = State::default(); + // SAFETY: assumes the impl of Read9p is a valid future for us to poll + let mut fut = unsafe { pin!(Self::read(&s)) }; loop { - match nr { - NinepReader::Pending(r9) => { - let n = Self::Reader::needs_bytes(&r9); - buf.reserve(n.saturating_sub(buf.len())); - // SAFETY: we've just reserved sufficient capacity - unsafe { buf.set_len(n) }; + match fut.as_mut().poll(&mut context) { + Poll::Ready(val) => return val, + // SAFETY: s is only shared with the future we're polling + Poll::Pending => unsafe { + let n = (*s.0.get()).n; + let mut buf = vec![0; n]; r.read_exact(&mut buf)?; - nr = r9.accept_bytes(&buf[0..n])?; - } - - NinepReader::Complete(t) => return Ok(t), + (*s.0.get()).buf = Some(buf); + }, } } } diff --git a/crates/ninep/src/tokio/mod.rs b/crates/ninep/src/tokio/mod.rs index 16d2c53..c678c01 100644 --- a/crates/ninep/src/tokio/mod.rs +++ b/crates/ninep/src/tokio/mod.rs @@ -1,9 +1,18 @@ //! Tokio based asynchronous implementation of 9p Servers and Clients use crate::{ - sansio::protocol::{NineP, NinepReader, Rdata, Read9p, Rmessage}, + sansio::{ + protocol::{NineP, Rdata, Rmessage, State}, + stub_waker, + }, Result, }; -use std::{io, marker::Unpin}; +use std::{ + future::Future, + io, + marker::Unpin, + pin::pin, + task::{Context, Poll}, +}; use tokio::{ io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, net::{TcpStream, UnixStream}, @@ -12,43 +21,69 @@ use tokio::{ pub mod client; pub mod server; -/// Synchronous IO support for reading and writing 9p messages -#[async_trait::async_trait] -pub trait AsyncNineP: NineP { +/// Asynchronous IO support for reading and writing 9p messages +pub trait AsyncNineP: NineP + Send + Sync { /// Encode self as bytes for the 9p protocol and write to the given [SyncStream]. - async fn write_to(&self, w: &mut W) -> io::Result<()> { - let mut buf = vec![0; self.n_bytes()]; - self.write_bytes(&mut buf) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - - w.write_all(&buf).await + fn write_to(&self, w: &mut W) -> impl Future> + Send + where + W: AsyncWrite + Unpin + Send, + { + write_to(self, w) } /// Decode self from 9p protocol bytes coming from the given [SyncStream]. - #[allow(clippy::uninit_vec)] - async fn read_from(r: &mut R) -> io::Result { - let mut nr = NinepReader::Pending(Self::reader()); - let mut buf = Vec::new(); + fn read_from(r: &mut R) -> impl Future> + Send + where + R: AsyncRead + Unpin + Send, + { + read_from(r) + } +} + +impl AsyncNineP for T where T: NineP + Send + Sync {} - loop { - match nr { - NinepReader::Pending(r9) => { - let n = Self::Reader::needs_bytes(&r9); - buf.reserve(n.saturating_sub(buf.len())); - // SAFETY: we've just reserved sufficient capacity - unsafe { buf.set_len(n) }; - r.read_exact(&mut buf).await?; - nr = r9.accept_bytes(&buf[0..n])?; - } +// write_to and read_from are written as free functions so we can use async/await here while also +// explicitly requiring a Send bound on the methods of the AsyncNineP trait above. - NinepReader::Complete(t) => return Ok(t), - } +#[inline(always)] +async fn write_to(t: &T, w: &mut W) -> io::Result<()> +where + T: NineP + Sync, + W: AsyncWrite + Unpin + Send, +{ + let mut buf = vec![0; t.n_bytes()]; + t.write_bytes(&mut buf) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + + w.write_all(&buf).await +} + +#[inline(always)] +async fn read_from(r: &mut R) -> io::Result +where + T: NineP + Send, + R: AsyncRead + Unpin + Send, +{ + let waker = stub_waker(); + let s = State::default(); + + // SAFETY: assumes the impl of Read9p is a valid future for us to poll + let mut fut = unsafe { pin!(T::read(&s)) }; + loop { + let poll = fut.as_mut().poll(&mut Context::from_waker(&waker)); + match poll { + Poll::Ready(val) => return val, + // SAFETY: s is only shared with the future we're polling + Poll::Pending => unsafe { + let n = (*s.0.get()).n; + let mut buf = vec![0; n]; + r.read_exact(&mut buf).await?; + (*s.0.get()).buf = Some(buf); + }, } } } -impl AsyncNineP for T where T: NineP {} - /// A [Stream] that makes use of the standard library [Read] and [Write] traits to perform IO #[allow(async_fn_in_trait)] pub trait AsyncStream: AsyncRead + AsyncWrite + Unpin + Send + Sized + 'static { diff --git a/crates/ninep/src/tokio/server.rs b/crates/ninep/src/tokio/server.rs index 568ac32..9fdaf39 100644 --- a/crates/ninep/src/tokio/server.rs +++ b/crates/ninep/src/tokio/server.rs @@ -14,7 +14,7 @@ use crate::{ tokio::{AsyncNineP, AsyncStream}, Result, }; -use std::{collections::btree_map::Entry, fs, mem::size_of}; +use std::{collections::btree_map::Entry, fs, future::Future, mem::size_of}; use tokio::{ net::{TcpListener, UnixListener}, sync::mpsc::{unbounded_channel, Receiver, UnboundedSender}, @@ -83,11 +83,10 @@ async fn tcp_socket(port: u16) -> TcpListener { /// as such, [Serve9p] only needs to worry about maintaining `qids` for resources. /// /// The source code of [Server] is a useful reference for those wanting to learn more. -#[async_trait::async_trait] pub trait AsyncServe9p: Send + Sync + 'static { // #[allow(unused_variables)] - // async fn auth(&self, afid: u32, uname: &str, aname: &str) -> Result { - // Err("authentication not required".to_string()) + // fn auth(&self, afid: u32, uname: &str, aname: &str) -> impl Future> + Send { + // async { Err("authentication not required".to_string()) } // } /// Lookup a child node under a known parent directory by name. @@ -98,27 +97,35 @@ pub trait AsyncServe9p: Send + Sync + 'static { /// /// [Server] will ensure that this method is only called for known parents who have previously /// been identified has having [FileType::Directory]. - async fn walk( + fn walk( &self, cid: ClientId, parent_qid: u64, child: &str, uname: &str, - ) -> Result; + ) -> impl Future> + Send; /// Open an existing file in the requested mode for subsequent I/O via [read](Serve9p::read) and /// [write](Serve9p::write) calls. /// /// The return of this method is an [IoUnit] used to inform the client of the maximum number of /// bytes that will be supported per read/write call on this resource. - async fn open(&self, cid: ClientId, qid: u64, mode: Mode, uname: &str) -> Result; + fn open( + &self, + cid: ClientId, + qid: u64, + mode: Mode, + uname: &str, + ) -> impl Future> + Send; /// Clunk a currently open file. #[allow(unused_variables)] - async fn clunk(&self, cid: ClientId, qid: u64) {} + fn clunk(&self, cid: ClientId, qid: u64) -> impl Future + Send { + async {} + } /// Create a new file in the given parent directory. - async fn create( + fn create( &self, cid: ClientId, parent: u64, @@ -126,39 +133,60 @@ pub trait AsyncServe9p: Send + Sync + 'static { perm: Perm, mode: Mode, uname: &str, - ) -> Result<(FileMeta, IoUnit)>; + ) -> impl Future> + Send; /// Read `count` bytes from the requested file starting from the given `offset`. - async fn read( + fn read( &self, cid: ClientId, qid: u64, offset: usize, count: usize, uname: &str, - ) -> Result; + ) -> impl Future> + Send; /// List the contents of the given directory. - async fn read_dir(&self, cid: ClientId, qid: u64, uname: &str) -> Result>; + fn read_dir( + &self, + cid: ClientId, + qid: u64, + uname: &str, + ) -> impl Future>> + Send; /// Write the given `data` to the requested file starting at `offset` - async fn write( + fn write( &self, cid: ClientId, qid: u64, offset: usize, data: Vec, uname: &str, - ) -> Result; + ) -> impl Future> + Send; /// Remove the requested file from the filesystem. - async fn remove(&self, cid: ClientId, qid: u64, uname: &str) -> Result<()>; + fn remove( + &self, + cid: ClientId, + qid: u64, + uname: &str, + ) -> impl Future> + Send; /// Request a machine independent "directory entry" for the given resource. - async fn stat(&self, cid: ClientId, qid: u64, uname: &str) -> Result; + fn stat( + &self, + cid: ClientId, + qid: u64, + uname: &str, + ) -> impl Future> + Send; /// Attempt to set the machine independent "directory entry" for the given resource. - async fn write_stat(&self, cid: ClientId, qid: u64, stat: Stat, uname: &str) -> Result<()>; + fn write_stat( + &self, + cid: ClientId, + qid: u64, + stat: Stat, + uname: &str, + ) -> impl Future> + Send; } impl Server From ec3c2b019c4cbf33a47b361804a4017d4b6d678e Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Tue, 25 Feb 2025 09:10:33 +0000 Subject: [PATCH 07/12] removing fsm example --- crates/ninep/examples/async_fsm.rs | 153 ----------------------------- 1 file changed, 153 deletions(-) delete mode 100644 crates/ninep/examples/async_fsm.rs diff --git a/crates/ninep/examples/async_fsm.rs b/crates/ninep/examples/async_fsm.rs deleted file mode 100644 index ce0b76d..0000000 --- a/crates/ninep/examples/async_fsm.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! This is a little exploration of using async/await + a dummy Waker to simplify writing sans-io -//! state machine code. -use std::{ - cell::UnsafeCell, - future::{Future, IntoFuture}, - io::{Cursor, Read}, - pin::{pin, Pin}, - sync::Arc, - task::{Context, Poll, Wake, Waker}, -}; -use tokio::io::{AsyncRead, AsyncReadExt}; - -#[tokio::main] -async fn main() { - // "Hello, 世界" with a u16 length header - // - // In 9p, data items of larger or variable lengths are represented by a two-byte field - // specifying a count, n, followed by n bytes of data. Text strings are represented this way, - // with the text itself stored as a UTF-8 encoded sequence of Unicode characters without a - // trailing null byte. - let mut cur = Cursor::new(vec![ - 0x0d, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c, - ]); - - println!(">> reading using std::io::Read"); - let s: String = read_9p_sync(&mut cur); - println!(" got val: {s:?}\n"); - - cur.set_position(0); - - println!(">> reading using tokio::io::AsyncRead"); - let s: String = read_9p_async(&mut cur).await; - println!(" got val: {s:?}"); -} - -fn read_9p_sync(r: &mut R) -> T -where - T: Read9p, - R: Read, -{ - let waker = Waker::from(Arc::new(StubWaker)); - let mut context = Context::from_waker(&waker); - let s = State::default(); - - // SAFETY: assumes the impl of Read9p is a valid future for us to poll - let mut fut = unsafe { pin!(T::read(s.clone()).into_future()) }; - loop { - match fut.as_mut().poll(&mut context) { - Poll::Ready(val) => return val, - Poll::Pending => unsafe { - let n = (*s.0.get()).n; - println!("{n} bytes requested"); - let mut buf = vec![0; n]; - r.read_exact(&mut buf).unwrap(); - (*s.0.get()).buf = Some(buf); - }, - } - } -} - -async fn read_9p_async(r: &mut R) -> T -where - T: Read9p, - R: AsyncRead + Unpin, -{ - let waker = Waker::from(Arc::new(StubWaker)); - let mut context = Context::from_waker(&waker); - let s = State::default(); - - // SAFETY: assumes the impl of Read9p is a valid future for us to poll - let mut fut = unsafe { pin!(T::read(s.clone()).into_future()) }; - loop { - match fut.as_mut().poll(&mut context) { - Poll::Ready(val) => return val, - Poll::Pending => unsafe { - let n = (*s.0.get()).n; - println!("{n} bytes requested"); - let mut buf = vec![0; n]; - r.read_exact(&mut buf).await.unwrap(); - (*s.0.get()).buf = Some(buf); - }, - } - } -} - -/// A no-op waker that is just used to create a [Context] in order to poll the [Read9p] future. -struct StubWaker; -impl Wake for StubWaker { - fn wake(self: Arc) {} - fn wake_by_ref(self: &Arc) {} -} - -/// Helper struct for awaiting a Future that returns pending once so we can return control to the -/// poll loop and perform IO. -struct Yield(bool); -impl Future for Yield { - type Output = (); - fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<()> { - if self.0 { - Poll::Ready(()) - } else { - self.0 = true; - Poll::Pending - } - } -} - -/// Shared state between a [NineP] impl and a parent read loop that is performing IO. -#[derive(Default, Debug, Clone)] -pub struct State(pub(crate) Arc>); - -// SAFETY: StateInner is only accessable in this crate -unsafe impl Send for State {} -// SAFETY: StateInner is only accessable in this crate -unsafe impl Sync for State {} - -#[derive(Default, Debug)] -struct StateInner { - n: usize, - buf: Option>, -} - -/// Request a specific number of bytes from the parent poll loop and then yield to that poll loop -/// so it can perform IO and provide the requested data. -macro_rules! request_bytes { - ($s:expr, $n:expr) => {{ - (*$s.0.get()).n = $n; - Yield(false).await; - (*$s.0.get()).buf.take().unwrap() - }}; -} - -/// # Safety -/// The read method of this trait requires that you only yield view the [request_bytes] macro. -unsafe trait Read9p { - /// # Safety - /// Implementations of `read` need to ensure that the only await points they contain are - /// from calls to the [request_bytes] macro. - unsafe fn read(state: State) -> impl Future + Send; -} - -#[allow(async_fn_in_trait)] -unsafe impl Read9p for String { - async unsafe fn read(state: State) -> Self { - let n = size_of::(); - let buf = request_bytes!(state, n); - let data = buf[0..n].try_into().unwrap(); - let len = u16::from_le_bytes(data) as usize; - let buf = request_bytes!(state, len); - - String::from_utf8(buf).unwrap() - } -} From 0d1b92d28ee8226521eab0891b7e31ab10f6cbcb Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Tue, 25 Feb 2025 13:34:32 +0000 Subject: [PATCH 08/12] storing shared state in the Waker --- crates/ninep/src/sansio/mod.rs | 24 ++- crates/ninep/src/sansio/protocol.rs | 261 ++++++++++++++++------------ crates/ninep/src/sync/mod.rs | 27 +-- crates/ninep/src/tokio/mod.rs | 22 +-- src/buffer/internal.rs | 4 +- 5 files changed, 188 insertions(+), 150 deletions(-) diff --git a/crates/ninep/src/sansio/mod.rs b/crates/ninep/src/sansio/mod.rs index 1dd296f..e3e067b 100644 --- a/crates/ninep/src/sansio/mod.rs +++ b/crates/ninep/src/sansio/mod.rs @@ -6,8 +6,8 @@ use crate::{ Result, }; use std::{ - sync::Arc, - task::{Wake, Waker}, + sync::{Arc, Mutex}, + task::Wake, }; pub mod protocol; @@ -22,13 +22,19 @@ impl From<(u16, Result)> for Rmessage { } } -struct StubWaker; -impl Wake for StubWaker { - fn wake(self: Arc) {} - fn wake_by_ref(self: &Arc) {} +/// Shared state between a NineP impl and a parent read loop that is performing IO. +#[derive(Default, Debug)] +pub(crate) struct State { + pub(crate) inner: Mutex, +} + +#[derive(Default, Debug)] +pub(crate) struct StateInner { + pub(crate) n: usize, + pub(crate) buf: Option>, } -/// A no-op waker that is just used to create a context for driving a NineP read loop. -pub(crate) fn stub_waker() -> Waker { - Waker::from(Arc::new(StubWaker)) +impl Wake for State { + fn wake(self: Arc) {} + fn wake_by_ref(self: &Arc) {} } diff --git a/crates/ninep/src/sansio/protocol.rs b/crates/ninep/src/sansio/protocol.rs index 1419d77..b3e48ca 100644 --- a/crates/ninep/src/sansio/protocol.rs +++ b/crates/ninep/src/sansio/protocol.rs @@ -1,15 +1,13 @@ //! Sans-io 9p protocol implementation //! //! http://man.cat-v.org/plan_9/5/ -use crate::sync::SyncNineP; +use crate::{sansio::State, sync::SyncNineP}; use std::{ - cell::UnsafeCell, fmt, future::Future, io::{self, Cursor, ErrorKind}, mem::size_of, pin::Pin, - sync::Arc, task::{Context, Poll}, }; @@ -72,46 +70,62 @@ pub trait NineP: Sized { /// # Safety /// Implementations of `read` need to ensure that the only await points they contain are /// from calls to the [request_bytes] macro. - unsafe fn read(state: &State) -> impl Future> + Send; + unsafe fn read() -> impl Future> + Send; } /// Helper struct for awaiting a Future that returns pending once so we can return control to the /// poll loop and perform IO. -struct Yield(bool); -impl Future for Yield { - type Output = (); - fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<()> { - if self.0 { - Poll::Ready(()) +struct RequestBytes { + polled: bool, + n: usize, +} + +impl Future for RequestBytes { + type Output = Vec; + fn poll(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + if self.polled { + // SAFETY: we can only poll this future using a waker wrapping State + let data = unsafe { + let state = (ctx.waker().data() as *mut () as *mut State) + .as_mut() + .unwrap_unchecked(); + state + .inner + .lock() + .unwrap_unchecked() + .buf + .take() + .unwrap_unchecked() + }; + + Poll::Ready(data) } else { - self.0 = true; + self.polled = true; + // SAFETY: we can only poll this future using a waker wrapping State + unsafe { + (ctx.waker().data() as *mut () as *mut State) + .as_mut() + .unwrap_unchecked() + .inner + .lock() + .unwrap_unchecked() + .n = self.n; + }; + Poll::Pending } } } -/// Shared state between a [NineP] impl and a parent read loop that is performing IO. -#[derive(Default, Debug, Clone)] -pub struct State(pub(crate) Arc>); - -// SAFETY: StateInner is only accessable in this crate -unsafe impl Send for State {} -// SAFETY: StateInner is only accessable in this crate -unsafe impl Sync for State {} - -#[derive(Default, Debug)] -pub(crate) struct StateInner { - pub(crate) n: usize, - pub(crate) buf: Option>, -} - /// Request a specific number of bytes from the parent poll loop and then yield to that poll loop /// so it can perform IO and provide the requested data. macro_rules! request_bytes { - ($s:expr, $n:expr) => {{ - (*$s.0.get()).n = $n; - Yield(false).await; - (*$s.0.get()).buf.take().unwrap() + ($n:expr) => {{ + RequestBytes { + polled: false, + n: $n, + } + .await }}; } @@ -119,7 +133,7 @@ macro_rules! request_bytes { macro_rules! from_le_bytes { ($ty:ty, $bytes:expr) => { // SAFETY: we know we are setting the correct array length - unsafe { <$ty>::from_le_bytes($bytes[0..size_of::<$ty>()].try_into().unwrap_unchecked()) } + <$ty>::from_le_bytes($bytes[0..size_of::<$ty>()].try_into().unwrap_unchecked()) }; } @@ -138,9 +152,12 @@ macro_rules! impl_u { Ok(()) } - async unsafe fn read(state: &State) -> io::Result<$ty> { - let buf = request_bytes!(state, size_of::<$ty>()); - Ok(from_le_bytes!($ty, buf)) + async unsafe fn read() -> io::Result<$ty> { + // SAFETY: we are only awaiting via request_bytes + unsafe { + let buf = request_bytes!(size_of::<$ty>()); + Ok(from_le_bytes!($ty, buf)) + } } } )+ @@ -176,12 +193,15 @@ impl NineP for String { Ok(()) } - async unsafe fn read(state: &State) -> io::Result { - let buf = request_bytes!(state, size_of::()); - let len = from_le_bytes!(u16, buf) as usize; - let buf = request_bytes!(state, len); + async unsafe fn read() -> io::Result { + // SAFETY: we are only awaiting via request_bytes + unsafe { + let len = u16::read().await? as usize; + let buf = request_bytes!(len); - String::from_utf8(buf).map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string())) + String::from_utf8(buf) + .map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string())) + } } } @@ -212,16 +232,17 @@ impl NineP for Vec { Ok(()) } - async unsafe fn read(state: &State) -> io::Result { - let buf = request_bytes!(state, size_of::()); - let len = from_le_bytes!(u16, buf) as usize; + async unsafe fn read() -> io::Result { + // SAFETY: we are only awaiting via request_bytes + unsafe { + let len = u16::read().await? as usize; + let mut buf = Vec::with_capacity(len); + for _ in 0..len { + buf.push(T::read().await?); + } - let mut buf = Vec::with_capacity(len); - for _ in 0..len { - buf.push(T::read(state).await?); + Ok(buf) } - - Ok(buf) } } @@ -293,17 +314,19 @@ impl NineP for Data { Ok(()) } - async unsafe fn read(state: &State) -> io::Result { - let buf = request_bytes!(state, size_of::()); - let len = from_le_bytes!(u32, buf) as usize; - if len > MAX_DATA_LEN { - return Err(io::Error::new( - ErrorKind::InvalidData, - format!("data field too long: max={MAX_DATA_LEN} len={len}"), - )); - } + async unsafe fn read() -> io::Result { + // SAFETY: we are only awaiting via request_bytes + unsafe { + let len = u32::read().await? as usize; + if len > MAX_DATA_LEN { + return Err(io::Error::new( + ErrorKind::InvalidData, + format!("data field too long: max={MAX_DATA_LEN} len={len}"), + )); + } - Ok(Data(request_bytes!(state, len))) + Ok(Data(request_bytes!(len))) + } } } @@ -364,37 +387,41 @@ impl NineP for RawStat { ) } - async unsafe fn read(state: &State) -> io::Result { - let buf = request_bytes!(state, 41); - let bytes = buf.as_slice(); - - let size = from_le_bytes!(u16, bytes); - let ty = from_le_bytes!(u16, &bytes[2..]); - let dev = from_le_bytes!(u32, &bytes[4..]); - let qid = Qid::read_from(&mut &bytes[8..])?; - let mode = from_le_bytes!(u32, &bytes[21..]); - let atime = from_le_bytes!(u32, &bytes[25..]); - let mtime = from_le_bytes!(u32, &bytes[29..]); - let length = from_le_bytes!(u64, &bytes[33..]); - let name = String::read(state).await?; - let uid = String::read(state).await?; - let gid = String::read(state).await?; - let muid = String::read(state).await?; - - Ok(RawStat { - size, - ty, - dev, - qid, - mode, - atime, - mtime, - length, - name, - uid, - gid, - muid, - }) + async unsafe fn read() -> io::Result { + // SAFETY: we are only awaiting via request_bytes + unsafe { + // Request the fixed sized data before the strings in one block up front + let buf = request_bytes!(41); + let bytes = buf.as_slice(); + + let size = from_le_bytes!(u16, bytes); + let ty = from_le_bytes!(u16, &bytes[2..]); + let dev = from_le_bytes!(u32, &bytes[4..]); + let qid = Qid::read_from(&mut &bytes[8..])?; + let mode = from_le_bytes!(u32, &bytes[21..]); + let atime = from_le_bytes!(u32, &bytes[25..]); + let mtime = from_le_bytes!(u32, &bytes[29..]); + let length = from_le_bytes!(u64, &bytes[33..]); + let name = String::read().await?; + let uid = String::read().await?; + let gid = String::read().await?; + let muid = String::read().await?; + + Ok(RawStat { + size, + ty, + dev, + qid, + mode, + atime, + mtime, + length, + name, + uid, + gid, + muid, + }) + } } } @@ -432,9 +459,12 @@ macro_rules! impl_message_datatype { write_fields!(buf, self, $($field),*) } - async unsafe fn read(state: &State) -> io::Result { - $(let $field = <$ty>::read(&state).await?;)* - Ok($struct { $($field),* }) + async unsafe fn read() -> io::Result { + // SAFETY: we are only awaiting via request_bytes + unsafe { + $(let $field = <$ty>::read().await?;)* + Ok($struct { $($field),* }) + } } } }; @@ -565,30 +595,31 @@ macro_rules! impl_message_format { } #[allow(unused_assignments)] - async unsafe fn read(state: &State) -> io::Result { - let buf = request_bytes!(state, size_of::()); - let len = from_le_bytes!(u32, buf) as usize; - - let bytes = request_bytes!(state, len-4); - let ty = from_le_bytes!(u8, &bytes); - let tag = from_le_bytes!(u16, &bytes[1..]); - let mut cur = Cursor::new(bytes); - cur.set_position(3); - - let content = match MessageType(ty) { - $( - MessageType::$message_variant => $enum_ty::$enum_variant { - $($field: <$ty>::read_from(&mut cur)?),* - }, - )+ - - MessageType(ty) => return Err(io::Error::new( - ErrorKind::InvalidData, - format!($err, ty), - )), - }; - - Ok($message_ty { tag, content }) + async unsafe fn read() -> io::Result { + // SAFETY: we are only awaiting via request_bytes + unsafe { + let len = u32::read().await? as usize; + let bytes = request_bytes!(len-4); + let ty = from_le_bytes!(u8, &bytes); + let tag = from_le_bytes!(u16, &bytes[1..]); + let mut cur = Cursor::new(bytes); + cur.set_position(3); + + let content = match MessageType(ty) { + $( + MessageType::$message_variant => $enum_ty::$enum_variant { + $($field: <$ty>::read_from(&mut cur)?),* + }, + )+ + + MessageType(ty) => return Err(io::Error::new( + ErrorKind::InvalidData, + format!($err, ty), + )), + }; + + Ok($message_ty { tag, content }) + } } } }; diff --git a/crates/ninep/src/sync/mod.rs b/crates/ninep/src/sync/mod.rs index 8a3f40e..e835d45 100644 --- a/crates/ninep/src/sync/mod.rs +++ b/crates/ninep/src/sync/mod.rs @@ -1,8 +1,8 @@ //! A synchronous implementation of 9p Servers and Clients use crate::{ sansio::{ - protocol::{NineP, Rdata, Rmessage, State}, - stub_waker, + protocol::{NineP, Rdata, Rmessage}, + State, }, Result, }; @@ -12,7 +12,8 @@ use std::{ net::TcpStream, os::unix::net::UnixStream, pin::pin, - task::{Context, Poll}, + sync::Arc, + task::{Context, Poll, Waker}, }; pub mod client; @@ -31,22 +32,22 @@ pub trait SyncNineP: NineP { /// Decode self from 9p protocol bytes coming from the given [SyncStream]. fn read_from(r: &mut R) -> io::Result { - let waker = stub_waker(); - let mut context = Context::from_waker(&waker); - let s = State::default(); + let s = Arc::new(State::default()); + let waker = Waker::from(s.clone()); + let mut ctx = Context::from_waker(&waker); // SAFETY: assumes the impl of Read9p is a valid future for us to poll - let mut fut = unsafe { pin!(Self::read(&s)) }; + let mut fut = unsafe { pin!(Self::read()) }; loop { - match fut.as_mut().poll(&mut context) { + match fut.as_mut().poll(&mut ctx) { Poll::Ready(val) => return val, - // SAFETY: s is only shared with the future we're polling - Poll::Pending => unsafe { - let n = (*s.0.get()).n; + Poll::Pending => { + let mut guard = s.inner.lock().unwrap(); + let n = guard.n; let mut buf = vec![0; n]; r.read_exact(&mut buf)?; - (*s.0.get()).buf = Some(buf); - }, + guard.buf = Some(buf); + } } } } diff --git a/crates/ninep/src/tokio/mod.rs b/crates/ninep/src/tokio/mod.rs index c678c01..aa4b05c 100644 --- a/crates/ninep/src/tokio/mod.rs +++ b/crates/ninep/src/tokio/mod.rs @@ -1,8 +1,8 @@ //! Tokio based asynchronous implementation of 9p Servers and Clients use crate::{ sansio::{ - protocol::{NineP, Rdata, Rmessage, State}, - stub_waker, + protocol::{NineP, Rdata, Rmessage}, + State, }, Result, }; @@ -11,7 +11,8 @@ use std::{ io, marker::Unpin, pin::pin, - task::{Context, Poll}, + sync::Arc, + task::{Context, Poll, Waker}, }; use tokio::{ io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, @@ -64,22 +65,21 @@ where T: NineP + Send, R: AsyncRead + Unpin + Send, { - let waker = stub_waker(); - let s = State::default(); + let s = Arc::new(State::default()); + let waker = Waker::from(s.clone()); // SAFETY: assumes the impl of Read9p is a valid future for us to poll - let mut fut = unsafe { pin!(T::read(&s)) }; + let mut fut = unsafe { pin!(T::read()) }; loop { let poll = fut.as_mut().poll(&mut Context::from_waker(&waker)); match poll { Poll::Ready(val) => return val, - // SAFETY: s is only shared with the future we're polling - Poll::Pending => unsafe { - let n = (*s.0.get()).n; + Poll::Pending => { + let n = s.inner.lock().unwrap().n; let mut buf = vec![0; n]; r.read_exact(&mut buf).await?; - (*s.0.get()).buf = Some(buf); - }, + s.inner.lock().unwrap().buf = Some(buf); + } } } } diff --git a/src/buffer/internal.rs b/src/buffer/internal.rs index fcde7c5..46ab158 100644 --- a/src/buffer/internal.rs +++ b/src/buffer/internal.rs @@ -1072,13 +1072,13 @@ unsafe fn decode_char_at(start: usize, bytes: &[u8]) -> char { // SAFETY: `bytes` contains UTF-8-like string data so we have the next byte, let z = bytes[start + 2]; let y_z = utf8_acc_cont_byte((y & CONT_MASK) as u32, z); - ch = init << 12 | y_z; + ch = (init << 12) | y_z; if x >= 0xF0 { // [x y z w] case // use only the lower 3 bits of `init` // SAFETY: `bytes` contains UTF-8-like string data so we have the next byte, let w = bytes[start + 3]; - ch = (init & 7) << 18 | utf8_acc_cont_byte(y_z, w); + ch = ((init & 7) << 18) | utf8_acc_cont_byte(y_z, w); } } From 51f0385b33745575cca04d21ab92dc69c83a2dd1 Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Tue, 25 Feb 2025 16:11:41 +0000 Subject: [PATCH 09/12] back to unsafe cell for the shared state as there is no need to lock --- crates/ninep/src/sansio/mod.rs | 34 ++++++++++++++++++++++++----- crates/ninep/src/sansio/protocol.rs | 15 +++---------- crates/ninep/src/sync/mod.rs | 11 +++++----- crates/ninep/src/tokio/mod.rs | 10 +++++---- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/crates/ninep/src/sansio/mod.rs b/crates/ninep/src/sansio/mod.rs index e3e067b..18d2450 100644 --- a/crates/ninep/src/sansio/mod.rs +++ b/crates/ninep/src/sansio/mod.rs @@ -5,10 +5,7 @@ use crate::{ sansio::protocol::{Rdata, Rmessage}, Result, }; -use std::{ - sync::{Arc, Mutex}, - task::Wake, -}; +use std::{cell::UnsafeCell, sync::Arc, task::Wake}; pub mod protocol; pub mod server; @@ -25,7 +22,34 @@ impl From<(u16, Result)> for Rmessage { /// Shared state between a NineP impl and a parent read loop that is performing IO. #[derive(Default, Debug)] pub(crate) struct State { - pub(crate) inner: Mutex, + inner: UnsafeCell, +} + +/// SAFETY: we can only access the inner state in this crate +unsafe impl Send for State {} +/// SAFETY: we can only access the inner state in this crate +unsafe impl Sync for State {} + +impl State { + pub(crate) unsafe fn requested_bytes(&self) -> usize { + // SAFETY: can only be called inside of an I/O read loop that owns the state + unsafe { (*self.inner.get()).n } + } + + pub(crate) unsafe fn take_bytes(&self) -> Vec { + // SAFETY: can only be called inside of an I/O read loop that owns the state + unsafe { (*self.inner.get()).buf.take().unwrap_unchecked() } + } + + pub(crate) unsafe fn set_requested(&self, n: usize) { + // SAFETY: can only be called inside of an I/O read loop that owns the state + unsafe { (*self.inner.get()).n = n }; + } + + pub(crate) unsafe fn set_bytes(&self, buf: Vec) { + // SAFETY: can only be called inside of an I/O read loop that owns the state + unsafe { (*self.inner.get()).buf = Some(buf) }; + } } #[derive(Default, Debug)] diff --git a/crates/ninep/src/sansio/protocol.rs b/crates/ninep/src/sansio/protocol.rs index b3e48ca..b26312e 100644 --- a/crates/ninep/src/sansio/protocol.rs +++ b/crates/ninep/src/sansio/protocol.rs @@ -86,16 +86,10 @@ impl Future for RequestBytes { if self.polled { // SAFETY: we can only poll this future using a waker wrapping State let data = unsafe { - let state = (ctx.waker().data() as *mut () as *mut State) + (ctx.waker().data() as *mut () as *mut State) .as_mut() - .unwrap_unchecked(); - state - .inner - .lock() - .unwrap_unchecked() - .buf - .take() .unwrap_unchecked() + .take_bytes() }; Poll::Ready(data) @@ -106,10 +100,7 @@ impl Future for RequestBytes { (ctx.waker().data() as *mut () as *mut State) .as_mut() .unwrap_unchecked() - .inner - .lock() - .unwrap_unchecked() - .n = self.n; + .set_requested(self.n); }; Poll::Pending diff --git a/crates/ninep/src/sync/mod.rs b/crates/ninep/src/sync/mod.rs index e835d45..75d2a5f 100644 --- a/crates/ninep/src/sync/mod.rs +++ b/crates/ninep/src/sync/mod.rs @@ -41,13 +41,14 @@ pub trait SyncNineP: NineP { loop { match fut.as_mut().poll(&mut ctx) { Poll::Ready(val) => return val, - Poll::Pending => { - let mut guard = s.inner.lock().unwrap(); - let n = guard.n; + // SAFETY: the only other reference to the shared state is in the future we are + // polling so mutating its inner state is safe + Poll::Pending => unsafe { + let n = s.requested_bytes(); let mut buf = vec![0; n]; r.read_exact(&mut buf)?; - guard.buf = Some(buf); - } + s.set_bytes(buf); + }, } } } diff --git a/crates/ninep/src/tokio/mod.rs b/crates/ninep/src/tokio/mod.rs index aa4b05c..69c864d 100644 --- a/crates/ninep/src/tokio/mod.rs +++ b/crates/ninep/src/tokio/mod.rs @@ -74,12 +74,14 @@ where let poll = fut.as_mut().poll(&mut Context::from_waker(&waker)); match poll { Poll::Ready(val) => return val, - Poll::Pending => { - let n = s.inner.lock().unwrap().n; + // SAFETY: the only other reference to the shared state is in the future we are + // polling so mutating its inner state is safe + Poll::Pending => unsafe { + let n = s.requested_bytes(); let mut buf = vec![0; n]; r.read_exact(&mut buf).await?; - s.inner.lock().unwrap().buf = Some(buf); - } + s.set_bytes(buf); + }, } } } From 30edecb7424a10d3cf6efd78c0616242909bd75a Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Tue, 4 Mar 2025 15:03:08 +0000 Subject: [PATCH 10/12] using coros to remove code duplication in the server impls --- Cargo.lock | 6 + crates/ninep/Cargo.toml | 1 + crates/ninep/src/sansio/mod.rs | 45 ---- crates/ninep/src/sansio/protocol.rs | 238 +++++++------------- crates/ninep/src/sansio/server.rs | 334 +++++++++++++++++++++------- crates/ninep/src/sync/mod.rs | 33 +-- crates/ninep/src/sync/server.rs | 171 +++++--------- crates/ninep/src/tokio/mod.rs | 38 +--- crates/ninep/src/tokio/server.rs | 172 +++----------- src/fsys/buffer.rs | 2 +- src/fsys/event.rs | 2 +- src/fsys/log.rs | 2 +- 12 files changed, 450 insertions(+), 594 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f97e0f2..0aa153f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,6 +204,11 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "crimes" +version = "0.1.0" +source = "git+https://github.com/sminez/crimes.git#6591acaba40572901ca6e2bbeaf1727562fa50c7" + [[package]] name = "criterion" version = "0.5.1" @@ -447,6 +452,7 @@ name = "ninep" version = "0.4.0" dependencies = [ "bitflags 2.8.0", + "crimes", "simple_test_case", "tokio", ] diff --git a/crates/ninep/Cargo.toml b/crates/ninep/Cargo.toml index 262c2cc..3dad518 100644 --- a/crates/ninep/Cargo.toml +++ b/crates/ninep/Cargo.toml @@ -20,6 +20,7 @@ tokio = ["dep:tokio"] [dependencies] bitflags = "2.6" tokio = { version = "1.43.0", features = ["macros", "net", "io-util", "rt", "sync"], optional = true } +simple_coro = { git = "https://github.com/sminez/crimes.git", package = "crimes" } [dev-dependencies] simple_test_case = "1" diff --git a/crates/ninep/src/sansio/mod.rs b/crates/ninep/src/sansio/mod.rs index 18d2450..e71a6aa 100644 --- a/crates/ninep/src/sansio/mod.rs +++ b/crates/ninep/src/sansio/mod.rs @@ -5,7 +5,6 @@ use crate::{ sansio::protocol::{Rdata, Rmessage}, Result, }; -use std::{cell::UnsafeCell, sync::Arc, task::Wake}; pub mod protocol; pub mod server; @@ -18,47 +17,3 @@ impl From<(u16, Result)> for Rmessage { } } } - -/// Shared state between a NineP impl and a parent read loop that is performing IO. -#[derive(Default, Debug)] -pub(crate) struct State { - inner: UnsafeCell, -} - -/// SAFETY: we can only access the inner state in this crate -unsafe impl Send for State {} -/// SAFETY: we can only access the inner state in this crate -unsafe impl Sync for State {} - -impl State { - pub(crate) unsafe fn requested_bytes(&self) -> usize { - // SAFETY: can only be called inside of an I/O read loop that owns the state - unsafe { (*self.inner.get()).n } - } - - pub(crate) unsafe fn take_bytes(&self) -> Vec { - // SAFETY: can only be called inside of an I/O read loop that owns the state - unsafe { (*self.inner.get()).buf.take().unwrap_unchecked() } - } - - pub(crate) unsafe fn set_requested(&self, n: usize) { - // SAFETY: can only be called inside of an I/O read loop that owns the state - unsafe { (*self.inner.get()).n = n }; - } - - pub(crate) unsafe fn set_bytes(&self, buf: Vec) { - // SAFETY: can only be called inside of an I/O read loop that owns the state - unsafe { (*self.inner.get()).buf = Some(buf) }; - } -} - -#[derive(Default, Debug)] -pub(crate) struct StateInner { - pub(crate) n: usize, - pub(crate) buf: Option>, -} - -impl Wake for State { - fn wake(self: Arc) {} - fn wake_by_ref(self: &Arc) {} -} diff --git a/crates/ninep/src/sansio/protocol.rs b/crates/ninep/src/sansio/protocol.rs index b26312e..844cb2a 100644 --- a/crates/ninep/src/sansio/protocol.rs +++ b/crates/ninep/src/sansio/protocol.rs @@ -1,14 +1,13 @@ //! Sans-io 9p protocol implementation //! //! http://man.cat-v.org/plan_9/5/ -use crate::{sansio::State, sync::SyncNineP}; +use crate::sync::SyncNineP; +use simple_coro::Handle; use std::{ fmt, future::Future, io::{self, Cursor, ErrorKind}, mem::size_of, - pin::Pin, - task::{Context, Poll}, }; /// The size of variable length data is denoted using a u16 so anything longer @@ -70,61 +69,14 @@ pub trait NineP: Sized { /// # Safety /// Implementations of `read` need to ensure that the only await points they contain are /// from calls to the [request_bytes] macro. - unsafe fn read() -> impl Future> + Send; -} - -/// Helper struct for awaiting a Future that returns pending once so we can return control to the -/// poll loop and perform IO. -struct RequestBytes { - polled: bool, - n: usize, -} - -impl Future for RequestBytes { - type Output = Vec; - fn poll(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { - if self.polled { - // SAFETY: we can only poll this future using a waker wrapping State - let data = unsafe { - (ctx.waker().data() as *mut () as *mut State) - .as_mut() - .unwrap_unchecked() - .take_bytes() - }; - - Poll::Ready(data) - } else { - self.polled = true; - // SAFETY: we can only poll this future using a waker wrapping State - unsafe { - (ctx.waker().data() as *mut () as *mut State) - .as_mut() - .unwrap_unchecked() - .set_requested(self.n); - }; - - Poll::Pending - } - } -} - -/// Request a specific number of bytes from the parent poll loop and then yield to that poll loop -/// so it can perform IO and provide the requested data. -macro_rules! request_bytes { - ($n:expr) => {{ - RequestBytes { - polled: false, - n: $n, - } - .await - }}; + fn read_9p(handle: Handle>) -> impl Future> + Send; } /// wrapper around uX::from_le_bytes that accepts a slice rather than a fixed size array macro_rules! from_le_bytes { ($ty:ty, $bytes:expr) => { // SAFETY: we know we are setting the correct array length - <$ty>::from_le_bytes($bytes[0..size_of::<$ty>()].try_into().unwrap_unchecked()) + unsafe { <$ty>::from_le_bytes($bytes[0..size_of::<$ty>()].try_into().unwrap_unchecked()) } }; } @@ -143,12 +95,9 @@ macro_rules! impl_u { Ok(()) } - async unsafe fn read() -> io::Result<$ty> { - // SAFETY: we are only awaiting via request_bytes - unsafe { - let buf = request_bytes!(size_of::<$ty>()); - Ok(from_le_bytes!($ty, buf)) - } + async fn read_9p(handle: Handle>) -> io::Result<$ty> { + let buf = handle.yield_value(size_of::<$ty>()).await; + Ok(from_le_bytes!($ty, buf)) } } )+ @@ -184,15 +133,11 @@ impl NineP for String { Ok(()) } - async unsafe fn read() -> io::Result { - // SAFETY: we are only awaiting via request_bytes - unsafe { - let len = u16::read().await? as usize; - let buf = request_bytes!(len); + async fn read_9p(handle: Handle>) -> io::Result { + let len = u16::read_9p(handle).await? as usize; + let buf = handle.yield_value(len).await; - String::from_utf8(buf) - .map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string())) - } + String::from_utf8(buf).map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string())) } } @@ -223,17 +168,14 @@ impl NineP for Vec { Ok(()) } - async unsafe fn read() -> io::Result { - // SAFETY: we are only awaiting via request_bytes - unsafe { - let len = u16::read().await? as usize; - let mut buf = Vec::with_capacity(len); - for _ in 0..len { - buf.push(T::read().await?); - } - - Ok(buf) + async fn read_9p(handle: Handle>) -> io::Result { + let len = u16::read_9p(handle).await? as usize; + let mut buf = Vec::with_capacity(len); + for _ in 0..len { + buf.push(T::read_9p(handle).await?); } + + Ok(buf) } } @@ -305,19 +247,16 @@ impl NineP for Data { Ok(()) } - async unsafe fn read() -> io::Result { - // SAFETY: we are only awaiting via request_bytes - unsafe { - let len = u32::read().await? as usize; - if len > MAX_DATA_LEN { - return Err(io::Error::new( - ErrorKind::InvalidData, - format!("data field too long: max={MAX_DATA_LEN} len={len}"), - )); - } - - Ok(Data(request_bytes!(len))) + async fn read_9p(handle: Handle>) -> io::Result { + let len = u32::read_9p(handle).await? as usize; + if len > MAX_DATA_LEN { + return Err(io::Error::new( + ErrorKind::InvalidData, + format!("data field too long: max={MAX_DATA_LEN} len={len}"), + )); } + + Ok(Data(handle.yield_value(len).await)) } } @@ -378,41 +317,38 @@ impl NineP for RawStat { ) } - async unsafe fn read() -> io::Result { - // SAFETY: we are only awaiting via request_bytes - unsafe { - // Request the fixed sized data before the strings in one block up front - let buf = request_bytes!(41); - let bytes = buf.as_slice(); - - let size = from_le_bytes!(u16, bytes); - let ty = from_le_bytes!(u16, &bytes[2..]); - let dev = from_le_bytes!(u32, &bytes[4..]); - let qid = Qid::read_from(&mut &bytes[8..])?; - let mode = from_le_bytes!(u32, &bytes[21..]); - let atime = from_le_bytes!(u32, &bytes[25..]); - let mtime = from_le_bytes!(u32, &bytes[29..]); - let length = from_le_bytes!(u64, &bytes[33..]); - let name = String::read().await?; - let uid = String::read().await?; - let gid = String::read().await?; - let muid = String::read().await?; - - Ok(RawStat { - size, - ty, - dev, - qid, - mode, - atime, - mtime, - length, - name, - uid, - gid, - muid, - }) - } + async fn read_9p(handle: Handle>) -> io::Result { + // Request the fixed sized data before the strings in one block up front + let buf = handle.yield_value(41).await; + let bytes = buf.as_slice(); + + let size = from_le_bytes!(u16, bytes); + let ty = from_le_bytes!(u16, &bytes[2..]); + let dev = from_le_bytes!(u32, &bytes[4..]); + let qid = Qid::read_from(&mut &bytes[8..])?; + let mode = from_le_bytes!(u32, &bytes[21..]); + let atime = from_le_bytes!(u32, &bytes[25..]); + let mtime = from_le_bytes!(u32, &bytes[29..]); + let length = from_le_bytes!(u64, &bytes[33..]); + let name = String::read_9p(handle).await?; + let uid = String::read_9p(handle).await?; + let gid = String::read_9p(handle).await?; + let muid = String::read_9p(handle).await?; + + Ok(RawStat { + size, + ty, + dev, + qid, + mode, + atime, + mtime, + length, + name, + uid, + gid, + muid, + }) } } @@ -450,12 +386,9 @@ macro_rules! impl_message_datatype { write_fields!(buf, self, $($field),*) } - async unsafe fn read() -> io::Result { - // SAFETY: we are only awaiting via request_bytes - unsafe { - $(let $field = <$ty>::read().await?;)* - Ok($struct { $($field),* }) - } + async fn read_9p(handle: Handle>) -> io::Result { + $(let $field = <$ty>::read_9p(handle).await?;)* + Ok($struct { $($field),* }) } } }; @@ -586,31 +519,28 @@ macro_rules! impl_message_format { } #[allow(unused_assignments)] - async unsafe fn read() -> io::Result { - // SAFETY: we are only awaiting via request_bytes - unsafe { - let len = u32::read().await? as usize; - let bytes = request_bytes!(len-4); - let ty = from_le_bytes!(u8, &bytes); - let tag = from_le_bytes!(u16, &bytes[1..]); - let mut cur = Cursor::new(bytes); - cur.set_position(3); - - let content = match MessageType(ty) { - $( - MessageType::$message_variant => $enum_ty::$enum_variant { - $($field: <$ty>::read_from(&mut cur)?),* - }, - )+ - - MessageType(ty) => return Err(io::Error::new( - ErrorKind::InvalidData, - format!($err, ty), - )), - }; - - Ok($message_ty { tag, content }) - } + async fn read_9p(handle: Handle>) -> io::Result { + let len = u32::read_9p(handle).await? as usize; + let bytes = handle.yield_value(len-4).await; + let ty = from_le_bytes!(u8, &bytes); + let tag = from_le_bytes!(u16, &bytes[1..]); + let mut cur = Cursor::new(bytes); + cur.set_position(3); + + let content = match MessageType(ty) { + $( + MessageType::$message_variant => $enum_ty::$enum_variant { + $($field: <$ty>::read_from(&mut cur)?),* + }, + )+ + + MessageType(ty) => return Err(io::Error::new( + ErrorKind::InvalidData, + format!($err, ty), + )), + }; + + Ok($message_ty { tag, content }) } } }; diff --git a/crates/ninep/src/sansio/server.rs b/crates/ninep/src/sansio/server.rs index 78a1601..cb09024 100644 --- a/crates/ninep/src/sansio/server.rs +++ b/crates/ninep/src/sansio/server.rs @@ -1,14 +1,18 @@ //! Traits and structs for implementing a 9p fileserver use crate::{ - fs::{FileMeta, FileType, QID_ROOT}, - sansio::protocol::{Qid, Rdata, MAX_DATA_LEN}, + fs::{FileMeta, FileType, Stat, QID_ROOT}, + sansio::protocol::{Data, Qid, RawStat, Rdata, Tdata, Tmessage, MAX_DATA_LEN}, + sync::SyncNineP, Result, }; +use simple_coro::{Coro, Handle, ReadyCoro}; use std::{ cmp::min, collections::btree_map::BTreeMap, env, - sync::{mpsc::Receiver, Arc}, + future::Future, + ops::{Deref, DerefMut}, + sync::Arc, }; /// Marker afid to denode that auth is not required for establishing connections @@ -17,6 +21,7 @@ pub const AFID_NO_AUTH: u32 = u32::MAX; // Error messages pub(crate) const E_NO_VERSION_MESSAGE: &str = "first message must be Tversion"; pub(crate) const E_UNATTACHED: &str = "session is not attached"; +pub(crate) const E_ALREADY_ATTACHED: &str = "session is already attached"; pub(crate) const E_AUTH_NOT_REQUIRED: &str = "authentication not required"; pub(crate) const E_DUPLICATE_FID: &str = "duplicate fid"; pub(crate) const E_UNKNOWN_FID: &str = "unknown fid"; @@ -48,13 +53,10 @@ pub fn socket_path(name: &str) -> String { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ClientId(pub(crate) u64); -/// The outcome of a client attempting to [read](Serve9p::read) a given file. #[derive(Debug)] -pub enum ReadOutcome { - /// The data is immediately available. - Immediate(Vec), - /// No response should be sent until data is received on the provided channel - Blocked(Receiver>), +pub(crate) enum Either { + L(L), + R(R), } /// A 9p server wrapping an `S` that must implement an IO specific handler trait to provide the @@ -139,31 +141,191 @@ impl Attached { } } -/// A connected client session over a given stream +/// Internal state for a running client session other than the user provided filesystem +/// implementation. We keep this separate in order to allow for splitting borrows between this +/// state and the filesystem impl when creating coroutine based helper methods. #[derive(Debug)] -pub(crate) struct Session +pub(crate) struct SessionState where T: SessionType, - S: Send, { - pub(crate) client_id: ClientId, pub(crate) state: T, + pub(crate) client_id: ClientId, pub(crate) msize: u32, pub(crate) roots: BTreeMap, - pub(crate) s: Arc, pub(crate) qids: BTreeMap, - pub(crate) stream: U, } -impl Session +impl SessionState where T: SessionType, - S: Send, { pub(crate) fn qid(&self, qid: u64) -> Option { self.qids.get(&qid).map(|fm| fm.as_qid()) } +} + +impl SessionState { + pub(crate) fn try_file_meta(&self, fid: u32) -> Result { + let opt = match self.state.fids.get(&fid) { + Some(&qid) => self.qids.get(&qid).cloned(), + None => None, + }; + + opt.ok_or_else(|| E_UNKNOWN_FID.to_string()) + } + + pub(crate) fn handle_attached_walk<'a, 's: 'a>( + &'s mut self, + fid: u32, + new_fid: u32, + wnames: &'a [String], + ) -> ReadyCoro< + (u64, &'a str, &'a str), + FileMeta, + Result, + impl Future> + use<'s, 'a>, + > { + Coro::from( + move |handle: Handle<(u64, &'a str, &'a str), FileMeta>| async move { + if new_fid != fid && self.state.fids.contains_key(&new_fid) { + return Err(E_DUPLICATE_FID.to_string()); + } + + let fm = self.try_file_meta(fid)?; + + if wnames.is_empty() { + self.state.fids.insert(new_fid, fm.qid); + return Ok(Rdata::Walk { wqids: vec![] }); + } else if matches!(fm.ty, FileType::Regular) { + return Err(E_WALK_NON_DIR.to_string()); + } + + let mut wqids = Vec::with_capacity(wnames.len()); + let mut qid = fm.qid; + + for name in wnames.iter() { + let fm = handle.yield_value((qid, name, &self.state.uname)).await; + qid = fm.qid; + wqids.push(fm.as_qid()); + self.qids.insert(qid, fm); + } + + if wqids.len() == wnames.len() { + let qid = wqids.last().expect("empty was handled above").path; + self.state.fids.insert(new_fid, qid); + } + + Ok(Rdata::Walk { wqids }) + }, + ) + } + + #[allow(clippy::type_complexity)] + pub(crate) fn handle_attached_read<'a, 's: 'a>( + &'s mut self, + fid: u32, + offset: u64, + count: u32, + ) -> ReadyCoro< + Either<(u64, &'a str), (u64, &'a str)>, // L=read_dir R=read + Vec, // we never send or use a value in response to a read, only read-dir + Result>, + impl Future>> + use<'s, 'a>, + > { + Coro::from( + move |handle: Handle, Vec>| async move { + use FileType::*; + + let fm = self.try_file_meta(fid)?; + if offset > u32::MAX as u64 { + return Err(format!("offset too large: {offset} > {}", u32::MAX)); + } + + let stats = match fm.ty { + Regular | AppendOnly | Exclusive => { + handle + .yield_value(Either::R((fm.qid, &self.state.uname))) + .await; + return Ok(None); // processing of the ReadOutcome is handled by the caller + } + + Directory => { + handle + .yield_value(Either::L((fm.qid, &self.state.uname))) + .await + } + }; + + let mut buf = Vec::with_capacity(count as usize); + let mut to_skip = offset as usize; + + for stat in stats.into_iter() { + self.qids.entry(stat.fm.qid).or_insert(stat.fm.clone()); + let rstat: RawStat = stat.into(); + let mut tmp = Vec::new(); + rstat.write_to(&mut tmp).unwrap(); + + if to_skip != 0 { + if tmp.len() > to_skip { + return Err(E_INVALID_OFFSET.to_string()); + } else { + to_skip -= tmp.len(); + continue; + } + } + + if buf.len() + tmp.len() > count as usize { + break; + } + buf.extend(tmp); + } + + Ok(Some(Rdata::Read { data: Data(buf) })) + }, + ) + } +} + +/// A connected client session over a given stream +#[derive(Debug)] +pub(crate) struct Session +where + T: SessionType, + S: Send, +{ + pub(crate) s: Arc, + pub(crate) stream: U, + pub(crate) session_state: SessionState, +} + +impl Deref for Session +where + T: SessionType, + S: Send, +{ + type Target = SessionState; + + fn deref(&self) -> &Self::Target { + &self.session_state + } +} + +impl DerefMut for Session +where + T: SessionType, + S: Send, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.session_state + } +} +impl Session +where + T: SessionType, + S: Send, +{ /// The version request negotiates the protocol version and message size to be used on the /// connection and initializes the connection for I/O. Tversion must be the first message sent /// on the 9P connection, and the client cannot issue any further requests until it has @@ -191,17 +353,17 @@ where /// A successful version request initializes the connection. All outstanding I/O on the /// connection is aborted; all active fids are freed (‘clunked’) automatically. The set of /// messages between version requests is called a session. - pub(crate) fn handle_version(&mut self, msize: u32, version: String) -> Result { + pub(crate) fn handle_version(&mut self, msize: u32, version: String) -> Rdata { let server_version = if version != SUPPORTED_VERSION { UNKNOWN_VERSION } else { SUPPORTED_VERSION }; - Ok(Rdata::Version { + Rdata::Version { msize: min(self.msize, msize), version: server_version.to_string(), - }) + } } /// If the client does wish to authenticate, it must acquire and validate an afid using an auth @@ -239,25 +401,74 @@ where stream: U, ) -> Self { Self { - client_id, - state: Unattached::default(), - msize, - roots, s, - qids, stream, + session_state: SessionState { + client_id, + state: Unattached::default(), + msize, + roots, + qids, + }, } } + pub(crate) fn handle_tmessage_unattached( + &mut self, + Tmessage { tag, content }: Tmessage, + ) -> Either<(u16, Result), (u16, Attached, Qid)> { + use Tdata::*; + + let resp = match content { + Version { msize, version } => { + self.state.seen_version = version == SUPPORTED_VERSION; + Ok(self.handle_version(msize, version)) + } + + Auth { afid, uname, aname } => { + if !self.state.seen_version { + return Either::L((tag, Err(E_NO_VERSION_MESSAGE.to_string()))); + } + + self.handle_auth(afid, uname, aname) + } + + Attach { + fid, + afid, + uname, + aname, + } => { + if !self.state.seen_version { + return Either::L((tag, Err(E_NO_VERSION_MESSAGE.to_string()))); + } + + let (st, aqid) = match self.handle_attach(fid, afid, uname, aname) { + Err(e) => return Either::L((tag, Err(e))), + Ok((st, aqid)) => (st, aqid), + }; + + return Either::R((tag, st, aqid)); + } + + _ => Err(E_UNATTACHED.into()), + }; + + Either::L((tag, resp)) + } + pub(crate) fn into_attached(self, ty: Attached) -> Session { let Self { - client_id, - msize, - roots, s, - qids, stream, - .. + session_state: + SessionState { + client_id, + msize, + roots, + qids, + .. + }, } = self; Session::new_attached(client_id, ty, msize, roots, s, qids, stream) @@ -295,12 +506,6 @@ where } } -#[derive(Debug)] -pub(crate) enum Either { - L(L), - R(R), -} - impl Session where S: Send, @@ -315,58 +520,15 @@ where stream: U, ) -> Self { Self { - client_id, - state, - msize, - roots, s, - qids, stream, + session_state: SessionState { + client_id, + state, + msize, + roots, + qids, + }, } } - - pub(crate) fn try_file_meta(&self, fid: u32) -> Result { - let opt = match self.state.fids.get(&fid) { - Some(&qid) => self.qids.get(&qid).cloned(), - None => None, - }; - - opt.ok_or_else(|| E_UNKNOWN_FID.to_string()) - } - - pub(crate) fn prep_walk( - &mut self, - fid: u32, - new_fid: u32, - wnames: &[String], - ) -> Result> { - if new_fid != fid && self.state.fids.contains_key(&new_fid) { - return Err(E_DUPLICATE_FID.to_string()); - } - - let fm = self.try_file_meta(fid)?; - - if wnames.is_empty() { - self.state.fids.insert(new_fid, fm.qid); - Ok(Either::L(Rdata::Walk { wqids: vec![] })) - } else if matches!(fm.ty, FileType::Regular) { - Err(E_WALK_NON_DIR.to_string()) - } else { - Ok(Either::R(fm)) - } - } - - pub(crate) fn complete_walk( - &mut self, - new_fid: u32, - wqids: Vec, - n_wnames: usize, - ) -> Rdata { - if wqids.len() == n_wnames { - let qid = wqids.last().expect("empty was handled in prep_walk").path; - self.state.fids.insert(new_fid, qid); - } - - Rdata::Walk { wqids } - } } diff --git a/crates/ninep/src/sync/mod.rs b/crates/ninep/src/sync/mod.rs index 75d2a5f..b15a0c3 100644 --- a/crates/ninep/src/sync/mod.rs +++ b/crates/ninep/src/sync/mod.rs @@ -1,19 +1,13 @@ //! A synchronous implementation of 9p Servers and Clients use crate::{ - sansio::{ - protocol::{NineP, Rdata, Rmessage}, - State, - }, + sansio::protocol::{NineP, Rdata, Rmessage}, Result, }; +use simple_coro::{Coro, CoroState}; use std::{ - future::Future, io::{self, Read, Write}, net::TcpStream, os::unix::net::UnixStream, - pin::pin, - sync::Arc, - task::{Context, Poll, Waker}, }; pub mod client; @@ -32,24 +26,17 @@ pub trait SyncNineP: NineP { /// Decode self from 9p protocol bytes coming from the given [SyncStream]. fn read_from(r: &mut R) -> io::Result { - let s = Arc::new(State::default()); - let waker = Waker::from(s.clone()); - let mut ctx = Context::from_waker(&waker); - - // SAFETY: assumes the impl of Read9p is a valid future for us to poll - let mut fut = unsafe { pin!(Self::read()) }; + let mut coro = Coro::from(Self::read_9p); loop { - match fut.as_mut().poll(&mut ctx) { - Poll::Ready(val) => return val, - // SAFETY: the only other reference to the shared state is in the future we are - // polling so mutating its inner state is safe - Poll::Pending => unsafe { - let n = s.requested_bytes(); + coro = match coro.resume() { + CoroState::Pending(c, n) => { let mut buf = vec![0; n]; r.read_exact(&mut buf)?; - s.set_bytes(buf); - }, - } + c.send(buf) + } + + CoroState::Complete(res) => return res, + }; } } } diff --git a/crates/ninep/src/sync/server.rs b/crates/ninep/src/sync/server.rs index 2472cef..daa79e1 100644 --- a/crates/ninep/src/sync/server.rs +++ b/crates/ninep/src/sync/server.rs @@ -7,24 +7,35 @@ use crate::{ sansio::{ protocol::{Data, RawStat, Rdata, Tdata, Tmessage}, server::{ - Attached, Either, Session, SessionType, Unattached, E_CREATE_NON_DIR, E_INVALID_OFFSET, - E_NO_VERSION_MESSAGE, E_UNATTACHED, E_UNKNOWN_FID, SUPPORTED_VERSION, + Attached, Either, Session, SessionType, Unattached, E_ALREADY_ATTACHED, + E_CREATE_NON_DIR, E_UNKNOWN_FID, }, }, sync::{SyncNineP, SyncStream}, Result, }; +use simple_coro::CoroState; use std::{ collections::btree_map::Entry, fs, mem::size_of, net::TcpListener, os::unix::net::UnixListener, + sync::mpsc::Receiver, thread::{spawn, JoinHandle}, }; // re-exports -pub use crate::sansio::server::{socket_dir, socket_path, ClientId, ReadOutcome, Server}; +pub use crate::sansio::server::{socket_dir, socket_path, ClientId, Server}; + +/// The outcome of a client attempting to [read](Serve9p::read) a given file. +#[derive(Debug)] +pub enum ReadOutcome { + /// The data is immediately available. + Immediate(Vec), + /// No response should be sent until data is received on the provided channel + Blocked(Receiver>), +} #[derive(Debug)] struct Socket { @@ -197,59 +208,19 @@ where U: SyncStream, { fn handle_connection(mut self) { - use Tdata::*; - loop { let t = match Tmessage::read_from(&mut self.stream) { Ok(t) => t, Err(_) => return, }; - let Tmessage { tag, content } = t; - - let resp = match content { - Version { msize, version } => { - self.state.seen_version = version == SUPPORTED_VERSION; - self.handle_version(msize, version) - } - - Auth { afid, uname, aname } => { - if !self.state.seen_version { - self.reply(tag, Err(E_NO_VERSION_MESSAGE.to_string())); - continue; - } - - self.handle_auth(afid, uname, aname) - } - - Attach { - fid, - afid, - uname, - aname, - } => { - if !self.state.seen_version { - self.reply(tag, Err(E_NO_VERSION_MESSAGE.to_string())); - continue; - } - - let (st, aqid) = match self.handle_attach(fid, afid, uname, aname) { - Err(e) => { - self.reply(tag, Err(e)); - continue; - } - - Ok((st, aqid)) => (st, aqid), - }; - + match self.handle_tmessage_unattached(t) { + Either::L((tag, resp)) => self.reply(tag, resp), + Either::R((tag, st, aqid)) => { self.reply(tag, Ok(Rdata::Attach { aqid })); return self.into_attached(st).handle_connection(); } - - _ => Err(E_UNATTACHED.into()), - }; - - self.reply(tag, resp); + } } } } @@ -279,16 +250,14 @@ where let Tmessage { tag, content } = t; let resp = match content { + Auth { .. } | Attach { .. } => Err(E_ALREADY_ATTACHED.into()), + Flush { .. } => Ok(Rdata::Flush {}), Version { msize, version } => { - let res = self.handle_version(msize, version); - if res.is_ok() { - self.clunk_and_clear(); - } + let rdata = self.handle_version(msize, version); + self.clunk_and_clear(); - res + Ok(rdata) } - Auth { .. } | Attach { .. } => Err("session is already attached".into()), - Flush { .. } => Ok(Rdata::Flush {}), Walk { fid, @@ -363,22 +332,20 @@ where /// this restriction, the system imposes no limit on the number of elements in a file name, /// only the number that may be transmitted in a single message. fn handle_walk(&mut self, fid: u32, new_fid: u32, wnames: Vec) -> Result { - let fm = match self.prep_walk(fid, new_fid, &wnames)? { - Either::L(rdata) => return Ok(rdata), - Either::R(fm) => fm, - }; - - let mut wqids = Vec::with_capacity(wnames.len()); - let mut qid = fm.qid; - - for name in wnames.iter() { - let fm = self.s.walk(self.client_id, qid, name, &self.state.uname)?; - qid = fm.qid; - wqids.push(fm.as_qid()); - self.qids.insert(qid, fm); - } + let client_id = self.client_id; + let mut coro = self + .session_state + .handle_attached_walk(fid, new_fid, &wnames); - Ok(self.complete_walk(new_fid, wqids, wnames.len())) + loop { + coro = match coro.resume() { + CoroState::Complete(res) => return res, + CoroState::Pending(c, (qid, name, uname)) => { + let fm = self.s.walk(client_id, qid, name, uname)?; + c.send(fm) + } + }; + } } fn handle_clunk(&mut self, fid: u32) -> Result { @@ -459,70 +426,32 @@ where offset: u64, count: u32, ) -> Result> { - use FileType::*; - - let fm = self.try_file_meta(fid)?; - if offset > u32::MAX as u64 { - return Err(format!("offset too large: {offset} > {}", u32::MAX)); - } - - let data = match fm.ty { - Directory => self.read_dir(fm.qid, offset as usize, count as usize)?, - Regular | AppendOnly | Exclusive => { - let outcome = self.s.read( - self.client_id, - fm.qid, - offset as usize, - count as usize, - &self.state.uname, - )?; - + let cid = self.client_id; + let coro = self.session_state.handle_attached_read(fid, offset, count); + let (offset, count) = (offset as usize, count as usize); + + match coro.resume() { + CoroState::Complete(res) => res, + CoroState::Pending(c, Either::L((qid, uname))) => { + let stats = self.s.read_dir(cid, qid, uname)?; + c.send(stats).resume().unwrap() + } + CoroState::Pending(_, Either::R((qid, uname))) => { + let outcome = self.s.read(cid, qid, offset, count, uname)?; match outcome { - ReadOutcome::Immediate(data) => data, + ReadOutcome::Immediate(data) => Ok(Some(Rdata::Read { data: Data(data) })), ReadOutcome::Blocked(chan) => { let mut stream = self.stream.try_clone()?; - spawn(move || { let data = chan.recv().unwrap_or_default(); stream.reply(tag, Ok(Rdata::Read { data: Data(data) })); }); - return Ok(None); + Ok(None) } } } - }; - - Ok(Some(Rdata::Read { data: Data(data) })) - } - - fn read_dir(&mut self, qid: u64, offset: usize, count: usize) -> Result> { - let stats = self.s.read_dir(self.client_id, qid, &self.state.uname)?; - let mut buf = Vec::with_capacity(count); - let mut to_skip = offset; - - for stat in stats.into_iter() { - self.qids.entry(stat.fm.qid).or_insert(stat.fm.clone()); - let rstat: RawStat = stat.into(); - let mut tmp = Vec::new(); - rstat.write_to(&mut tmp).unwrap(); - - if to_skip != 0 { - if tmp.len() > to_skip { - return Err(E_INVALID_OFFSET.to_string()); - } else { - to_skip -= tmp.len(); - continue; - } - } - - if buf.len() + tmp.len() > count { - break; - } - buf.extend(tmp); } - - Ok(buf) } fn handle_write(&mut self, fid: u32, offset: u64, data: Vec) -> Result { diff --git a/crates/ninep/src/tokio/mod.rs b/crates/ninep/src/tokio/mod.rs index 69c864d..00dc0d5 100644 --- a/crates/ninep/src/tokio/mod.rs +++ b/crates/ninep/src/tokio/mod.rs @@ -1,19 +1,10 @@ //! Tokio based asynchronous implementation of 9p Servers and Clients use crate::{ - sansio::{ - protocol::{NineP, Rdata, Rmessage}, - State, - }, + sansio::protocol::{NineP, Rdata, Rmessage}, Result, }; -use std::{ - future::Future, - io, - marker::Unpin, - pin::pin, - sync::Arc, - task::{Context, Poll, Waker}, -}; +use simple_coro::{Coro, CoroState}; +use std::{future::Future, io, marker::Unpin}; use tokio::{ io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, net::{TcpStream, UnixStream}, @@ -65,24 +56,17 @@ where T: NineP + Send, R: AsyncRead + Unpin + Send, { - let s = Arc::new(State::default()); - let waker = Waker::from(s.clone()); - - // SAFETY: assumes the impl of Read9p is a valid future for us to poll - let mut fut = unsafe { pin!(T::read()) }; + let mut coro = Coro::from(T::read_9p); loop { - let poll = fut.as_mut().poll(&mut Context::from_waker(&waker)); - match poll { - Poll::Ready(val) => return val, - // SAFETY: the only other reference to the shared state is in the future we are - // polling so mutating its inner state is safe - Poll::Pending => unsafe { - let n = s.requested_bytes(); + coro = match coro.resume() { + CoroState::Pending(c, n) => { let mut buf = vec![0; n]; r.read_exact(&mut buf).await?; - s.set_bytes(buf); - }, - } + c.send(buf) + } + + CoroState::Complete(res) => return res, + }; } } diff --git a/crates/ninep/src/tokio/server.rs b/crates/ninep/src/tokio/server.rs index 9fdaf39..fcf34e7 100644 --- a/crates/ninep/src/tokio/server.rs +++ b/crates/ninep/src/tokio/server.rs @@ -7,13 +7,13 @@ use crate::{ sansio::{ protocol::{Data, RawStat, Rdata, Tdata, Tmessage}, server::{ - Attached, Either, Session, SessionType, Unattached, E_CREATE_NON_DIR, E_INVALID_OFFSET, - E_NO_VERSION_MESSAGE, E_UNATTACHED, E_UNKNOWN_FID, SUPPORTED_VERSION, + Attached, Either, Session, SessionType, Unattached, E_CREATE_NON_DIR, E_UNKNOWN_FID, }, }, tokio::{AsyncNineP, AsyncStream}, Result, }; +use simple_coro::CoroState; use std::{collections::btree_map::Entry, fs, future::Future, mem::size_of}; use tokio::{ net::{TcpListener, UnixListener}, @@ -24,7 +24,7 @@ use tokio::{ // re-exports pub use crate::sansio::server::{socket_dir, socket_path, ClientId, Server}; -/// The outcome of a client attempting to [read](Serve9p::read) a given file. +/// The outcome of a client attempting to [read](AsyncServe9p::read) a given file. #[derive(Debug)] pub enum ReadOutcome { /// The data is immediately available. @@ -239,61 +239,19 @@ where U: AsyncStream, { async fn handle_connection_async(mut self) { - use Tdata::*; - loop { let t = match Tmessage::read_from(&mut self.stream).await { Ok(t) => t, Err(_) => return, }; - let Tmessage { tag, content } = t; - - let resp = match content { - Version { msize, version } => { - self.state.seen_version = version == SUPPORTED_VERSION; - self.handle_version(msize, version) - } - - Auth { afid, uname, aname } => { - if !self.state.seen_version { - self.reply_async(tag, Err(E_NO_VERSION_MESSAGE.to_string())) - .await; - continue; - } - - self.handle_auth(afid, uname, aname) - } - - Attach { - fid, - afid, - uname, - aname, - } => { - if !self.state.seen_version { - self.reply_async(tag, Err(E_NO_VERSION_MESSAGE.to_string())) - .await; - continue; - } - - let (st, aqid) = match self.handle_attach(fid, afid, uname, aname) { - Err(e) => { - self.reply_async(tag, Err(e)).await; - continue; - } - - Ok((st, aqid)) => (st, aqid), - }; - + match self.handle_tmessage_unattached(t) { + Either::L((tag, resp)) => self.reply_async(tag, resp).await, + Either::R((tag, st, aqid)) => { self.reply_async(tag, Ok(Rdata::Attach { aqid })).await; return self.into_attached(st).handle_connection_async().await; } - - _ => Err(E_UNATTACHED.into()), - }; - - self.reply_async(tag, resp).await; + } } } } @@ -333,12 +291,10 @@ where let resp = match content { Version { msize, version } => { - let res = self.handle_version(msize, version); - if res.is_ok() { - self.clunk_and_clear_async().await; - } + let resp = self.handle_version(msize, version); + self.clunk_and_clear_async().await; - res + Ok(resp) } Auth { .. } | Attach { .. } => Err("session is already attached".into()), Flush { .. } => Ok(Rdata::Flush {}), @@ -426,25 +382,20 @@ where new_fid: u32, wnames: Vec, ) -> Result { - let fm = match self.prep_walk(fid, new_fid, &wnames)? { - Either::L(rdata) => return Ok(rdata), - Either::R(fm) => fm, - }; - - let mut wqids = Vec::with_capacity(wnames.len()); - let mut qid = fm.qid; - - for name in wnames.iter() { - let fm = self - .s - .walk(self.client_id, qid, name, &self.state.uname) - .await?; - qid = fm.qid; - wqids.push(fm.as_qid()); - self.qids.insert(qid, fm); - } + let client_id = self.client_id; + let mut coro = self + .session_state + .handle_attached_walk(fid, new_fid, &wnames); - Ok(self.complete_walk(new_fid, wqids, wnames.len())) + loop { + coro = match coro.resume() { + CoroState::Complete(res) => return res, + CoroState::Pending(c, (qid, name, uname)) => { + let fm = self.s.walk(client_id, qid, name, uname).await?; + c.send(fm) + } + }; + } } async fn handle_clunk_async(&mut self, fid: u32) -> Result { @@ -538,32 +489,20 @@ where count: u32, tx: &UnboundedSender<(u16, Vec)>, ) -> Result> { - use FileType::*; - - let fm = self.try_file_meta(fid)?; - if offset > u32::MAX as u64 { - return Err(format!("offset too large: {offset} > {}", u32::MAX)); - } - - let data = match fm.ty { - Directory => { - self.read_dir_async(fm.qid, offset as usize, count as usize) - .await? + let cid = self.client_id; + let coro = self.session_state.handle_attached_read(fid, offset, count); + let (offset, count) = (offset as usize, count as usize); + + match coro.resume() { + CoroState::Complete(res) => res, + CoroState::Pending(c, Either::L((qid, uname))) => { + let stats = self.s.read_dir(cid, qid, uname).await?; + c.send(stats).resume().unwrap() } - Regular | AppendOnly | Exclusive => { - let outcome = self - .s - .read( - self.client_id, - fm.qid, - offset as usize, - count as usize, - &self.state.uname, - ) - .await?; - + CoroState::Pending(_, Either::R((qid, uname))) => { + let outcome = self.s.read(cid, qid, offset, count, uname).await?; match outcome { - ReadOutcome::Immediate(data) => data, + ReadOutcome::Immediate(data) => Ok(Some(Rdata::Read { data: Data(data) })), ReadOutcome::Blocked(mut chan) => { let tx = tx.clone(); spawn(async move { @@ -571,55 +510,19 @@ where tx.send((tag, data)) }); - return Ok(None); + Ok(None) } } } - }; - - Ok(Some(Rdata::Read { data: Data(data) })) - } - - async fn read_dir_async(&mut self, qid: u64, offset: usize, count: usize) -> Result> { - let stats = self - .s - .read_dir(self.client_id, qid, &self.state.uname) - .await?; - - let mut buf = Vec::with_capacity(count); - let mut to_skip = offset; - - for stat in stats.into_iter() { - self.qids.entry(stat.fm.qid).or_insert(stat.fm.clone()); - - let rstat: RawStat = stat.into(); - let mut tmp = Vec::new(); - rstat.write_to(&mut tmp).await.unwrap(); - - if to_skip != 0 { - if tmp.len() > to_skip { - return Err(E_INVALID_OFFSET.to_string()); - } else { - to_skip -= tmp.len(); - continue; - } - } - - if buf.len() + tmp.len() > count { - break; - } - buf.extend(tmp); } - - Ok(buf) } async fn handle_write_async(&mut self, fid: u32, offset: u64, data: Vec) -> Result { - let fm = self.try_file_meta(fid)?; if offset > u32::MAX as u64 { return Err(format!("offset too large: {offset} > {}", u32::MAX)); } + let fm = self.try_file_meta(fid)?; let count = self .s .write( @@ -636,7 +539,6 @@ where async fn handle_remove_async(&mut self, fid: u32) -> Result { let fm = self.try_file_meta(fid)?; - self.s .remove(self.client_id, fm.qid, &self.state.uname) .await?; diff --git a/src/fsys/buffer.rs b/src/fsys/buffer.rs index 745822e..4c4360a 100644 --- a/src/fsys/buffer.rs +++ b/src/fsys/buffer.rs @@ -9,7 +9,7 @@ use crate::{ }, input::Event, }; -use ninep::{fs::Stat, sansio::server::ReadOutcome}; +use ninep::{fs::Stat, sync::server::ReadOutcome}; use std::{ collections::BTreeMap, sync::mpsc::{channel, Receiver, Sender}, diff --git a/src/fsys/event.rs b/src/fsys/event.rs index bffe745..8bf2ecf 100644 --- a/src/fsys/event.rs +++ b/src/fsys/event.rs @@ -8,7 +8,7 @@ use crate::{ input::Event, }; use ad_event::{FsysEvent, Kind, Source}; -use ninep::sansio::server::ReadOutcome; +use ninep::sync::server::ReadOutcome; use std::{ sync::mpsc::{channel, Receiver, Sender}, thread::spawn, diff --git a/src/fsys/log.rs b/src/fsys/log.rs index 5e39b90..4940008 100644 --- a/src/fsys/log.rs +++ b/src/fsys/log.rs @@ -1,4 +1,4 @@ -use ninep::sansio::server::{ClientId, ReadOutcome}; +use ninep::sync::server::{ClientId, ReadOutcome}; use std::{ collections::HashMap, mem::swap, From a513440edbf0d20b3a5d826afd3cffab14b1492b Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Wed, 5 Mar 2025 11:40:41 +0000 Subject: [PATCH 11/12] factoring out shared client logic using coroutines --- crates/ninep/src/sansio/client.rs | 312 ++++++++++++++++++ crates/ninep/src/sansio/mod.rs | 1 + crates/ninep/src/sansio/protocol.rs | 7 + crates/ninep/src/sync/client.rs | 455 ++++++++------------------- crates/ninep/src/tokio/client.rs | 469 ++++++++-------------------- 5 files changed, 583 insertions(+), 661 deletions(-) create mode 100644 crates/ninep/src/sansio/client.rs diff --git a/crates/ninep/src/sansio/client.rs b/crates/ninep/src/sansio/client.rs new file mode 100644 index 0000000..c40ea31 --- /dev/null +++ b/crates/ninep/src/sansio/client.rs @@ -0,0 +1,312 @@ +//! Traits and structs for implementing a 9p client +use crate::{ + fs::{Mode, Perm, Stat}, + sansio::protocol::{Data, RawStat, Rdata, Rmessage, Tdata, Tmessage}, + sync::SyncNineP, +}; +use simple_coro::{Coro, Handle, ReadyCoro}; +use std::{cmp::min, collections::HashMap, future::Future, io}; + +macro_rules! expect_rmessage { + ($resp:expr, $variant:ident { $($field:ident),+, .. }) => { + match $resp.content { + Rdata::$variant { $($field),+, .. } => ($($field),+), + Rdata::Error { ename } => return err(ename), + m => return err(format!("unexpected response: {m:?}")), + } + + }; + + ($resp:expr, $variant:ident { $($field:ident),+ }) => { + match $resp.content { + Rdata::$variant { $($field),+ } => ($($field),+), + Rdata::Error { ename } => return err(ename), + m => return err(format!("unexpected response: {m:?}")), + } + + }; +} + +pub(crate) const MSIZE: u32 = u16::MAX as u32; +pub(crate) const VERSION: &str = "9P2000"; + +type Coro9p = ReadyCoro, F>; + +pub(crate) fn err(e: E) -> io::Result +where + E: Into>, +{ + Err(io::Error::new(io::ErrorKind::Other, e)) +} + +/// Internal sans-IO state for a 9p client implementation that can be used along with an I/O stream +/// to implement a concrete Client. +#[derive(Debug)] +pub(crate) struct State { + pub(crate) uname: String, + pub(crate) msize: u32, + pub(crate) fids: HashMap, + pub(crate) next_fid: u32, +} + +impl State { + fn next_fid(&mut self) -> u32 { + let fid = self.next_fid; + self.next_fid += 1; + + fid + } + + /// Establish our connection to the target 9p server and begin the session. + pub(crate) fn handle_connect( + &mut self, + aname: String, + ) -> Coro9p<(), impl Future> + use<'_>> { + Coro::from(move |handle: Handle| async move { + let rmessage = handle + .yield_value(Tmessage::new( + u16::MAX, + Tdata::Version { + msize: MSIZE, + version: VERSION.to_string(), + }, + )) + .await; + + let (msize, version) = expect_rmessage!(rmessage, Version { msize, version }); + if version != VERSION { + return err("server version not supported"); + } + + handle + .yield_value(Tmessage::new( + 0, + Tdata::Attach { + fid: 0, + afid: u32::MAX, // no auth + uname: self.uname.clone(), + aname, + }, + )) + .await; + + self.msize = msize; + + Ok(()) + }) + } + + /// Associate the given path with a new fid. + pub(crate) fn handle_walk( + &mut self, + path: String, + ) -> Coro9p> + use<'_>> { + Coro::from(move |handle: Handle| async move { + let new_fid = { + if let Some(fid) = self.fids.get(&path) { + return Ok(*fid); + } + + self.next_fid() + }; + + // If the walk succeeds then we've successfully associated our new fid with this path + // and we don't currently do anything with the qids for each of the path elements + handle + .yield_value(Tmessage::new( + 0, + Tdata::Walk { + fid: 0, + new_fid, + wnames: path.split('/').map(Into::into).collect(), + }, + )) + .await; + + self.fids.insert(path, new_fid); + + Ok(new_fid) + }) + } + + /// Request the current [Stat] of the file or directory identified by the given path. + pub(crate) fn handle_stat( + &mut self, + path: String, + ) -> Coro9p> + use<'_>> { + Coro::from(move |handle: Handle| async move { + let fid = handle.yield_from(self.handle_walk(path)).await?; + let rmessage = handle + .yield_value(Tmessage::new(0, Tdata::Stat { fid })) + .await; + + let raw_stat = expect_rmessage!(rmessage, Stat { stat, .. }); + match raw_stat.try_into() { + Ok(s) => Ok(s), + Err(e) => err(e), + } + }) + } + + pub(crate) fn handle_read_count( + &mut self, + fid: u32, + offset: u64, + count: u32, + ) -> Coro9p, impl Future>> + use<'_>> { + Coro::from(move |handle: Handle| async move { + let rmessage = handle + .yield_value(Tmessage::new(0, Tdata::Read { fid, offset, count })) + .await; + let Data(data) = expect_rmessage!(rmessage, Read { data }); + + Ok(data) + }) + } + + fn _read_all( + &mut self, + path: String, + mode: Mode, + ) -> Coro9p, impl Future>> + use<'_>> { + Coro::from(move |handle: Handle| async move { + let fid = handle.yield_from(self.handle_walk(path)).await?; + let mode = mode.bits(); + handle + .yield_value(Tmessage::new(0, Tdata::Open { fid, mode })) + .await; + + let count = self.msize; + let mut bytes = Vec::new(); + let mut offset = 0; + loop { + let data = handle + .yield_from(self.handle_read_count(fid, offset, count)) + .await?; + if data.is_empty() { + break; + } + offset += data.len() as u64; + bytes.extend(data); + } + + Ok(bytes) + }) + } + + /// Read the full contents of the file at `path` as bytes. + pub(crate) fn handle_read( + &mut self, + path: String, + ) -> Coro9p, impl Future>> + use<'_>> { + self._read_all(path, Mode::FILE) + } + + /// Read the directory listing of the directory at `path`. + pub(crate) fn handle_read_dir( + &mut self, + path: String, + ) -> Coro9p, impl Future>> + use<'_>> { + Coro::from(move |handle: Handle| async move { + let bytes = handle.yield_from(self._read_all(path, Mode::DIR)).await?; + let mut buf = io::Cursor::new(bytes); + let mut stats: Vec = Vec::new(); + + loop { + match RawStat::read_from(&mut buf) { + Ok(rs) => match rs.try_into() { + Ok(s) => stats.push(s), + Err(e) => return err(e), + }, + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, + Err(e) => return Err(e), + } + } + + Ok(stats) + }) + } + + /// Write the provided data to the file at `path` at the given offset. + pub(crate) fn handle_write<'a, 's: 'a>( + &'s mut self, + path: String, + mut offset: u64, + content: &'a [u8], + ) -> Coro9p> + use<'a, 's>> { + Coro::from(move |handle: Handle| async move { + let fid = handle.yield_from(self.handle_walk(path)).await?; + let len = content.len(); + let mut cur = 0; + let header_size = 4 + 8 + 4; // fid + offset + data len + let chunk_size = (self.msize - header_size) as usize; + + while cur < len { + let end = min(cur + chunk_size, len); + let rmessage = handle + .yield_value(Tmessage::new( + 0, + Tdata::Write { + fid, + offset, + data: Data(content[cur..end].to_vec()), + }, + )) + .await; + let n = expect_rmessage!(rmessage, Write { count }); + if n == 0 { + break; + } + cur += n as usize; + offset += n as u64; + } + + if cur != len { + return err(format!("partial write: {cur} < {len}")); + } + + Ok(cur) + }) + } + + /// Attempt to create a new file within the connected filesystem. + pub(crate) fn handle_create( + &mut self, + dir: String, + name: String, + perms: Perm, + mode: Mode, + ) -> Coro9p<(), impl Future> + use<'_>> { + Coro::from(move |handle: Handle| async move { + let fid = handle.yield_from(self.handle_walk(dir)).await?; + handle + .yield_value(Tmessage::new( + 0, + Tdata::Create { + fid, + name, + perm: perms.bits(), + mode: mode.bits(), + }, + )) + .await; + + Ok(()) + }) + } + + /// Attempt to remove a file from the connected filesystem. + pub(crate) fn handle_remove( + &mut self, + path: String, + ) -> Coro9p<(), impl Future> + use<'_>> { + Coro::from(move |handle: Handle| async move { + let fid = handle.yield_from(self.handle_walk(path)).await?; + handle + .yield_value(Tmessage::new(0, Tdata::Remove { fid })) + .await; + + Ok(()) + }) + } +} diff --git a/crates/ninep/src/sansio/mod.rs b/crates/ninep/src/sansio/mod.rs index e71a6aa..f653a6c 100644 --- a/crates/ninep/src/sansio/mod.rs +++ b/crates/ninep/src/sansio/mod.rs @@ -6,6 +6,7 @@ use crate::{ Result, }; +pub mod client; pub mod protocol; pub mod server; diff --git a/crates/ninep/src/sansio/protocol.rs b/crates/ninep/src/sansio/protocol.rs index 844cb2a..668ded5 100644 --- a/crates/ninep/src/sansio/protocol.rs +++ b/crates/ninep/src/sansio/protocol.rs @@ -568,6 +568,13 @@ pub struct Tmessage { pub content: Tdata, } +impl Tmessage { + /// Construct a new [Tmessage] + pub const fn new(tag: u16, content: Tdata) -> Self { + Self { tag, content } + } +} + /// Generate the Tdata enum along with the wrapped T-message types and their implementations of NineP macro_rules! impl_tdata { ($( diff --git a/crates/ninep/src/sync/client.rs b/crates/ninep/src/sync/client.rs index 7d083d2..4f6548c 100644 --- a/crates/ninep/src/sync/client.rs +++ b/crates/ninep/src/sync/client.rs @@ -1,157 +1,89 @@ //! A simple 9p client for building out application specific client applications. use crate::{ fs::{Mode, Perm, Stat}, - sansio::protocol::{Data, RawStat, Rdata, Rmessage, Tdata, Tmessage}, + sansio::{ + client::{err, State, MSIZE}, + protocol::{Rdata, Rmessage, Tdata, Tmessage}, + }, sync::{SyncNineP, SyncStream}, }; +use simple_coro::CoroState; use std::{ - cmp::min, collections::HashMap, - env, - io::{self, Cursor, ErrorKind}, - mem, + env, io, mem, net::{TcpStream, ToSocketAddrs}, os::unix::net::UnixStream, + path::Path, sync::{Arc, Mutex, MutexGuard}, }; -// TODO: -// - need a proper error enum rather than just using io::Error - -macro_rules! expect_rmessage { - ($resp:expr, $variant:ident { $($field:ident),+, .. }) => { - match $resp.content { - Rdata::$variant { $($field),+, .. } => ($($field),+), - Rdata::Error { ename } => return err(ename), - m => return err(format!("unexpected response: {m:?}")), - } - - }; - - ($resp:expr, $variant:ident { $($field:ident),+ }) => { - match $resp.content { - Rdata::$variant { $($field),+ } => ($($field),+), - Rdata::Error { ename } => return err(ename), - m => return err(format!("unexpected response: {m:?}")), - } - - }; -} - -const MSIZE: u32 = u16::MAX as u32; -const VERSION: &str = "9P2000"; - -fn err(e: E) -> io::Result -where - E: Into>, -{ - Err(io::Error::new(io::ErrorKind::Other, e)) -} - -/// A client that operates over an underlying [UnixStream]. -pub type UnixClient = Client; - -/// A client that operates over an underlying [TcpStream]. -pub type TcpClient = Client; - -/// A 9p client. +/// A synchronous 9p client. /// /// Support for each of the operations exposed by this client is determined by the server /// implementation that it is connected to. #[derive(Debug)] -pub struct Client -where - S: SyncStream, -{ - /// The shared inner client holding our connection to the server - /// - /// Shared between clones - inner: Arc>>, - msize: u32, +pub struct Client { + state: Arc>, + stream: Arc>, } -impl Clone for Client -where - S: SyncStream, -{ +impl Clone for Client { fn clone(&self) -> Self { Self { - inner: Arc::clone(&self.inner), - msize: self.msize, + state: Arc::clone(&self.state), + stream: Arc::clone(&self.stream), } } } -#[derive(Debug)] -struct ClientInner -where - S: SyncStream, -{ - stream: S, - uname: String, - msize: u32, - fids: HashMap, - next_fid: u32, -} - -impl Drop for ClientInner -where - S: SyncStream, -{ - fn drop(&mut self) { - let fids = std::mem::take(&mut self.fids); - for (_, fid) in fids.into_iter() { - _ = self.send(0, Tdata::Clunk { fid }); +impl Client { + fn new(uname: impl Into, fids: HashMap, stream: S) -> Self { + Self { + state: Arc::new(Mutex::new(State { + uname: uname.into(), + msize: MSIZE, + fids, + next_fid: 1, + })), + stream: Arc::new(Mutex::new(stream)), } } -} - -impl ClientInner -where - S: SyncStream, -{ - fn send(&mut self, tag: u16, content: Tdata) -> io::Result { - let t = Tmessage { tag, content }; - t.write_to(&mut self.stream)?; - match Rmessage::read_from(&mut self.stream)? { - Rmessage { - content: Rdata::Error { ename }, - .. - } => err(ename), - msg => Ok(msg), + #[inline] + fn state(&self) -> MutexGuard<'_, State> { + match self.state.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), } } - fn next_fid(&mut self) -> u32 { - let fid = self.next_fid; - self.next_fid += 1; - - fid + #[inline] + fn stream(&self) -> MutexGuard<'_, S> { + match self.stream.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } } } +/// A client that operates over an underlying [UnixStream]. +pub type UnixClient = Client; + +/// A client that operates over an underlying [TcpStream]. +pub type TcpClient = Client; + impl Client { /// Create a new [Client] connected to a unix socket at the specified path. pub fn new_unix_with_explicit_path( - uname: String, - path: String, + uname: impl Into, + path: impl AsRef, aname: impl Into, ) -> io::Result { - let stream = UnixStream::connect(path)?; + let stream = UnixStream::connect(path.as_ref())?; let mut fids = HashMap::new(); fids.insert(String::new(), 0); - let mut client = Self { - inner: Arc::new(Mutex::new(ClientInner { - stream, - uname, - msize: MSIZE, - fids, - next_fid: 1, - })), - msize: MSIZE, - }; + let mut client = Self::new(uname, fids, stream); client.connect(aname)?; Ok(client) @@ -176,108 +108,80 @@ impl Client { impl Client { /// Create a new [Client] connected to a tcp socket at the specified address. - pub fn new_tcp(uname: String, addr: T, aname: impl Into) -> io::Result - where - T: ToSocketAddrs, - { + pub fn new_tcp( + uname: impl Into, + addr: impl ToSocketAddrs, + aname: impl Into, + ) -> io::Result { let stream = TcpStream::connect(addr)?; let mut fids = HashMap::new(); fids.insert(String::new(), 0); - let mut client = Self { - inner: Arc::new(Mutex::new(ClientInner { - stream, - uname, - msize: MSIZE, - fids, - next_fid: 1, - })), - msize: MSIZE, - }; + let mut client = Self::new(uname, fids, stream); client.connect(aname)?; Ok(client) } } +macro_rules! run_9p_coro { + ($self:ident, $method:ident, $($arg:expr),*) => {{ + let mut state = $self.state(); + let mut coro = state.$method($($arg),*); + loop { + coro = match coro.resume() { + CoroState::Complete(res) => break res, + CoroState::Pending(c, t) => { + let mut stream = $self.stream(); + t.write_to(&mut *stream)?; + + match Rmessage::read_from(&mut *stream)? { + Rmessage { + content: Rdata::Error { ename }, + .. + } => return err(ename), + rmessage => c.send(rmessage), + } + } + } + } + }}; +} + impl Client where S: SyncStream, { - #[inline] - fn inner(&mut self) -> MutexGuard<'_, ClientInner> { - match self.inner.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), + fn send(&mut self, tag: u16, content: Tdata) -> io::Result { + let mut stream = self.stream(); + Tmessage { tag, content }.write_to(&mut *stream)?; + + match Rmessage::read_from(&mut *stream)? { + Rmessage { + content: Rdata::Error { ename }, + .. + } => err(ename), + msg => Ok(msg), } } /// Establish our connection to the target 9p server and begin the session. fn connect(&mut self, aname: impl Into) -> io::Result<()> { - let mut inner = self.inner(); - let resp = inner.send( - u16::MAX, - Tdata::Version { - msize: MSIZE, - version: VERSION.to_string(), - }, - )?; - - let (msize, version) = expect_rmessage!(resp, Version { msize, version }); - if version != VERSION { - return err("server version not supported"); - } - inner.msize = msize; - let uname = inner.uname.clone(); - - inner.send( - 0, - Tdata::Attach { - fid: 0, - afid: u32::MAX, // no auth - uname, - aname: aname.into(), - }, - )?; - - drop(inner); - self.msize = msize; - - Ok(()) + run_9p_coro!(self, handle_connect, aname.into()) } /// Associate the given path with a new fid. pub fn walk(&mut self, path: impl Into) -> io::Result { - let mut inner = self.inner(); - let path = path.into(); - if let Some(fid) = inner.fids.get(&path) { - return Ok(*fid); - } - - let new_fid = inner.next_fid(); - - inner.send( - 0, - Tdata::Walk { - fid: 0, - new_fid, - wnames: path.split('/').map(Into::into).collect(), - }, - )?; - - inner.fids.insert(path, new_fid); - - Ok(new_fid) + run_9p_coro!(self, handle_walk, path.into()) } /// Free server side state for the given fid. /// /// Clunks of the root fid (0) will be ignored pub fn clunk(&mut self, fid: u32) -> io::Result<()> { - let mut inner = self.inner(); if fid != 0 { - inner.send(0, Tdata::Clunk { fid })?; - inner.fids.retain(|_, v| *v != fid); + self.send(0, Tdata::Clunk { fid })?; + self.state().fids.retain(|_, v| *v != fid); } Ok(()) @@ -285,7 +189,7 @@ where /// Free server side state for the given path. pub fn clunk_path(&mut self, path: impl Into) -> io::Result<()> { - let fid = match self.inner().fids.get(&path.into()) { + let fid = match self.state().fids.get(&path.into()) { Some(fid) => *fid, None => return Ok(()), }; @@ -295,52 +199,17 @@ where /// Request the current [Stat] of the file or directory identified by the given path. pub fn stat(&mut self, path: impl Into) -> io::Result { - let fid = self.walk(path)?; - let mut inner = self.inner(); - let resp = inner.send(0, Tdata::Stat { fid })?; - let raw_stat = expect_rmessage!(resp, Stat { stat, .. }); - - match raw_stat.try_into() { - Ok(s) => Ok(s), - Err(e) => err(e), - } - } - - fn _read_count(&mut self, fid: u32, offset: u64, count: u32) -> io::Result> { - let resp = self.inner().send(0, Tdata::Read { fid, offset, count })?; - let Data(data) = expect_rmessage!(resp, Read { data }); - - Ok(data) - } - - fn _read_all(&mut self, path: impl Into, mode: Mode) -> io::Result> { - let fid = self.walk(path)?; - let mode = mode.bits(); - self.inner().send(0, Tdata::Open { fid, mode })?; - - let count = self.msize; - let mut bytes = Vec::new(); - let mut offset = 0; - loop { - let data = self._read_count(fid, offset, count)?; - if data.is_empty() { - break; - } - offset += data.len() as u64; - bytes.extend(data); - } - - Ok(bytes) + run_9p_coro!(self, handle_stat, path.into()) } /// Read the full contents of the file at `path` as bytes. pub fn read(&mut self, path: impl Into) -> io::Result> { - self._read_all(path, Mode::FILE) + run_9p_coro!(self, handle_read, path.into()) } /// Read the full contents of the file at `path` as utf-8 encoded text. pub fn read_str(&mut self, path: impl Into) -> io::Result { - let bytes = self._read_all(path, Mode::FILE)?; + let bytes = run_9p_coro!(self, handle_read, path.into())?; let s = match String::from_utf8(bytes) { Ok(s) => s, Err(_) => return err("invalid utf8"), @@ -351,22 +220,43 @@ where /// Read the directory listing of the directory at `path`. pub fn read_dir(&mut self, path: impl Into) -> io::Result> { - let bytes = self._read_all(path, Mode::DIR)?; - let mut buf = Cursor::new(bytes); - let mut stats: Vec = Vec::new(); + run_9p_coro!(self, handle_read_dir, path.into()) + } - loop { - match RawStat::read_from(&mut buf) { - Ok(rs) => match rs.try_into() { - Ok(s) => stats.push(s), - Err(e) => return err(e), - }, - Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, - Err(e) => return Err(e), - } - } + /// Write the provided data to the file at `path` at the given offset. + pub fn write( + &mut self, + path: impl Into, + offset: u64, + content: &[u8], + ) -> io::Result { + run_9p_coro!(self, handle_write, path.into(), offset, content) + } - Ok(stats) + /// Write the provided string data to the file at `path` at the given offset. + pub fn write_str( + &mut self, + path: impl Into, + offset: u64, + content: &str, + ) -> io::Result { + run_9p_coro!(self, handle_write, path.into(), offset, content.as_bytes()) + } + + /// Attempt to create a new file within the connected filesystem. + pub fn create( + &mut self, + dir: impl Into, + name: impl Into, + perms: Perm, + mode: Mode, + ) -> io::Result<()> { + run_9p_coro!(self, handle_create, dir.into(), name.into(), perms, mode) + } + + /// Attempt to remove a file from the connected filesystem. + pub fn remove(&mut self, path: impl Into) -> io::Result<()> { + run_9p_coro!(self, handle_remove, path.into()) } /// Iterate over Vec's of bytes from the file at `path`. @@ -376,8 +266,8 @@ where pub fn iter_chunks(&mut self, path: impl Into) -> io::Result> { let fid = self.walk(path)?; let mode = Mode::FILE.bits(); - let count = self.msize; - self.inner().send(0, Tdata::Open { fid, mode })?; + let count = self.state().msize; + self.send(0, Tdata::Open { fid, mode })?; Ok(ChunkIter { client: self.clone(), @@ -391,8 +281,8 @@ where pub fn iter_lines(&mut self, path: impl Into) -> io::Result> { let fid = self.walk(path)?; let mode = Mode::FILE.bits(); - let count = self.msize; - self.inner().send(0, Tdata::Open { fid, mode })?; + let count = self.state().msize; + self.send(0, Tdata::Open { fid, mode })?; Ok(ReadLineIter { client: self.clone(), @@ -404,83 +294,8 @@ where }) } - /// Write the provided data to the file at `path` at the given offset. - pub fn write( - &mut self, - path: impl Into, - mut offset: u64, - content: &[u8], - ) -> io::Result { - let fid = self.walk(path)?; - let len = content.len(); - let mut cur = 0; - let header_size = 4 + 8 + 4; // fid + offset + data len - let chunk_size = (self.msize - header_size) as usize; - let mut inner = self.inner(); - - while cur < len { - let end = min(cur + chunk_size, len); - let resp = inner.send( - 0, - Tdata::Write { - fid, - offset, - data: Data(content[cur..end].to_vec()), - }, - )?; - let n = expect_rmessage!(resp, Write { count }); - if n == 0 { - break; - } - cur += n as usize; - offset += n as u64; - } - - if cur != len { - return err(format!("partial write: {cur} < {len}")); - } - - Ok(cur) - } - - /// Write the provided string data to the file at `path` at the given offset. - pub fn write_str( - &mut self, - path: impl Into, - offset: u64, - content: &str, - ) -> io::Result { - self.write(path, offset, content.as_bytes()) - } - - /// Attempt to create a new file within the connected filesystem. - pub fn create( - &mut self, - dir: impl Into, - name: impl Into, - perms: Perm, - mode: Mode, - ) -> io::Result<()> { - let fid = self.walk(dir)?; - self.inner().send( - 0, - Tdata::Create { - fid, - name: name.into(), - perm: perms.bits(), - mode: mode.bits(), - }, - )?; - - Ok(()) - } - - /// Attempt to remove a file from the connected filesystem. - pub fn remove(&mut self, path: impl Into) -> io::Result<()> { - let fid = self.walk(path)?; - self.inner().send(0, Tdata::Remove { fid })?; - - Ok(()) + fn _read_count(&mut self, fid: u32, offset: u64, count: u32) -> io::Result> { + run_9p_coro!(self, handle_read_count, fid, offset, count) } } @@ -541,6 +356,7 @@ where fn next(&mut self) -> Option { if self.at_eof { + _ = self.client.clunk(self.fid); return None; } @@ -564,6 +380,7 @@ where if data.is_empty() { self.at_eof = true; if self.buf.is_empty() { + _ = self.client.clunk(self.fid); return None; } return String::from_utf8(mem::take(&mut self.buf)).ok(); diff --git a/crates/ninep/src/tokio/client.rs b/crates/ninep/src/tokio/client.rs index f5697b9..500ceb0 100644 --- a/crates/ninep/src/tokio/client.rs +++ b/crates/ninep/src/tokio/client.rs @@ -1,147 +1,70 @@ //! A simple async 9p client for building out application specific client applications. use crate::{ fs::{Mode, Perm, Stat}, - sansio::protocol::{Data, RawStat, Rdata, Rmessage, Tdata, Tmessage}, + sansio::{ + client::{err, State, MSIZE}, + protocol::{Rdata, Rmessage, Tdata, Tmessage}, + }, tokio::{AsyncNineP, AsyncStream}, }; -use std::{ - cmp::min, - collections::HashMap, - env, - io::{self, Cursor, ErrorKind}, - mem, - sync::Arc, -}; +use simple_coro::CoroState; +use std::{collections::HashMap, env, io, mem, path::Path, sync::Arc}; use tokio::{ net::{TcpStream, ToSocketAddrs, UnixStream}, sync::Mutex, }; -// TODO: -// - need a proper error enum rather than just using io::Error - -macro_rules! expect_rmessage { - ($resp:expr, $variant:ident { $($field:ident),+, .. }) => { - match $resp.content { - Rdata::$variant { $($field),+, .. } => ($($field),+), - Rdata::Error { ename } => return err(ename), - m => return err(format!("unexpected response: {m:?}")), - } - - }; - - ($resp:expr, $variant:ident { $($field:ident),+ }) => { - match $resp.content { - Rdata::$variant { $($field),+ } => ($($field),+), - Rdata::Error { ename } => return err(ename), - m => return err(format!("unexpected response: {m:?}")), - } - - }; -} - -const MSIZE: u32 = u16::MAX as u32; -const VERSION: &str = "9P2000"; - -fn err(e: E) -> io::Result -where - E: Into>, -{ - Err(io::Error::new(io::ErrorKind::Other, e)) -} - -/// A client that operates over an underlying [UnixStream]. -pub type UnixClient = Client; - -/// A client that operates over an underlying [TcpStream]. -pub type TcpClient = Client; - /// A 9p client. /// /// Support for each of the operations exposed by this client is determined by the server /// implementation that it is connected to. #[derive(Debug)] -pub struct Client -where - S: AsyncStream, -{ - /// The shared inner client holding our connection to the server - /// - /// Shared between clones - inner: Arc>>, - msize: u32, +pub struct Client { + state: Arc>, + stream: Arc>, } -impl Clone for Client -where - S: AsyncStream, -{ +impl Clone for Client { fn clone(&self) -> Self { Self { - inner: Arc::clone(&self.inner), - msize: self.msize, + state: Arc::clone(&self.state), + stream: Arc::clone(&self.stream), } } } -#[derive(Debug)] -struct ClientInner -where - S: AsyncStream, -{ - stream: S, - uname: String, - msize: u32, - fids: HashMap, - next_fid: u32, -} - -impl ClientInner -where - S: AsyncStream, -{ - async fn send(&mut self, tag: u16, content: Tdata) -> io::Result { - let t = Tmessage { tag, content }; - t.write_to(&mut self.stream).await?; - - match Rmessage::read_from(&mut self.stream).await? { - Rmessage { - content: Rdata::Error { ename }, - .. - } => err(ename), - msg => Ok(msg), +impl Client { + fn new(uname: impl Into, fids: HashMap, stream: S) -> Self { + Self { + state: Arc::new(Mutex::new(State { + uname: uname.into(), + msize: MSIZE, + fids, + next_fid: 1, + })), + stream: Arc::new(Mutex::new(stream)), } } +} - fn next_fid(&mut self) -> u32 { - let fid = self.next_fid; - self.next_fid += 1; +/// A client that operates over an underlying tokio [UnixStream]. +pub type UnixClient = Client; - fid - } -} +/// A client that operates over an underlying tokio [TcpStream]. +pub type TcpClient = Client; impl Client { /// Create a new [Client] connected to a unix socket at the specified path. pub async fn new_unix_with_explicit_path( - uname: String, - path: String, + uname: impl Into, + path: impl AsRef, aname: impl Into, ) -> io::Result { - let stream = UnixStream::connect(path).await?; + let stream = UnixStream::connect(path.as_ref()).await?; let mut fids = HashMap::new(); fids.insert(String::new(), 0); - let mut client = Self { - inner: Arc::new(Mutex::new(ClientInner { - stream, - uname, - msize: MSIZE, - fids, - next_fid: 1, - })), - msize: MSIZE, - }; + let mut client = Self::new(uname, fids, stream); client.connect(aname).await?; Ok(client) @@ -166,106 +89,82 @@ impl Client { impl Client { /// Create a new [Client] connected to a tcp socket at the specified address. - pub async fn new_tcp(uname: String, addr: T, aname: impl Into) -> io::Result - where - T: ToSocketAddrs, - { + pub async fn new_tcp( + uname: impl Into, + addr: impl ToSocketAddrs, + aname: impl Into, + ) -> io::Result { let stream = TcpStream::connect(addr).await?; let mut fids = HashMap::new(); fids.insert(String::new(), 0); - let mut client = Self { - inner: Arc::new(Mutex::new(ClientInner { - stream, - uname, - msize: MSIZE, - fids, - next_fid: 1, - })), - msize: MSIZE, - }; + let mut client = Self::new(uname, fids, stream); client.connect(aname).await?; Ok(client) } } +macro_rules! run_9p_coro { + ($self:ident, $method:ident, $($arg:expr),*) => { + { + let mut state = $self.state.lock().await; + let mut coro = state.$method($($arg),*); + loop { + coro = match coro.resume() { + CoroState::Complete(res) => break res, + CoroState::Pending(c, t) => { + let mut stream = $self.stream.lock().await; + t.write_to(&mut *stream).await?; + + match Rmessage::read_from(&mut *stream).await? { + Rmessage { + content: Rdata::Error { ename }, + .. + } => return err(ename), + rmessage => c.send(rmessage), + } + } + } + } + } + }; +} + impl Client where S: AsyncStream, { - /// Establish our connection to the target 9p server and begin the session. - async fn connect(&mut self, aname: impl Into) -> io::Result<()> { - let mut inner = self.inner.lock().await; - let resp = inner - .send( - u16::MAX, - Tdata::Version { - msize: MSIZE, - version: VERSION.to_string(), - }, - ) - .await?; - - let (msize, version) = expect_rmessage!(resp, Version { msize, version }); - if version != VERSION { - return err("server version not supported"); + async fn send(&mut self, tag: u16, content: Tdata) -> io::Result { + let mut stream = self.stream.lock().await; + Tmessage { tag, content }.write_to(&mut *stream).await?; + + match Rmessage::read_from(&mut *stream).await? { + Rmessage { + content: Rdata::Error { ename }, + .. + } => err(ename), + msg => Ok(msg), } - inner.msize = msize; - let uname = inner.uname.clone(); - - inner - .send( - 0, - Tdata::Attach { - fid: 0, - afid: u32::MAX, // no auth - uname, - aname: aname.into(), - }, - ) - .await?; - - drop(inner); - self.msize = msize; + } - Ok(()) + /// Establish our connection to the target 9p server and begin the session. + async fn connect(&mut self, aname: impl Into) -> io::Result<()> { + run_9p_coro!(self, handle_connect, aname.into()) } /// Associate the given path with a new fid. pub async fn walk(&mut self, path: impl Into) -> io::Result { - let mut inner = self.inner.lock().await; - let path = path.into(); - if let Some(fid) = inner.fids.get(&path) { - return Ok(*fid); - } - - let new_fid = inner.next_fid(); - - inner - .send( - 0, - Tdata::Walk { - fid: 0, - new_fid, - wnames: path.split('/').map(Into::into).collect(), - }, - ) - .await?; - - inner.fids.insert(path, new_fid); - - Ok(new_fid) + run_9p_coro!(self, handle_walk, path.into()) } /// Free server side state for the given fid. /// /// Clunks of the root fid (0) will be ignored pub async fn clunk(&mut self, fid: u32) -> io::Result<()> { - let mut inner = self.inner.lock().await; if fid != 0 { - inner.send(0, Tdata::Clunk { fid }).await?; - inner.fids.retain(|_, v| *v != fid); + self.send(0, Tdata::Clunk { fid }).await?; + self.state.lock().await.fids.retain(|_, v| *v != fid); } Ok(()) @@ -273,7 +172,7 @@ where /// Free server side state for the given path. pub async fn clunk_path(&mut self, path: impl Into) -> io::Result<()> { - let fid = match self.inner.lock().await.fids.get(&path.into()) { + let fid = match self.state.lock().await.fids.get(&path.into()) { Some(fid) => *fid, None => return Ok(()), }; @@ -283,61 +182,17 @@ where /// Request the current [Stat] of the file or directory identified by the given path. pub async fn stat(&mut self, path: impl Into) -> io::Result { - let fid = self.walk(path).await?; - let mut inner = self.inner.lock().await; - let resp = inner.send(0, Tdata::Stat { fid }).await?; - let raw_stat = expect_rmessage!(resp, Stat { stat, .. }); - - match raw_stat.try_into() { - Ok(s) => Ok(s), - Err(e) => err(e), - } - } - - async fn _read_count(&mut self, fid: u32, offset: u64, count: u32) -> io::Result> { - let resp = self - .inner - .lock() - .await - .send(0, Tdata::Read { fid, offset, count }) - .await?; - let Data(data) = expect_rmessage!(resp, Read { data }); - - Ok(data) - } - - async fn _read_all(&mut self, path: impl Into, mode: Mode) -> io::Result> { - let fid = self.walk(path).await?; - let mode = mode.bits(); - self.inner - .lock() - .await - .send(0, Tdata::Open { fid, mode }) - .await?; - - let count = self.msize; - let mut bytes = Vec::new(); - let mut offset = 0; - loop { - let data = self._read_count(fid, offset, count).await?; - if data.is_empty() { - break; - } - offset += data.len() as u64; - bytes.extend(data); - } - - Ok(bytes) + run_9p_coro!(self, handle_stat, path.into()) } /// Read the full contents of the file at `path` as bytes. pub async fn read(&mut self, path: impl Into) -> io::Result> { - self._read_all(path, Mode::FILE).await + run_9p_coro!(self, handle_read, path.into()) } /// Read the full contents of the file at `path` as utf-8 encoded text. pub async fn read_str(&mut self, path: impl Into) -> io::Result { - let bytes = self._read_all(path, Mode::FILE).await?; + let bytes = run_9p_coro!(self, handle_read, path.into())?; let s = match String::from_utf8(bytes) { Ok(s) => s, Err(_) => return err("invalid utf8"), @@ -348,22 +203,43 @@ where /// Read the directory listing of the directory at `path`. pub async fn read_dir(&mut self, path: impl Into) -> io::Result> { - let bytes = self._read_all(path, Mode::DIR).await?; - let mut buf = Cursor::new(bytes); - let mut stats: Vec = Vec::new(); + run_9p_coro!(self, handle_read_dir, path.into()) + } - loop { - match RawStat::read_from(&mut buf).await { - Ok(rs) => match rs.try_into() { - Ok(s) => stats.push(s), - Err(e) => return err(e), - }, - Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, - Err(e) => return Err(e), - } - } + /// Write the provided data to the file at `path` at the given offset. + pub async fn write( + &mut self, + path: impl Into, + offset: u64, + content: &[u8], + ) -> io::Result { + run_9p_coro!(self, handle_write, path.into(), offset, content) + } + + /// Write the provided string data to the file at `path` at the given offset. + pub async fn write_str( + &mut self, + path: impl Into, + offset: u64, + content: &str, + ) -> io::Result { + run_9p_coro!(self, handle_write, path.into(), offset, content.as_bytes()) + } - Ok(stats) + /// Attempt to create a new file within the connected filesystem. + pub async fn create( + &mut self, + dir: impl Into, + name: impl Into, + perms: Perm, + mode: Mode, + ) -> io::Result<()> { + run_9p_coro!(self, handle_create, dir.into(), name.into(), perms, mode) + } + + /// Attempt to remove a file from the connected filesystem. + pub async fn remove(&mut self, path: impl Into) -> io::Result<()> { + run_9p_coro!(self, handle_remove, path.into()) } /// Asynchronously iterate over Vec's of bytes from the file at `path`. @@ -376,12 +252,8 @@ where pub async fn stream_chunks(&mut self, path: impl Into) -> io::Result> { let fid = self.walk(path).await?; let mode = Mode::FILE.bits(); - let count = self.msize; - self.inner - .lock() - .await - .send(0, Tdata::Open { fid, mode }) - .await?; + let count = self.state.lock().await.msize; + self.send(0, Tdata::Open { fid, mode }).await?; Ok(ChunkStream { client: self.clone(), @@ -398,12 +270,8 @@ where pub async fn stream_lines(&mut self, path: impl Into) -> io::Result> { let fid = self.walk(path).await?; let mode = Mode::FILE.bits(); - let count = self.msize; - self.inner - .lock() - .await - .send(0, Tdata::Open { fid, mode }) - .await?; + let count = self.state.lock().await.msize; + self.send(0, Tdata::Open { fid, mode }).await?; Ok(ReadLineStream { client: self.clone(), @@ -415,93 +283,8 @@ where }) } - /// Write the provided data to the file at `path` at the given offset. - pub async fn write( - &mut self, - path: impl Into, - mut offset: u64, - content: &[u8], - ) -> io::Result { - let fid = self.walk(path).await?; - let len = content.len(); - let mut cur = 0; - let header_size = 4 + 8 + 4; // fid + offset + data len - let chunk_size = (self.msize - header_size) as usize; - let mut inner = self.inner.lock().await; - - while cur < len { - let end = min(cur + chunk_size, len); - let resp = inner - .send( - 0, - Tdata::Write { - fid, - offset, - data: Data(content[cur..end].to_vec()), - }, - ) - .await?; - let n = expect_rmessage!(resp, Write { count }); - if n == 0 { - break; - } - cur += n as usize; - offset += n as u64; - } - - if cur != len { - return err(format!("partial write: {cur} < {len}")); - } - - Ok(cur) - } - - /// Write the provided string data to the file at `path` at the given offset. - pub async fn write_str( - &mut self, - path: impl Into, - offset: u64, - content: &str, - ) -> io::Result { - self.write(path, offset, content.as_bytes()).await - } - - /// Attempt to create a new file within the connected filesystem. - pub async fn create( - &mut self, - dir: impl Into, - name: impl Into, - perms: Perm, - mode: Mode, - ) -> io::Result<()> { - let fid = self.walk(dir).await?; - self.inner - .lock() - .await - .send( - 0, - Tdata::Create { - fid, - name: name.into(), - perm: perms.bits(), - mode: mode.bits(), - }, - ) - .await?; - - Ok(()) - } - - /// Attempt to remove a file from the connected filesystem. - pub async fn remove(&mut self, path: impl Into) -> io::Result<()> { - let fid = self.walk(path).await?; - self.inner - .lock() - .await - .send(0, Tdata::Remove { fid }) - .await?; - - Ok(()) + async fn _read_count(&mut self, fid: u32, offset: u64, count: u32) -> io::Result> { + run_9p_coro!(self, handle_read_count, fid, offset, count) } } @@ -561,6 +344,7 @@ where /// Await the next newline delimited line out of a file. pub async fn next(&mut self) -> Option { if self.at_eof { + _ = self.client.clunk(self.fid); return None; } @@ -585,6 +369,7 @@ where if data.is_empty() { self.at_eof = true; if self.buf.is_empty() { + _ = self.client.clunk(self.fid); return None; } return String::from_utf8(mem::take(&mut self.buf)).ok(); From 4682a8206e6bcd2571da65933d08ec6e28efa96c Mon Sep 17 00:00:00 2001 From: Innes Anderson-Morrison Date: Wed, 5 Mar 2025 12:14:16 +0000 Subject: [PATCH 12/12] pulling simple_coro from crates.io --- Cargo.lock | 13 +++++++------ crates/ninep/Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0aa153f..a98ffdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,11 +204,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" -[[package]] -name = "crimes" -version = "0.1.0" -source = "git+https://github.com/sminez/crimes.git#6591acaba40572901ca6e2bbeaf1727562fa50c7" - [[package]] name = "criterion" version = "0.5.1" @@ -452,7 +447,7 @@ name = "ninep" version = "0.4.0" dependencies = [ "bitflags 2.8.0", - "crimes", + "simple_coro", "simple_test_case", "tokio", ] @@ -704,6 +699,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simple_coro" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9c4b3d9397b286092543bdfc5289ba7bd09295187176367b6545e01882911f" + [[package]] name = "simple_test_case" version = "1.2.0" diff --git a/crates/ninep/Cargo.toml b/crates/ninep/Cargo.toml index 3dad518..2732873 100644 --- a/crates/ninep/Cargo.toml +++ b/crates/ninep/Cargo.toml @@ -20,7 +20,7 @@ tokio = ["dep:tokio"] [dependencies] bitflags = "2.6" tokio = { version = "1.43.0", features = ["macros", "net", "io-util", "rt", "sync"], optional = true } -simple_coro = { git = "https://github.com/sminez/crimes.git", package = "crimes" } +simple_coro = "0.1" [dev-dependencies] simple_test_case = "1"