Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 26 additions & 51 deletions sdk/std/sys/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,12 @@ enum Inner {
Served(Arc<Served>),
}

/// A file open on a file server.
/// A file open on a file server, which keeps its offset.
struct Served {
dir: Arc<Capability>,
rel: String,
append: bool,
state: Mutex<Position>,
}

struct Position {
fid: u64,
generation: u64,
offset: u64,
}

#[path = "turns.rs"]
Expand Down Expand Up @@ -378,6 +372,7 @@ fn served_error(e: toyos::fs::Error) -> io::Error {
Error::InvalidRequest => io::const_error!(Kind::InvalidInput, "the file server takes no such request"),
Error::Unsupported => io::const_error!(Kind::Unsupported, "the volume's format has no such thing"),
Error::OutOfResources => io::const_error!(Kind::OutOfMemory, "the kernel had no room for the request"),
Error::NoDirectory => io::const_error!(Kind::NotADirectory, "the connection was given files and no directory"),
Error::Gone => io::const_error!(Kind::StaleNetworkFileHandle, "the file server is gone"),
Error::NotHeld => io::const_error!(Kind::NotFound, "this process holds no such directory"),
Error::Malformed => io::const_error!(Kind::InvalidData, "the file server answered outside its protocol"),
Expand Down Expand Up @@ -480,19 +475,16 @@ pub fn is_served(path: &Path) -> bool {
impl Served {
fn with<T>(
&self,
op: impl FnOnce(&mut toyos::fs::Dir, &mut Position) -> Result<T, toyos::fs::Error>,
op: impl FnOnce(&mut toyos::fs::Dir, u64, u64) -> Result<T, toyos::fs::Error>,
) -> io::Result<T> {
let mut dir = self.dir.lock();
let mut pos = self.state.lock().unwrap();
op(&mut dir, &mut pos).map_err(served_error)
op(&mut dir, self.fid, self.generation).map_err(served_error)
}
}

impl Drop for Served {
fn drop(&mut self) {
let mut dir = self.dir.lock();
let pos = self.state.lock().unwrap();
dir.close(pos.fid, pos.generation);
self.dir.lock().close(self.fid, self.generation);
}
}

Expand All @@ -515,16 +507,13 @@ impl File {
}

/// A kernel handle a child may write this file through: the file's own, or
/// a pipe its server appends to the file from this file's offset. The
/// pipe lives as long as the answer and the child's copy of it.
/// a pipe its server writes to the file at this file's offset. The pipe
/// lives as long as the answer and the child's copy of it.
pub fn as_child_stdio(&self) -> io::Result<(RawHandle, Option<toyos::Pipe>)> {
match &self.0 {
Inner::Kernel(h) => Ok((*h, None)),
Inner::Served(s) => {
let pipe = s.with(|dir, pos| {
let at = if s.append { dir.fstat(pos.fid, pos.generation)?.size } else { pos.offset };
dir.stream(pos.fid, pos.generation, at)
})?;
let pipe = s.with(|dir, fid, generation| dir.stream(fid, generation))?;
Ok((toyos::AsHandle::as_handle(&pipe), Some(pipe)))
}
}
Expand All @@ -543,12 +532,8 @@ impl File {
Ok(File(Inner::Served(Arc::new(Served {
dir: Arc::clone(cap),
rel: String::from(rel),
append: opts.append,
state: Mutex::new(Position {
fid: opened.fid,
generation: opened.generation,
offset: 0,
}),
fid: opened.fid,
generation: opened.generation,
}))))
},
)
Expand All @@ -557,14 +542,14 @@ impl File {
pub fn file_attr(&self) -> io::Result<FileAttr> {
match &self.0 {
Inner::Kernel(h) => syscall::fstat(*h).map(FileAttr::of_kernel).map_err(to_io_error),
Inner::Served(s) => s.with(|dir, pos| dir.fstat(pos.fid, pos.generation)).map(FileAttr::of),
Inner::Served(s) => s.with(|dir, fid, generation| dir.fstat(fid, generation)).map(FileAttr::of),
}
}

pub fn fsync(&self) -> io::Result<()> {
match &self.0 {
Inner::Kernel(h) => syscall::fsync(*h).map_err(to_io_error),
Inner::Served(s) => s.with(|dir, pos| dir.fsync(pos.fid, pos.generation)),
Inner::Served(s) => s.with(|dir, fid, generation| dir.fsync(fid, generation)),
}
}

Expand All @@ -591,18 +576,14 @@ impl File {
pub fn truncate(&self, size: u64) -> io::Result<()> {
match &self.0 {
Inner::Kernel(h) => syscall::ftruncate(*h, size).map_err(to_io_error),
Inner::Served(s) => s.with(|dir, pos| dir.truncate(pos.fid, pos.generation, size)),
Inner::Served(s) => s.with(|dir, fid, generation| dir.truncate(fid, generation, size)),
}
}

pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
match &self.0 {
Inner::Kernel(h) => syscall::read(*h, buf).map_err(to_io_error),
Inner::Served(s) => s.with(|dir, pos| {
let n = dir.read(pos.fid, pos.generation, pos.offset, buf)?;
pos.offset += n as u64;
Ok(n)
}),
Inner::Served(s) => s.with(|dir, fid, generation| dir.read(fid, generation, buf)),
}
}

Expand Down Expand Up @@ -637,11 +618,7 @@ impl File {
pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
match &self.0 {
Inner::Kernel(h) => syscall::write(*h, buf).map_err(to_io_error),
Inner::Served(s) => s.with(|dir, pos| {
let written = dir.write(pos.fid, pos.generation, pos.offset, buf)?;
pos.offset = written.offset;
Ok(written.len)
}),
Inner::Served(s) => s.with(|dir, fid, generation| dir.write(fid, generation, buf).map(|w| w.len)),
}
}

Expand Down Expand Up @@ -681,20 +658,18 @@ impl File {
};
syscall::seek(*h, abi_pos).map_err(to_io_error)
}
Inner::Served(s) => s.with(|dir, p| {
let (base, delta) = match pos {
SeekFrom::Start(n) => (n, 0),
SeekFrom::Current(d) => (p.offset, d),
SeekFrom::End(d) => (dir.fstat(p.fid, p.generation)?.size, d),
Inner::Served(s) => {
let (whence, delta) = match pos {
// Past the largest offset a file has is the request's fault, as the kernel's route says.
SeekFrom::Start(n) => {
let n = i64::try_from(n).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
(toyos::fs::SEEK_START, n)
}
SeekFrom::Current(d) => (toyos::fs::SEEK_CURRENT, d),
SeekFrom::End(d) => (toyos::fs::SEEK_END, d),
};
// Past either end is the request's fault, as the kernel's route says.
let at = base
.checked_add_signed(delta)
.filter(|&at| at <= toyos::fs::MAX_FILE_BYTES)
.ok_or(toyos::fs::Error::InvalidRequest)?;
p.offset = at;
Ok(at)
}),
s.with(|dir, fid, generation| dir.seek(fid, generation, whence, delta))
}
}
}

Expand Down
134 changes: 109 additions & 25 deletions sdk/toyos/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
//! target and the rest of the path in the window, and resolved again in the
//! client's own table, so it reaches nothing the client does not hold.
//!
//! **An open file is a description the server keeps**: the file, the access
//! the open fixed and the offset. A connection names descriptions by id, and
//! [`DUP`] and [`EXPORT`] make second ids that share one description's offset,
//! on the same connection or on a connection made for another process. An
//! exported connection was given files and no directory: it names no path.
//!
//! **One connection per directory per process, a request and then its
//! reply.** The client lends the server a [`WINDOW_BYTES`] region at connect
//! ([`HELLO`]); paths, data and listings travel through it and the frames stay
Expand Down Expand Up @@ -75,6 +81,21 @@ pub const SYMLINK: u32 = 16;
pub const READLINK: u32 = 17;
pub const STREAM: u32 = 18;
pub const SYNC: u32 = 19;
/// A second id on the description `fid` names: `value`.
pub const DUP: u32 = 20;
/// Move the description's offset: `flags` is [`SEEK_START`], [`SEEK_CURRENT`]
/// or [`SEEK_END`], `offset` the signed distance from it; `value` is where it
/// landed.
pub const SEEK: u32 = 21;
/// A connection to this server holding the descriptions of the `len` ids in
/// the window, little-endian `u64`s, as ids 1.. in that order: the reply
/// carries its other end. It spends a served slot of this connection's share
/// from here, and names no path.
pub const EXPORT: u32 = 22;

pub const SEEK_START: u64 = 0;
pub const SEEK_CURRENT: u64 = 1;
pub const SEEK_END: u64 = 2;

// Replies, by frame type.
pub const REPLY: u32 = 0x100;
Expand Down Expand Up @@ -220,6 +241,8 @@ errors! {
/// The kernel had no room for what the request needs: a handle, memory,
/// a place in a queue.
OutOfResources = 28,
/// A path on a connection that was given files and no directory.
NoDirectory = 32,
}
local {
/// The server ended, and the connection with it.
Expand Down Expand Up @@ -448,11 +471,17 @@ impl From<Error> for Refused {
}
}

/// One directory capability, connected.
/// Where a [`Dir`]'s connection comes from.
enum Source {
/// A directory capability, connected again through the same connector.
Named { names: &'static Namespace, name: [u8; MAX_SERVICE_NAME], len: usize },
/// A connection another process exported: one connection, never another.
Exported,
}

/// One directory capability, or one exported connection, connected.
pub struct Dir {
names: &'static Namespace,
name: [u8; MAX_SERVICE_NAME],
name_len: usize,
source: Source,
conn: Option<Connection>,
window: SharedMemory,
generation: u64,
Expand All @@ -470,16 +499,23 @@ impl Dir {
}
let mut buf = [0u8; MAX_SERVICE_NAME];
buf[..name.len()].copy_from_slice(name.as_bytes());
let window = SharedMemory::create(WINDOW_BYTES).map_err(|e| Error::kernel("a window", e))?;
let mut dir =
Self { names, name: buf, name_len: name.len(), conn: None, window, generation: 0, writable: false, volume: 0 };
let mut dir = Self::unconnected(Source::Named { names, name: buf, len: name.len() })?;
dir.reconnect()?;
Ok(dir)
}

/// The capability's namespace name.
pub fn name(&self) -> &str {
core::str::from_utf8(&self.name[..self.name_len]).expect("copied from a str")
/// Serve the connection an [`EXPORT`] answered, whose descriptions are ids
/// 1.. in the order exported. It names no path, and is not connected again
/// once it has ended.
pub fn adopt(conn: Connection) -> Result<Self, Error> {
let mut dir = Self::unconnected(Source::Exported)?;
dir.attach(conn)?;
Ok(dir)
}

fn unconnected(source: Source) -> Result<Self, Error> {
let window = SharedMemory::create(WINDOW_BYTES).map_err(|e| Error::kernel("a window", e))?;
Ok(Self { source, conn: None, window, generation: 0, writable: false, volume: 0 })
}

/// Counts this directory's connections; a file id is good only on the one
Expand All @@ -500,14 +536,22 @@ impl Dir {
}

/// Open a fresh connection through the same connector, and lend it the
/// window again. The old one's file ids name nothing from here on.
/// window again. The old one's file ids name nothing from here on; an
/// exported connection has no connector, and stays gone.
fn reconnect(&mut self) -> Result<(), Error> {
self.conn = None;
let conn = match self.names.open(self.name()) {
let Source::Named { names, name, len } = &self.source else { return Err(Error::Gone) };
let name = core::str::from_utf8(&name[..*len]).expect("copied from a str");
let conn = match names.open(name) {
Ok(conn) => conn,
Err(SyscallError::NotFound) => return Err(Error::NotHeld),
Err(e) => return Err(Error::kernel("a connection", e)),
};
self.attach(conn)
}

/// Lend `conn` the window and serve on it.
fn attach(&mut self, conn: Connection) -> Result<(), Error> {
let reply = hello(&conn, &self.window)?;
self.writable = reply.value & RIGHT_WRITE != 0;
self.volume = reply.value2 | SERVED_VOLUME;
Expand Down Expand Up @@ -602,24 +646,65 @@ impl Dir {
let _ = self.fid_call(CLOSE, generation, &Request { fid, ..Request::new() });
}

/// Read at most `out.len()` bytes, and at most the window, at `offset`.
pub fn read(&mut self, fid: u64, generation: u64, offset: u64, out: &mut [u8]) -> Result<usize, Error> {
/// Read at most `out.len()` bytes, and at most the window, at the
/// description's offset, which moves past them.
pub fn read(&mut self, fid: u64, generation: u64, out: &mut [u8]) -> Result<usize, Error> {
let len = out.len().min(WINDOW_BYTES);
let reply = self.fid_call(READ, generation, &Request { fid, offset, len: len as u64, ..Request::new() })?;
let reply = self.fid_call(READ, generation, &Request { fid, len: len as u64, ..Request::new() })?;
let n = counted(reply.value, len)?;
window_take(self.window(), 0, &mut out[..n]);
Ok(n)
}

/// Write at most the window of `data` at `offset`, or at the end for a file
/// opened to append. Answers how much, and the offset after it.
pub fn write(&mut self, fid: u64, generation: u64, offset: u64, data: &[u8]) -> Result<Written, Error> {
/// Write at most the window of `data` at the description's offset, or at
/// the end for a file opened to append; the offset moves past it. Answers
/// how much, and the offset after it.
pub fn write(&mut self, fid: u64, generation: u64, data: &[u8]) -> Result<Written, Error> {
let len = data.len().min(WINDOW_BYTES);
window_put(self.window(), 0, &data[..len]);
let reply = self.fid_call(WRITE, generation, &Request { fid, offset, len: len as u64, ..Request::new() })?;
let reply = self.fid_call(WRITE, generation, &Request { fid, len: len as u64, ..Request::new() })?;
Ok(Written { len: counted(reply.value, len)?, offset: reply.value2 })
}

/// Move the description's offset `delta` from `whence` ([`SEEK_START`],
/// [`SEEK_CURRENT`] or [`SEEK_END`]): where it landed.
pub fn seek(&mut self, fid: u64, generation: u64, whence: u64, delta: i64) -> Result<u64, Error> {
let request = Request { fid, offset: delta as u64, flags: whence, ..Request::new() };
self.fid_call(SEEK, generation, &request).map(|r| r.value)
}

/// A second id on `fid`'s description, sharing its offset.
pub fn dup(&mut self, fid: u64, generation: u64) -> Result<u64, Error> {
self.fid_call(DUP, generation, &Request { fid, ..Request::new() }).map(|r| r.value)
}

/// A connection for another process holding the descriptions `fids` name,
/// as ids 1.. in this order ([`Dir::adopt`]). It spends a served slot of
/// this connection's share for as long as anybody holds it.
pub fn export(&mut self, generation: u64, fids: &[u64]) -> Result<Connection, Error> {
if generation != self.generation || self.conn.is_none() {
return Err(Error::Stale);
}
if fids.is_empty() || fids.len() * 8 > WINDOW_BYTES {
return Err(Error::InvalidRequest);
}
for (i, fid) in fids.iter().enumerate() {
window_put(self.window(), i * 8, &fid.to_le_bytes());
}
let mut batch = [toyos_abi::HANDLE_INVALID; MAX_TRANSFER_HANDLES];
let request = Request { len: fids.len() as u64, ..Request::new() };
let (msg_type, reply, carried) = self.call_with(EXPORT, &request, &mut batch)?;
match (answer(msg_type, reply), carried) {
(Ok(_), 1) => Ok(Connection::received(batch[0])),
(Err(e), 0) => Err(e),
(_, n) => {
close_all(&batch[..n]);
self.conn = None;
Err(Error::Malformed)
}
}
}

pub fn fstat(&mut self, fid: u64, generation: u64) -> Result<Stat, Error> {
let volume = self.volume;
self.fid_call(FSTAT, generation, &Request { fid, ..Request::new() }).map(|r| stat_of(&r, volume))
Expand All @@ -643,15 +728,14 @@ impl Dir {
})
}

/// A pipe whose bytes the server appends to this file from `offset` on,
/// for a program's standard output: the write end.
pub fn stream(&mut self, fid: u64, generation: u64, offset: u64) -> Result<Pipe, Error> {
/// A pipe whose bytes the server writes to this file at the description's
/// offset, which they move, for a program's standard output: the write end.
pub fn stream(&mut self, fid: u64, generation: u64) -> Result<Pipe, Error> {
if generation != self.generation || self.conn.is_none() {
return Err(Error::Stale);
}
let mut batch = [toyos_abi::HANDLE_INVALID; MAX_TRANSFER_HANDLES];
let (msg_type, reply, carried) =
self.call_with(STREAM, &Request { fid, offset, ..Request::new() }, &mut batch)?;
let (msg_type, reply, carried) = self.call_with(STREAM, &Request { fid, ..Request::new() }, &mut batch)?;
match (answer(msg_type, reply), carried) {
(Ok(_), 1) => Ok(Pipe(crate::OwnedHandle(batch[0]))),
(Err(e), 0) => Err(e),
Expand Down Expand Up @@ -957,7 +1041,7 @@ mod tests {
/// refused by name, never read as another answer.
#[test]
fn a_reply_outside_the_protocol_is_malformed() {
let past = Error::WIRE.len() as u64 + 1;
let past = Error::WIRE.iter().filter_map(|e| e.code()).max().expect("wire words") + 1;
// 29 to 31 are what the client's own words once were, and 0x80 on what
// their discriminants are.
for status in [past, 29, 30, 31, 0x80, 0x81, 0x82, u64::MAX, SyscallError::NotFound.to_u64()] {
Expand Down
Loading