Skip to main content

godspeed/
file.rs

1// SPDX-License-Identifier: Apache-2.0
2//! A file as a capability, safely, from a service that serves other clients.
3//!
4//! This module was called `cap` until the standard library grew general capability
5//! operations. It never held any: it holds [`File`] and nothing else, so the name promised
6//! a capability API and delivered a file one - which is why `cap-grant` and `holder` reached
7//! past it into the SDK. The rights constants moved to [`crate::cap`], where `cap::READ`
8//! reads as what it is: a capability right (CLAUDE.md 7.4).
9//!
10//! # What this is
11//!
12//! [`Fs::open`](crate::fs::Fs::open) asks `fs` for a **delegated resource capability** (CLAUDE.md
13//! 7.10) to one file, and returns a [`File`]. The capability is real and kernel-minted: unforgeable,
14//! revocable, and non-escalating. A READ-only [`File`] cannot write, and the refusal comes from the
15//! kernel before `fs` is ever reached.
16//!
17//! ```ignore
18//! let mut fs = gs::fs::Fs::new(ctx);
19//! let mut f = fs.open("/data/log.txt", cap::READ | cap::WRITE)?;
20//! f.write_at(0, b"hello")?;
21//! let mut buf = [0u8; 64];
22//! let n = f.read_at(0, &mut buf)?;
23//! f.close()?;
24//! ```
25//!
26//! # Why this could not be written until the protocol carried a tag
27//!
28//! Using a resource capability is `resource_invoke` - a SEND that embeds a one-shot reply cap. The
29//! kernel routes it to the owning service and then forgets the exchange, so the caller must wait for
30//! the answer on **its own ordinary endpoint**, the same endpoint its clients send to. A task owns
31//! exactly one endpoint; there is no second one to wait on.
32//!
33//! That leaves a caller two bad choices, and `services/shell` takes the second:
34//!
35//! - block on a plain `recv` and read whatever arrives as the reply, or
36//! - drain the endpoint first, destroying any client request that was queued.
37//!
38//! The shell gets away with draining because the shell serves nobody. **A library cannot make that
39//! assumption**, which is why this module did not exist for as long as the file-cap protocol carried
40//! no correlation tag.
41//!
42//! It carries one now, matching the convention the NAMED `fs` protocol in the same service always
43//! had. With a tag, a message that is not the reply is *recognisable*, and a caller that receives one
44//! can **hold it and hand it back** instead of losing it. That is what [`File::take_held`] is for,
45//! and it is the whole reason this is safe to hand to a service.
46//!
47//! **Nothing here is a new kernel facility.** The tag is a protocol byte and the holding is a fixed
48//! array; the standard library is re-serving what the system already does, which is the only thing it
49//! is allowed to do.
50//!
51//! # The obligation this puts on a serving caller
52//!
53//! If your task answers clients on its endpoint, **you must drain [`File::take_held`] after any
54//! operation** and feed those messages back into your own loop. They are real client requests that
55//! arrived while you were waiting. Ignoring them loses them just as surely as draining would have -
56//! the difference is that here you are told, and there you were not.
57//!
58//! A program that serves nobody (most programs) can ignore all of this: nothing will ever be held.
59
60use godspeed_sdk::capability::CapHandle;
61use godspeed_sdk::ipc::Message;
62use godspeed_sdk::service_context::ServiceContext;
63
64use crate::call;
65use crate::cap::{READ, WRITE};
66use crate::error::{from_fs_status, Error};
67use crate::fs::Fs;
68use crate::resource::{self, Held};
69
70// The file-cap operations, as `services/fs` numbers them.
71const FOP_READ: u8 = 1;
72const FOP_WRITE: u8 = 2;
73const FOP_STAT: u8 = 3;
74const FOP_CLOSE: u8 = 4;
75
76/// The most bytes one invocation moves, as `services/fs` frames it.
77pub const IO_CHUNK: usize = crate::fs::IO_CHUNK;
78
79/// How many messages that are NOT our reply a [`File`] will hold before it stops taking them.
80///
81/// See [`File::take_held`]. The bound and the reason for it live in the shared resource-invocation
82/// module, because sockets need exactly the same thing.
83pub const HELD_MAX: usize = resource::HELD_MAX;
84
85/// An open file, held as a capability.
86///
87/// # Why it borrows the [`Fs`] handle
88///
89/// A [`File`] and its `Fs` speak to the same service on the same endpoint, so they must share ONE
90/// correlation tag counter - two counters can mint the same tag for two exchanges in flight, and the
91/// result is not a loud rejection but a stale reply silently accepted as the current answer. Holding
92/// `&mut Fs` makes that structural: the borrow checker will not let the two be used at once, so
93/// there is no way to spell the bug.
94///
95/// The cost is that one `Fs` handle opens one file at a time. To hold two, thread the counter
96/// yourself with [`Fs::from_tag`](crate::fs::Fs::from_tag) and [`Fs::tag`](crate::fs::Fs::tag).
97pub struct File<'f, 'a: 'f> {
98    fs: &'f mut Fs<'a>,
99    ctx: &'a ServiceContext,
100    cap: CapHandle,
101    right: u8,
102    /// Messages that arrived while we were waiting and are NOT ours. Never dropped.
103    held: Held,
104    closed: bool,
105}
106
107impl<'f, 'a: 'f> File<'f, 'a> {
108    pub(crate) fn new(fs: &'f mut Fs<'a>, ctx: &'a ServiceContext, cap: CapHandle, right: u8) -> Self {
109        File { fs, ctx, cap, right, held: Held::new(), closed: false }
110    }
111
112    /// The rights this capability CARRIES - [`cap::READ`](crate::cap::READ) and
113    /// [`cap::WRITE`](crate::cap::WRITE) - as the kernel reported them when the file was opened.
114    ///
115    /// Not necessarily what you asked [`Fs::open`](crate::fs::Fs::open) for: `fs` narrows a sealed
116    /// file to read-only rather than mint a cap it cannot honour (7.3 - rights narrow), and
117    /// [`cap::APPEND`](crate::cap::APPEND) is a request to `fs`, never a kernel right, so it is not
118    /// here. (Until 2026-10-10 this was the mask asked for, and a close under it was refused -
119    /// `backlog/fixed/80` G2.)
120    pub fn rights(&self) -> u8 {
121        self.right
122    }
123
124    /// Take one message that arrived during an operation and was NOT the reply.
125    ///
126    /// Call this in a loop until it returns `None` after every operation, if your task serves
127    /// clients. These are real requests from them; the library held them rather than dropping them,
128    /// but only you can answer them.
129    ///
130    /// Returns `None` for a task that serves nobody, always.
131    pub fn take_held(&mut self) -> Option<Message> {
132        self.held.take()
133    }
134
135    /// Read from the file through the capability.
136    ///
137    /// Returns how many bytes landed in `buf`, which may be fewer than asked for at end of file.
138    /// One invocation moves at most [`IO_CHUNK`] bytes; call again with a later offset for more.
139    ///
140    /// **Blocks** up to [`call::DEFAULT_SECS`]. **Authority:** this capability's `READ` right, which
141    /// the KERNEL checks before `fs` is reached.
142    ///
143    /// # Errors
144    /// - [`Error::PermissionDenied`] - this capability does not carry `READ`.
145    /// - [`Error::Revoked`] - the file was deleted or closed, or `fs` restarted: the capability is
146    ///   finished, and the answer is to re-open rather than to retry.
147    /// - A read changes nothing, so every no-answer error here is safe to retry.
148    pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, Error> {
149        let want = buf.len().min(IO_CHUNK);
150        let mut req = [0u8; 13];
151        req[0] = FOP_READ;
152        req[1..9].copy_from_slice(&offset.to_le_bytes());
153        req[9..13].copy_from_slice(&(want as u32).to_le_bytes());
154        let reply = self.invoke(READ, &req)?;
155        let b = reply.payload_bytes();
156        // `[tag, status, n:u32, bytes..]`. `invoke` VERIFIES the tag and leaves it in place - it does
157        // not strip it, because stripping means rebuilding a 4 KiB `Message` on the stack for every
158        // read. So the body starts at 2, not at 1.
159        if b.len() < 6 {
160            return Err(Error::Malformed);
161        }
162        let n = u32::from_le_bytes([b[2], b[3], b[4], b[5]]) as usize;
163        if n > want || 6 + n > b.len() {
164            return Err(Error::Malformed);
165        }
166        buf[..n].copy_from_slice(&b[6..6 + n]);
167        Ok(n)
168    }
169
170    /// Write to the file through the capability.
171    ///
172    /// **Blocks** up to [`call::DEFAULT_SECS`]. **Authority:** this capability's `WRITE` right.
173    ///
174    /// # Errors
175    /// - [`Error::PermissionDenied`] - no `WRITE`, or an `APPEND`-only capability was asked to write
176    ///   below the furthest offset it has already written.
177    /// - [`Error::Failed`] - `offset` is not a multiple of 508, or the write runs past the file's
178    ///   extent; as for [`Fs::write_at`](crate::fs::Fs::write_at), `fs` does not grow a file here.
179    /// - [`Error::InvalidInput`] - more than [`IO_CHUNK`] bytes in one call. **Nothing is written**;
180    ///   split it rather than assuming a partial write happened.
181    /// - **A write MUTATES.** Do not retry on [`Error::OutcomeUnknown`] without first reading back
182    ///   what is actually there - see [`Error::retry_is_safe`].
183    pub fn write_at(&mut self, offset: u64, data: &[u8]) -> Result<(), Error> {
184        if data.len() > IO_CHUNK {
185            return Err(Error::InvalidInput);
186        }
187        let mut req = [0u8; 9 + IO_CHUNK];
188        req[0] = FOP_WRITE;
189        req[1..9].copy_from_slice(&offset.to_le_bytes());
190        req[9..9 + data.len()].copy_from_slice(data);
191        self.invoke(WRITE, &req[..9 + data.len()])?;
192        Ok(())
193    }
194
195    /// The file's current size in bytes.
196    ///
197    /// **Blocks** up to [`call::DEFAULT_SECS`]. **Authority:** this capability's `READ` right.
198    pub fn size(&mut self) -> Result<u64, Error> {
199        let reply = self.invoke(READ, &[FOP_STAT])?;
200        let b = reply.payload_bytes();
201        // `[tag, status, size:u64]` - the body starts at 2, as in `read_at`.
202        if b.len() < 10 {
203            return Err(Error::Malformed);
204        }
205        Ok(u64::from_le_bytes([b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9]]))
206    }
207
208    /// Close the file, revoking this capability and every copy of it.
209    ///
210    /// Consumes the handle, and returns what the service said. Dropping a `File` without calling
211    /// this also closes it, but a `Drop` cannot report a failure - so close explicitly wherever the
212    /// outcome matters.
213    ///
214    /// The close is invoked under a right the capability carries ([`rights`](File::rights)), so a
215    /// read-only, a narrowed or an [`APPEND`](crate::cap::APPEND)-only file closes like any other.
216    pub fn close(mut self) -> Result<(), Error> {
217        self.close_inner()
218    }
219
220    fn close_inner(&mut self) -> Result<(), Error> {
221        if self.closed {
222            return Ok(());
223        }
224        self.closed = true;
225        // CLOSE is permitted to any holder, so it is invoked under ONE right the capability holds
226        // (READ if it has it) - a read-only holder must still be able to let go, and the kernel
227        // checks the right asked for against the capability.
228        let right = if self.right & READ != 0 { READ } else { self.right };
229        let r = self.invoke(right, &[FOP_CLOSE]).map(|_| ());
230        self.ctx.remove_cap(self.cap);
231        r
232    }
233
234    /// One invocation, through the shared resource-capability path, then the fs status byte.
235    ///
236    /// The generic half - reply-cap lifetime, holding a message that is not ours, reading the
237    /// kernel's refusal - is `crate::resource`. What is specific to a file is the status byte at
238    /// index 1 of the reply, which this maps to an [`Error`].
239    fn invoke(&mut self, right: u8, body: &[u8]) -> Result<Message, Error> {
240        let tag = self.fs.next_tag_pub();
241        // NO PATIENCE BYTE. `fs` answers a file-capability invocation from its own serve loop and
242        // never puts one aside, so the byte would be carried and never read - and `serve_filecap`
243        // reads the operation at index 1 of what it receives.
244        let m = resource::invoke(self.ctx, self.cap, right, tag, None, body, call::DEFAULT_SECS,
245                                 &mut self.held)?;
246        // `[tag, status, ..]`. The tag is verified and LEFT IN PLACE, so a body starts at index 2.
247        let status = *m.payload_bytes().get(1).ok_or(Error::Malformed)?;
248        from_fs_status(status)?;
249        Ok(m)
250    }
251}
252
253impl<'f, 'a: 'f> Drop for File<'f, 'a> {
254    /// Closes the file if [`close`](File::close) was not called.
255    ///
256    /// `fs` keeps a FIXED table of open resources, so leaking one is not merely untidy - enough
257    /// leaks and nothing can be opened at all. That is why this closes rather than merely warning.
258    ///
259    /// A `Drop` cannot return a `Result`, so a close that FAILS here is invisible. That is the
260    /// reason [`close`](File::close) exists and is worth calling wherever the outcome matters.
261    fn drop(&mut self) {
262        let _ = self.close_inner();
263    }
264}