godspeed/fs.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Files, without the byte layout.
3//!
4//! # What this replaces
5//!
6//! Reading a file today means knowing the wire protocol. From `services/shell`, abbreviated:
7//!
8//! ```text
9//! let stat = fs_request_q(ctx, OP_STAT_FILE, path, &[]); // three outcome arms
10//! let sp = stat.payload_bytes();
11//! let exists = sp.first() == Some(&FS_OK) && sp.len() >= 11 && sp[1] == 1;
12//! let is_dir = exists && sp[10] == 1;
13//! let size = u64::from_le_bytes([sp[2], sp[3], sp[4], sp[5], sp[6], sp[7], sp[8], sp[9]]);
14//! // then loop read_at in IO_CHUNK pieces, checking errors at each step
15//! ```
16//!
17//! Roughly thirty lines, every one an opportunity to index the wrong byte. `FS_OK` was declared
18//! independently in four crates, and the shell re-declared 21 of the filesystem's 27 opcodes. A
19//! guessed opcode in one of them (`1` where the protocol says `10`) made a command write nothing
20//! for twelve seconds and report that it was done.
21//!
22//! **The layout is owned here, once.** It is a fact `services/fs` defines; every other crate was
23//! re-deriving it from a comment.
24//!
25//! # There is no `read_to_string`
26//!
27//! GodspeedOS has no heap, deliberately (CLAUDE.md ยง26.6.1: fixed stack arrays and bounded arenas,
28//! so the maximum a subsystem can use is readable from its source). A function returning an
29//! allocated `String` cannot exist, and adding an allocator to make this library look like Rust's
30//! would be trading the architecture for a familiar shape.
31//!
32//! So the caller owns the buffer:
33//!
34//! ```ignore
35//! let mut buf = [0u8; 4096];
36//! let n = fs::read_into(&fs, "/data/hello.txt", &mut buf)?;
37//! io::println(ctx, core::str::from_utf8(&buf[..n]).unwrap_or("<not utf-8>"));
38//! ```
39//!
40//! Six lines against thirty, no byte indices, and the bound is visible in the source.
41
42use godspeed_sdk::ipc::Message;
43use godspeed_sdk::service_context::ServiceContext;
44
45use crate::call;
46use crate::error::{from_fs_status, Error};
47
48/// How long to wait for a whole-volume sweep ([`Fs::check`] or [`Fs::scrub`]).
49///
50/// **Far longer than an ordinary call, and that is the point.** These walk every referenced block on
51/// the volume; a chunk write and a full scrub are each one request and one reply, and only one of
52/// them can be expected back in seconds. A deadline shorter than the operation turns a slow success
53/// into [`Error::OutcomeUnknown`], which is worse than waiting because it forbids the retry that
54/// would have fixed it.
55pub const SWEEP_SECS: i64 = 120;
56
57/// What [`Fs::check`] repaired, and what it found.
58#[derive(Copy, Clone, PartialEq, Eq, Debug)]
59pub struct Check {
60 /// Files reachable from the root.
61 pub files: u32,
62 /// Directories reachable from the root.
63 pub dirs: u32,
64 /// Entries whose CRC did not verify. **Not repaired** - reported, so somebody decides.
65 pub bad: u32,
66 /// Blocks the rebuilt bitmap says are in use.
67 pub used: u64,
68 /// Blocks now free.
69 pub free: u64,
70 /// What the superblock claimed was free BEFORE the rebuild. Compare it with `free`: equal means
71 /// nothing needed repairing, and different means the count had drifted and now has not.
72 pub free_before: u64,
73}
74
75/// What [`Fs::scrub`] found. Read-only: it changes nothing on disk.
76#[derive(Copy, Clone, PartialEq, Eq, Debug)]
77pub struct Scrub {
78 /// Files reachable from the root.
79 pub files: u32,
80 /// Directories reachable from the root.
81 pub dirs: u32,
82 /// Entries whose CRC did not verify.
83 pub bad: u32,
84 /// Blocks read and verified.
85 pub scanned: u64,
86}
87
88/// What [`Fs::drive`] reports about the storage `fs` serves.
89#[derive(Copy, Clone, PartialEq, Eq, Debug)]
90pub struct Drive {
91 /// The device's size in 512-byte sectors, as `fs` re-derives it from the block driver on each ask.
92 /// ZERO means there is no storage device at all - not a device of size zero.
93 pub capacity_sectors: u64,
94 /// Whether a GSFS volume is mounted on it.
95 pub mounted: bool,
96 /// Blocks in the volume, and blocks in use (zero when nothing is mounted).
97 pub total_blocks: u64,
98 pub used_blocks: u64,
99 label: [u8; 31],
100 label_len: u8,
101}
102
103impl Drive {
104 /// The volume's label, as bytes; empty when it has none or nothing is mounted.
105 pub fn label(&self) -> &[u8] {
106 &self.label[..self.label_len as usize]
107 }
108}
109
110/// The longest path `services/fs` accepts. A longer one is [`Error::InvalidInput`] and is never
111/// sent, rather than being silently truncated into a request for a DIFFERENT file.
112///
113/// # Paths are BYTES
114///
115/// Every path parameter here takes `impl AsRef<[u8]>`, so a string literal works as it reads -
116/// `fs.read_into("/data/message.txt", &mut buf)` - and so does a byte slice a program is holding
117/// from somewhere else.
118///
119/// That is not generality for its own sake. `services/fs` accepts any byte above 0x1f except `/`
120/// and 0x7f in a name, so a disk prepared elsewhere holds names that are not UTF-8, and this OS must
121/// still be able to list and DELETE them. A `&str`-only API forces such a caller through
122/// `from_utf8(..).unwrap_or("")`, which turns an unreadable name into a request for a different
123/// file - silently. [`DirEntry::name`] hands back raw bytes for the same reason; these parameters
124/// accept them for the other half of the round trip.
125pub const PATH_MAX: usize = 120;
126
127/// The most file content one request can carry, as `services/fs` frames it (7 * 508). Reads and
128/// writes larger than this are split by [`read_into`](Fs::read_into) and [`write`]; it is public because a caller
129/// sizing its own buffer benefits from knowing the natural stride.
130pub const IO_CHUNK: usize = 7 * 508;
131
132// The opcodes, owned here. These are `services/fs`'s numbers and must not be guessed.
133const OP_WRITE_FILE: u8 = 10;
134const OP_LIST_DIR: u8 = 14;
135const OP_MOVE: u8 = 17;
136const OP_MKDIR_P: u8 = 18;
137const OP_DELETE_TREE: u8 = 19;
138const OP_OPEN: u8 = 30;
139const OP_CHECK: u8 = 27;
140const OP_SEAL: u8 = 31;
141const OP_QUIESCE: u8 = 32;
142const OP_SCRUB: u8 = 29;
143const OP_STAT_FILE: u8 = 12;
144const OP_MKDIR: u8 = 13;
145const OP_DELETE: u8 = 16;
146const OP_RENAME: u8 = 15;
147const OP_WRITE_NEW: u8 = 24;
148const OP_WRITE_AT: u8 = 25;
149const OP_READ_AT: u8 = 26;
150const OP_DRIVES_INFO: u8 = 20;
151
152/// The tag a fresh handle starts from. Any non-zero value would do; what matters is that
153/// consecutive requests on one channel differ.
154///
155/// The tag is the first byte of every request and of its reply, so a late answer to an EARLIER
156/// request is recognised instead of being read as the answer to this one. This is not decoration:
157/// without it, running one command twice could leave the filesystem protocol "out of step" - a
158/// reply arriving after its deadline matched to the next request, and every exchange after it
159/// answering the question before.
160///
161/// **There is deliberately no separate tag RANGE for this library.** An earlier draft minted tags
162/// from a 0xC0..0xFF band, which reads as tidy and is actively harmful: `services/shell` mints
163/// from the whole 1..=255 range, so the two overlapped, and a collision does not fail loudly - it
164/// lets a stale reply be ACCEPTED as the current request's answer. Two counters on one channel is
165/// the bug the tag exists to prevent. Use [`Fs::from_tag`] to share the one counter instead.
166const TAG_START: u8 = 1;
167
168/// The most of a service-supplied failure reason [`Fs::reason`] keeps. Bounded on purpose: the
169/// handle is a stack value, so its size must be readable from this line (26.6.1).
170pub const REASON_MAX: usize = 64;
171
172/// The most pages [`Fs::list_dir`] will request before stopping and saying so.
173///
174/// The same bound `services/shell` uses. A walk has to terminate on its own rather than on the
175/// service being well-behaved (26.6), and 512 pages is far past any directory this filesystem
176/// holds while still being a number rather than "however many it takes".
177pub const DIR_PAGE_MAX: u16 = 512;
178
179/// What [`Fs::list_dir`] did.
180///
181/// `visited` cannot be read without seeing `complete`, and that is the whole point of it being a
182/// struct: a partial listing that reads as a full one is the failure this type exists to prevent.
183#[derive(Copy, Clone, PartialEq, Eq, Debug)]
184pub struct Listing {
185 /// How many entries the closure was shown.
186 pub visited: usize,
187 /// `true` if the directory ended. `false` means the walk stopped first - the closure returned
188 /// `false`, or [`DIR_PAGE_MAX`] was reached - and there are MORE ENTRIES THAN YOU SAW. Say so
189 /// in whatever you report; do not present the count as the size of the directory.
190 pub complete: bool,
191}
192
193/// One entry from [`Fs::list_dir`].
194///
195/// `name` is BYTES, not `&str`, and that is not laziness. `services/fs` accepts any byte above
196/// 0x1f except `/` and 0x7f in a name, so a name is not guaranteed to be UTF-8 and a type that
197/// promised otherwise would be lying about the filesystem it reads. [`DirEntry::name_str`] is the
198/// convenience for the ordinary case.
199#[derive(Copy, Clone, PartialEq, Eq, Debug)]
200pub struct DirEntry<'n> {
201 /// The entry's name within its directory. Never contains a `/`.
202 pub name: &'n [u8],
203 /// `true` if this entry is itself a directory.
204 pub is_dir: bool,
205 /// Size in bytes. Zero for a directory.
206 pub size: u64,
207 /// Last-modified time as `services/fs` records it.
208 pub mtime: u32,
209 /// `true` if the file is sealed (append-only; writes to it are refused).
210 pub sealed: bool,
211}
212
213impl<'n> DirEntry<'n> {
214 /// The name as text, or `None` if it is not UTF-8.
215 ///
216 /// The honest call site is `e.name_str().unwrap_or("<not utf-8>")` - print something rather
217 /// than skipping a file that really exists.
218 pub fn name_str(&self) -> Option<&'n str> {
219 core::str::from_utf8(self.name).ok()
220 }
221}
222
223/// A handle to the filesystem service.
224///
225/// # Authority
226///
227/// Constructed from a `&ServiceContext`, and carries no authority of its own. Holding one does not
228/// grant filesystem access: every call goes through the context's existing send capability for
229/// `fs`, and a task whose spawn request granted none gets [`Error::Unreachable`], exactly as it
230/// would for any other peer it cannot reach. **This handle makes the filesystem convenient to use,
231/// never available where it was not granted.**
232///
233/// # Why it owns a counter
234///
235/// The request tag has to differ between consecutive requests, and it may not live in a `static`:
236/// unowned global mutable state is forbidden (invariant 9), and the shell's own tag counter was
237/// moved out of a `static AtomicU8` for exactly that reason (audit finding C6-1). So the counter
238/// lives here, owned by the handle, which is also why this type takes `&mut self` on operations
239/// that send.
240///
241/// # A missing path is not always [`Error::NotFound`]
242///
243/// `services/fs` answers `FS_NOTFOUND` only from its READS - stat, read and list. Every operation
244/// that changes something (write, create, delete, rename, move, seal, open) reports an absent path
245/// as `FS_ERR` with a reason such as "not found", "path not found" or "source not found", which this
246/// library returns as [`Error::Failed`] with that text in [`reason`](Fs::reason) - except
247/// [`open`](Fs::open), whose `FS_ERR` carries no reason at all. Ask
248/// [`exists`](Fs::exists) first where the difference matters.
249pub struct Fs<'a> {
250 ctx: &'a ServiceContext,
251 tag: u8,
252 reason: [u8; REASON_MAX],
253 reason_len: u8,
254 notice: Option<&'a dyn Fn()>,
255 /// Where the keys a noticing wait reads (and does not act on) go - see [`Fs::keeping`].
256 keep: Option<&'a dyn Fn(u8)>,
257 /// The command's watch (`call::Watch`): when lent and active, every request shows the command's
258 /// `[q] quit` and honours it, and the notice and keep above are not used.
259 watch: Option<&'a dyn call::Watch>,
260 /// How long ORDINARY operations wait. `None` means [`call::DEFAULT_SECS`].
261 ///
262 /// The operations with a budget of their own - a subtree delete walks a tree, so it carries
263 /// `SWEEP_SECS` - keep it. Overriding those would let a caller set a deadline shorter than the
264 /// work it is waiting for, which is the bug this whole module keeps finding elsewhere.
265 patience: Option<i64>,
266}
267
268/// What [`stat`](Fs::stat) found.
269#[derive(Copy, Clone, PartialEq, Eq, Debug)]
270pub struct Stat {
271 /// Size in bytes. Zero for a directory.
272 pub size: u64,
273 /// `true` if this path is a directory rather than a file.
274 pub is_dir: bool,
275}
276
277impl<'a> Fs<'a> {
278 /// Take a filesystem handle from a context.
279 ///
280 /// Cheap, allocates nothing, and grants nothing. See the type's authority note.
281 pub fn new(ctx: &'a ServiceContext) -> Fs<'a> {
282 Fs { ctx, tag: TAG_START, reason: [0; REASON_MAX], reason_len: 0, notice: None, keep: None, watch: None, patience: None }
283 }
284
285 /// A handle that calls `notice` when a request has been waiting a while.
286 ///
287 /// The callback takes nothing and returns nothing, and that is the whole of its contract: it
288 /// means only **"you have been waiting"**. It cannot cancel, cannot inspect the request, and
289 /// the library learns nothing about consoles from it - the shell prints `[q] quit` there, and
290 /// something headless prints nothing at all.
291 ///
292 /// **Use this for anything a person is waiting at.** A filesystem call can block on a device
293 /// that is settling after a replug, and a command that goes quiet for twenty seconds with no
294 /// way out is one the operator has to reboot out of (`utilities/0_conventions.md` rule 9). A
295 /// request the operator then aborts returns [`Error::Cancelled`], which is NOT a fault - report
296 /// it as the deliberate act it was.
297 pub fn with_notice(ctx: &'a ServiceContext, notice: &'a dyn Fn()) -> Fs<'a> {
298 Fs { ctx, tag: TAG_START, reason: [0; REASON_MAX], reason_len: 0, notice: Some(notice), keep: None, watch: None,
299 patience: None }
300 }
301
302 /// Wait `secs` for an ordinary operation instead of [`call::DEFAULT_SECS`].
303 ///
304 /// **This is the CALLER's patience, not a promise about the service.** Use it where waiting the
305 /// default would be worse than giving up: a shell loading its history at startup must not sit on
306 /// a prompt because `fs` respawned and is still re-mounting, and a background marker write is
307 /// not worth a long wait at all.
308 ///
309 /// **A short deadline does not make a failure smaller.** Giving up on a WRITE leaves
310 /// [`Error::OutcomeUnknown`], and shortening the wait makes that outcome MORE likely, not less -
311 /// so a caller that sets this must have something sensible to do with "it may have happened".
312 /// The right use is a write nobody is waiting on; the wrong one is shortening a write somebody
313 /// will act on the result of.
314 ///
315 /// Operations that carry their own budget because they do more work - [`delete_all`](Fs::delete_all)
316 /// walks a subtree - are NOT shortened by this. A caller cannot set a deadline under the work.
317 pub fn patience_secs(mut self, secs: i64) -> Fs<'a> {
318 self.patience = if secs > 0 { Some(secs) } else { None };
319 self
320 }
321
322 /// Take a handle that CONTINUES an existing tag sequence for this channel.
323 ///
324 /// For a caller that still makes some `fs` requests by hand: lend the handle your counter, and
325 /// take it back with [`tag`](Fs::tag) when you are done.
326 ///
327 /// ```ignore
328 /// let mut fs = Fs::from_tag(ctx, my_fs_tag.get());
329 /// let r = fs.list_dir("/data", |e| { .. })?;
330 /// my_fs_tag.set(fs.tag()); // hand-rolled calls resume where the handle left off
331 /// ```
332 ///
333 /// **Use this whenever anything else in the same task also talks to `fs` directly.** Two
334 /// counters on one channel can mint the same tag for two in-flight requests, and the result is
335 /// not a loud rejection - it is a stale reply silently accepted as the answer to the current
336 /// question. See [`TAG_START`](self).
337 pub fn from_tag(ctx: &'a ServiceContext, tag: u8) -> Fs<'a> {
338 Fs { ctx, tag, reason: [0; REASON_MAX], reason_len: 0, notice: None, keep: None, watch: None, patience: None }
339 }
340
341 /// Lend this handle a waiting-notice, as [`with_notice`](Fs::with_notice) describes.
342 ///
343 /// Separate from [`from_tag`](Fs::from_tag) rather than a fourth constructor taking both,
344 /// because a caller that shares a tag counter usually also wants the notice and the
345 /// combinations multiply faster than the constructors are worth.
346 pub fn noticing(mut self, notice: &'a dyn Fn()) -> Fs<'a> {
347 self.notice = Some(notice);
348 self
349 }
350
351 /// Hand every key a noticing wait reads, other than a quit key, to `keep` - in order - instead of
352 /// discarding it. A wait that watches the console for `q` takes what is typed; without this,
353 /// typing ahead while a command waits loses those keys (`backlog/fixed/80` H16). Only matters with a
354 /// notice, since only a noticing wait reads the console.
355 pub fn keeping(mut self, keep: &'a dyn Fn(u8)) -> Fs<'a> {
356 self.keep = Some(keep);
357 self
358 }
359
360 /// Lend this handle the command's watch ([`call::Watch`]): every request then shows the command's
361 /// `[q] quit` once the COMMAND has run 2 s, and stops at the next request after a quit. Takes
362 /// precedence over a notice; a watch that is not active leaves the handle's own notice, if any, else the
363 /// plain wait.
364 pub fn watching(mut self, watch: &'a dyn call::Watch) -> Fs<'a> {
365 self.watch = Some(watch);
366 self
367 }
368
369 /// The tag this handle last used, to hand back to whatever owns the channel's counter.
370 pub fn tag(&self) -> u8 {
371 self.tag
372 }
373
374 /// Why the last call failed, IN THE SERVICE'S OWN WORDS, or `""` if it did not fail or said
375 /// nothing.
376 ///
377 /// **This is for reporting, never for control flow.** Branch on the [`Error`]; print this.
378 /// It is free-text owned by `services/fs` and may be reworded at any time, so a caller that
379 /// matched on its content would break silently the next time somebody improved a message.
380 ///
381 /// It exists because the service already sends it and this library used to discard it:
382 /// `FS_ERR` became [`Error::Failed`] and "name already exists" was lost between the two. An
383 /// operator reading `fs: failed` has to go to the service log for a sentence the reply was
384 /// already carrying, which makes a loud failure quieter for no reason (26.7).
385 pub fn reason(&self) -> &str {
386 core::str::from_utf8(&self.reason[..self.reason_len as usize]).unwrap_or("")
387 }
388
389 /// The next tag, for the `cap` module - which speaks to the SAME service on the SAME endpoint
390 /// and therefore must draw from THIS counter rather than start a second one.
391 ///
392 /// `pub(crate)` rather than public: sharing the counter is an internal invariant of this
393 /// library, not something a caller should be able to interleave with by hand.
394 pub(crate) fn next_tag_pub(&mut self) -> u8 {
395 self.next_tag()
396 }
397
398 /// The deadline an ordinary operation waits: the caller's, or the library's default.
399 fn secs(&self) -> i64 {
400 self.patience.unwrap_or(call::DEFAULT_SECS)
401 }
402
403 /// The next request tag: wrapping +1, never 0.
404 ///
405 /// Identical to the rule `services/shell` uses, so one counter can be shared across both (see
406 /// [`from_tag`](Fs::from_tag)). Zero is skipped because a zero tag can only have come from a
407 /// sender that does not tag at all, and that must stay distinguishable.
408 fn next_tag(&mut self) -> u8 {
409 let t = self.tag.wrapping_add(1);
410 self.tag = if t == 0 { 1 } else { t };
411 self.tag
412 }
413
414 /// Build `[tag, op, path_len, path.., tail..]` and send it.
415 ///
416 /// Returns the reply body with the tag and status already checked and stripped, so callers work
417 /// in terms of the operation's own answer rather than in terms of offsets.
418 fn call(&mut self, op: u8, path: &[u8], tail: &[u8], secs: i64) -> Result<Reply, Error> {
419 if path.len() > PATH_MAX {
420 return Err(Error::InvalidInput);
421 }
422 let mut req = [0u8; 3 + PATH_MAX + 8 + IO_CHUNK];
423 let n = 3 + path.len() + tail.len();
424 if n > req.len() {
425 // Could not be built, so it was never sent: a failure, not a slow answer.
426 return Err(Error::InvalidInput);
427 }
428 let tag = self.next_tag();
429 req[0] = tag;
430 req[1] = op;
431 req[2] = path.len() as u8;
432 req[3..3 + path.len()].copy_from_slice(path);
433 req[3 + path.len()..n].copy_from_slice(tail);
434
435 let msg = Message::from_bytes(&req[..n]);
436 let reply = match self.watch {
437 // Only the reply carrying THIS request's tag: a late answer to a request already quit is dropped.
438 Some(w) if w.active() => call::request_watched_sifted(self.ctx, "fs", &msg, secs, w,
439 &mut |m| m.payload_bytes().first() == Some(&tag))?,
440 _ => call::request_within_notice(self.ctx, "fs", &msg, secs, self.notice, self.keep)?,
441 };
442 let body = reply.payload_bytes();
443
444 // The tag must match, or this is an answer to a question we already gave up on.
445 if body.first() != Some(&tag) {
446 return Err(Error::Malformed);
447 }
448 let status = *body.get(1).ok_or(Error::Malformed)?;
449 // Take the service's words BEFORE `from_fs_status` collapses the byte to an `Error`. Only
450 // FS_ERR carries a reason; every other status has one meaning and needs no sentence.
451 self.reason_len = 0;
452 if status == 1 {
453 let why = if body.len() > 2 { &body[2..] } else { &[][..] };
454 let n = why.len().min(REASON_MAX);
455 self.reason[..n].copy_from_slice(&why[..n]);
456 self.reason_len = n as u8;
457 }
458 from_fs_status(status)?;
459 Ok(Reply { msg: reply })
460 }
461
462
463 /// Visit every entry of a directory, in the order `services/fs` stores them.
464 ///
465 /// `f` is called once per entry and returns `true` to continue or `false` to stop early.
466 ///
467 /// ```ignore
468 /// let mut files = 0;
469 /// let r = fs.list_dir("/data", |e| { if !e.is_dir { files += 1; } true })?;
470 /// if !r.complete { io::println(ctx, " (listing truncated - there are more)"); }
471 /// ```
472 ///
473 /// # This is where a hand-rolled version goes wrong
474 ///
475 /// The reply is ONE PAGE: `[count, more, next]` plus the entries that fit in a block. A caller
476 /// that reads `count` entries and stops has silently listed a PREFIX of a large directory, and
477 /// nothing anywhere reports a problem. This follows `next` until `more` is clear, so the
478 /// closure sees the whole directory or the call returns an error - never a quiet partial.
479 ///
480 /// **Blocks**, once per page. **Authority:** the caller's existing `fs` capability.
481 ///
482 /// # Errors
483 /// - [`Error::NotFound`] - no such path, or it is a file rather than a directory. Returned
484 /// only for the FIRST page: a later page that fails ends the walk with `complete: false`,
485 /// because entries were already delivered and reporting "no such directory" for a read error
486 /// part way through would name the wrong fault.
487 /// - [`Error::Malformed`] - the service claimed more entries without advancing. Listing is
488 /// read-only, so every no-answer error here is safe to retry.
489 pub fn list_dir<F>(&mut self, path: impl AsRef<[u8]>, mut f: F) -> Result<Listing, Error>
490 where
491 F: FnMut(DirEntry<'_>) -> bool,
492 {
493 const HDR: usize = 6; // [count, more, next:u32] - the entries follow
494 let mut from = 0u32;
495 let mut total = 0usize;
496 let mut pages = 0u16;
497 loop {
498 let r = match self.call(OP_LIST_DIR, path.as_ref(), &from.to_le_bytes(), self.secs()) {
499 Ok(r) => r,
500 // The FIRST page failing means the path is not a readable directory, and the caller
501 // needs that error. A LATER one failing is a read error inside a walk that already
502 // produced real entries - so the honest answer is the entries plus "not complete",
503 // not an error that describes the directory rather than what happened.
504 Err(e) if pages == 0 => return Err(e),
505 Err(_) => return Ok(Listing { visited: total, complete: false }),
506 };
507 pages += 1;
508 let b = r.body();
509 if b.len() < HDR {
510 return Err(Error::Malformed);
511 }
512 let count = b[0] as usize;
513 let more = b[1] == 1;
514 let next = u32::from_le_bytes([b[2], b[3], b[4], b[5]]);
515
516 let mut w = HDR;
517 for _ in 0..count {
518 // Every field is bounds-checked against the REPLY, not against what the header
519 // promised. A truncated or malformed page must not be read past its end.
520 if w >= b.len() {
521 return Err(Error::Malformed);
522 }
523 let nl = b[w] as usize;
524 if w + 15 + nl > b.len() {
525 return Err(Error::Malformed);
526 }
527 let name = &b[w + 1..w + 1 + nl];
528 let size = u64::from_le_bytes([
529 b[w + 2 + nl], b[w + 3 + nl], b[w + 4 + nl], b[w + 5 + nl],
530 b[w + 6 + nl], b[w + 7 + nl], b[w + 8 + nl], b[w + 9 + nl],
531 ]);
532 let mtime = u32::from_le_bytes([
533 b[w + 10 + nl], b[w + 11 + nl], b[w + 12 + nl], b[w + 13 + nl],
534 ]);
535 total += 1;
536 let go_on = f(DirEntry {
537 name,
538 is_dir: b[w + 1 + nl] == 1,
539 size,
540 mtime,
541 sealed: b[w + 14 + nl] & 1 != 0,
542 });
543 if !go_on {
544 // The caller stopped us, so the directory did not end: NOT complete.
545 return Ok(Listing { visited: total, complete: false });
546 }
547 w += 15 + nl;
548 }
549
550 if !more {
551 return Ok(Listing { visited: total, complete: true });
552 }
553 // THE WALK TERMINATES ON ITS OWN. Following `more` for as long as the service offers it
554 // is an unbounded loop wearing a condition; the cap is what makes this bounded (26.6),
555 // and `complete: false` is what stops the cap from lying about what was read.
556 if pages >= DIR_PAGE_MAX {
557 return Ok(Listing { visited: total, complete: false });
558 }
559 // NO PROGRESS IS A LOUD ERROR, NOT A LOOP. `next` names the entry that did not fit, so
560 // it must advance past where this page started. A service that answers "more" without
561 // moving would otherwise spin here forever, asking the same question (26.6).
562 if next <= from {
563 return Err(Error::Malformed);
564 }
565 from = next;
566 }
567 }
568
569 /// Create a directory and any missing parents, like `mkdir -p`.
570 ///
571 /// Succeeds if the directory already exists, which is what makes it usable at start-up: a
572 /// service that ensures its own data directory should not have to care whether it is the first
573 /// to run. Use [`create_dir`](Fs::create_dir) where the path's absence is itself the thing being asserted.
574 ///
575 /// **Blocks** up to [`call::DEFAULT_SECS`], or this handle's
576 /// [`patience_secs`](Fs::patience_secs) if it was given one. **Authority:** the caller's existing `fs` capability.
577 ///
578 /// # Errors
579 /// [`Error::Failed`] with [`reason`](Fs::reason) naming the step that failed (a parent that is
580 /// a file, say). **A no-answer error must NOT be blindly retried** - see [`Error::retry_is_safe`];
581 /// creating directories is idempotent, but the transaction may have committed unseen, so
582 /// re-issuing is only safe once you accept that it is a second attempt rather than the first.
583 pub fn create_dir_all(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error> {
584 self.call(OP_MKDIR_P, path.as_ref(), &[], self.secs())?;
585 Ok(())
586 }
587
588 /// Move a file or directory to `dest`, ACROSS directories.
589 ///
590 /// This is the different operation from [`rename`](Fs::rename), which changes a name within one
591 /// parent. `dest` is a full path, and `services/fs` refuses a move that would put a directory
592 /// inside itself - the tree stays a tree, and that rule is the service's to enforce, not the
593 /// caller's to remember.
594 ///
595 /// **Blocks** up to [`call::DEFAULT_SECS`], or this handle's
596 /// [`patience_secs`](Fs::patience_secs) if it was given one. **Authority:** the caller's existing `fs` capability.
597 ///
598 /// # Errors
599 /// [`Error::Failed`] with [`reason`](Fs::reason) - "source not found" for an absent source
600 /// (NOT [`Error::NotFound`]; see the type's note), or the refusal ("name already exists",
601 /// "cannot move root", a destination inside the source). A move MUTATES - do not retry on a no-answer error without
602 /// first checking what happened.
603 pub fn move_to(&mut self, path: impl AsRef<[u8]>, dest: impl AsRef<[u8]>) -> Result<(), Error> {
604 if dest.as_ref().len() > PATH_MAX {
605 return Err(Error::InvalidInput);
606 }
607 self.call(OP_MOVE, path.as_ref(), dest.as_ref(), self.secs())?;
608 Ok(())
609 }
610
611 /// Delete a file, or a directory AND EVERYTHING INSIDE IT.
612 ///
613 /// The recursive one. [`delete`](Fs::delete) removes a single file and refuses a non-empty
614 /// directory; this removes the subtree. The names differ deliberately - a caller reaching for
615 /// the destructive one should have to type something that says so.
616 ///
617 /// **Blocks** up to [`SWEEP_SECS`], not the ordinary deadline: removing a subtree walks it, and
618 /// a bound shorter than the work turns a slow success into [`Error::OutcomeUnknown`] - which is
619 /// worse than waiting, because an unknown outcome forbids the retry that would have fixed it.
620 /// (This doc previously said "up to `DEFAULT_SECS`, and longer for a large tree", which cannot
621 /// happen: a call does not block past its own deadline, it gives up.)
622 ///
623 /// **Authority:** the caller's existing `fs` capability.
624 ///
625 /// # Errors
626 /// [`Error::Failed`] with [`reason`](Fs::reason), "not found" included (NOT
627 /// [`Error::NotFound`]; see the type's note). **Never retry this on
628 /// [`Error::OutcomeUnknown`]**: the service unlinks the subtree in one transaction and then
629 /// frees its blocks in many, so a deadline that passes mid-delete may leave the entry already
630 /// gone, and a retry then fails with "not found" for work that succeeded. A failure DURING the
631 /// freeing is also reported as [`Error::Failed`] although the unlink has committed; the blocks
632 /// it did not free are leaked until [`check`](Fs::check) rebuilds the bitmap.
633 pub fn delete_all(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error> {
634 self.call(OP_DELETE_TREE, path.as_ref(), &[], SWEEP_SECS)?;
635 Ok(())
636 }
637
638 /// Open a file as a CAPABILITY (CLAUDE.md 7.10), rather than acting on it by path.
639 ///
640 /// The returned [`File`](crate::file::File) holds an unforgeable, revocable, non-escalating
641 /// kernel capability to exactly this file. A read-only one cannot write, and the refusal comes
642 /// from the KERNEL before `fs` is reached - which is the difference between a capability and a
643 /// handle a service merely agrees to honour.
644 ///
645 /// `rights` is a mask of [`cap::READ`](crate::cap::READ), [`cap::WRITE`](crate::cap::WRITE) and
646 /// [`cap::APPEND`](crate::cap::APPEND). **Check
647 /// [`File::rights`](crate::file::File::rights) on the result**: it is what the capability
648 /// carries, read from the kernel. `fs` narrows rather than refuses in one case - it will not mint
649 /// a writable capability to a SEALED file, and hands back a read-only one instead.
650 ///
651 /// # Why this borrows the handle
652 ///
653 /// The `File` and this `Fs` share one correlation-tag counter, because they talk to one service
654 /// over one endpoint. The borrow is what makes that structural rather than a rule to remember.
655 /// See [`File`](crate::file::File) for how to hold two files at once.
656 ///
657 /// **Blocks** up to [`call::DEFAULT_SECS`], or this handle's
658 /// [`patience_secs`](Fs::patience_secs) if it was given one. **Authority:** the caller's existing `fs` capability
659 /// - opening a file grants nothing the contract did not already grant.
660 ///
661 /// # Errors
662 /// - [`Error::PermissionDenied`] - a writable capability was asked for on a sealed file and no
663 /// read-only fallback was available (`rights` did not include [`cap::READ`](crate::cap::READ)).
664 /// - [`Error::Failed`] - no such file, a directory, a path longer than `fs`'s 96-byte open
665 /// table allows, too many files open, or `fs` replied without a capability. `fs` sends this
666 /// one WITHOUT a reason, so [`reason`](Fs::reason) is empty and the cause is in `fs`'s log
667 /// only. Retrying an open is safe.
668 pub fn open<'f>(&'f mut self, path: impl AsRef<[u8]>, rights: u8) -> Result<crate::file::File<'f, 'a>, Error> {
669 let ctx = self.ctx;
670 self.call(OP_OPEN, path.as_ref(), &[rights], self.secs())?;
671 // The capability rode the reply as an EMBEDDED cap, not as payload bytes; the kernel placed
672 // it in our table on receipt and it is ours to claim or leak.
673 let cap = ctx.take_pending_cap().ok_or(Error::Failed)?;
674 // Record what the capability CARRIES, not what was asked: `fs` narrows a sealed file to
675 // read-only, and APPEND is a request flag that never becomes a kernel right. A slot the
676 // kernel cannot describe is no capability we can use (`backlog/fixed/80` G2).
677 let granted = match ctx.query_cap_rights(cap) {
678 Some(r) => (r as u8) & (crate::cap::READ | crate::cap::WRITE),
679 None => return Err(Error::Failed),
680 };
681 Ok(crate::file::File::new(self, ctx, cap, granted))
682 }
683
684 /// Rebuild the free-space bitmap from the file tree, and report what was found.
685 ///
686 /// This is `fsck`. It WRITES - the bitmap is rebuilt from the tree, which is the irreducible
687 /// source (26.4: the bitmap and free count are derived views, and this is their repair path).
688 /// A read-only mount refuses it.
689 ///
690 /// **Blocks** up to [`SWEEP_SECS`]. It walks the whole volume.
691 ///
692 /// # Errors
693 /// [`Error::Failed`] on a read-only mount or a failed sweep, with an EMPTY
694 /// [`reason`](Fs::reason) (`fs` logs why but sends a bare `FS_ERR`);
695 /// [`Error::NoFilesystem`] if nothing is mounted, or [`Error::Unavailable`] if storage is
696 /// present but unreadable. Rebuilding is idempotent - it derives the
697 /// bitmap from the tree either way - so a repeat is safe, though it costs another full sweep.
698 pub fn check(&mut self) -> Result<Check, Error> {
699 let r = self.call(OP_CHECK, &[], &[], SWEEP_SECS)?;
700 let b = r.body();
701 if b.len() < 36 {
702 return Err(Error::Malformed);
703 }
704 Ok(Check {
705 files: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
706 dirs: u32::from_le_bytes([b[4], b[5], b[6], b[7]]),
707 bad: u32::from_le_bytes([b[8], b[9], b[10], b[11]]),
708 used: u64::from_le_bytes([b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19]]),
709 free: u64::from_le_bytes([b[20], b[21], b[22], b[23], b[24], b[25], b[26], b[27]]),
710 free_before: u64::from_le_bytes([b[28], b[29], b[30], b[31], b[32], b[33], b[34], b[35]]),
711 })
712 }
713
714 /// What storage `fs` is serving: the device's size, whether a volume is mounted, how full it is
715 /// and its label (`OP_DRIVES_INFO`, the request behind the shell's `drives`). **Changes nothing**,
716 /// except that `fs` drops a mount whose device has gone - which is it noticing, not this asking.
717 ///
718 /// # Errors
719 /// [`Error::Malformed`] on a reply too short to be one; the no-answer errors of every request.
720 pub fn drive(&mut self) -> Result<Drive, Error> {
721 let secs = self.secs();
722 let r = self.call(OP_DRIVES_INFO, &[], &[], secs)?;
723 // [mounted, capacity u64, total u64, used u64, flags, label_len, label..]
724 let b = r.body();
725 if b.len() < 27 {
726 return Err(Error::Malformed);
727 }
728 let u = |i: usize| u64::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7]]);
729 let mut d = Drive { capacity_sectors: u(1), mounted: b[0] != 0, total_blocks: u(9), used_blocks: u(17),
730 label: [0; 31], label_len: 0 };
731 if d.mounted {
732 let ll = (b[26] as usize).min(31).min(b.len().saturating_sub(27));
733 d.label[..ll].copy_from_slice(&b[27..27 + ll]);
734 d.label_len = ll as u8;
735 }
736 Ok(d)
737 }
738
739 /// Verify every referenced block's CRC and report. **Changes nothing.**
740 ///
741 /// The read-only twin of [`check`](Fs::check): that one repairs the bitmap, this one reads the
742 /// data and says what does not verify. Safe on a read-only mount, and safe to run at any time.
743 ///
744 /// **Blocks** up to [`SWEEP_SECS`]. It reads the whole volume.
745 ///
746 /// # Errors
747 /// [`Error::NoFilesystem`] if nothing is mounted ([`Error::Unavailable`] if storage is present
748 /// but unreadable); [`Error::Failed`], with no reason, if the sweep itself failed. A scrub
749 /// writes nothing, so every no-answer
750 /// error here is safe to retry.
751 pub fn scrub(&mut self) -> Result<Scrub, Error> {
752 let r = self.call(OP_SCRUB, &[], &[], SWEEP_SECS)?;
753 let b = r.body();
754 if b.len() < 20 {
755 return Err(Error::Malformed);
756 }
757 Ok(Scrub {
758 files: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
759 dirs: u32::from_le_bytes([b[4], b[5], b[6], b[7]]),
760 bad: u32::from_le_bytes([b[8], b[9], b[10], b[11]]),
761 scanned: u64::from_le_bytes([b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19]]),
762 })
763 }
764
765 /// Freeze a file's content. **Permanently.**
766 ///
767 /// A sealed file can never be written again - not by you, not by anything holding a capability
768 /// to it, not after a restart. `fs` will not even mint a writable capability for one
769 /// ([`open`](Fs::open) narrows to read-only rather than handing back a capability it could not
770 /// honour).
771 ///
772 /// **There is no unseal**, by design: a seal a holder can lift is a request rather than a
773 /// guarantee. If you are offering this to a person, ask them first - every other destructive
774 /// operation here can be undone by doing the opposite, and this one cannot.
775 ///
776 /// **Blocks** up to [`call::DEFAULT_SECS`], or this handle's
777 /// [`patience_secs`](Fs::patience_secs) if it was given one. **Authority:** the caller's existing `fs` capability.
778 ///
779 /// # Errors
780 /// - [`Error::Failed`] - with [`reason`](Fs::reason) saying why: "not found" for an absent path
781 /// (NOT [`Error::NotFound`]; see the type's note), or "only a file can be sealed", because a
782 /// directory cannot be.
783 /// - Sealing twice is harmless - the second call finds it already sealed - so a no-answer error
784 /// here may be retried, unlike most mutations.
785 pub fn seal(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error> {
786 self.call(OP_SEAL, path.as_ref(), &[], self.secs())?;
787 Ok(())
788 }
789
790 /// Quiesce the filesystem for a shutdown (`docs/shutdown.md` 4.3): `fs` finishes the request in hand,
791 /// flushes the device, and then refuses every write for ten seconds - so nothing changes between the
792 /// flush and the power going off, and a shutdown that never comes leaves a filesystem that takes
793 /// writes again by itself. `Ok(true)` when the device confirmed the flush, `Ok(false)` when it did not.
794 ///
795 /// **Blocks** up to [`call::DEFAULT_SECS`], or this handle's [`patience_secs`](Fs::patience_secs) - a
796 /// shutdown gives it about one. **Authority:** the caller's existing `fs` capability.
797 pub fn quiesce(&mut self) -> Result<bool, Error> {
798 let r = self.call(OP_QUIESCE, b"", &[], self.secs())?;
799 Ok(r.msg.payload_bytes().get(2) == Some(&1))
800 }
801
802 /// End a [`quiesce`](Fs::quiesce) early, for a shutdown that was cancelled after its tidy-up: `fs` takes writes
803 /// again at once rather than refusing them for the rest of its ten seconds. **Authority:** the caller's `fs`
804 /// capability.
805 pub fn resume(&mut self) -> Result<(), Error> {
806 self.call(OP_QUIESCE, b"", &[1], self.secs())?;
807 Ok(())
808 }
809
810 /// Ask whether a path exists, and what it is.
811 ///
812 /// **Blocks** up to [`call::DEFAULT_SECS`], or this handle's
813 /// [`patience_secs`](Fs::patience_secs) if it was given one. **Authority:** the caller's existing `fs` capability.
814 ///
815 /// # Errors
816 /// [`Error::NotFound`] if the path is absent. See [`Error`] for the no-answer cases; a `stat` is
817 /// read-only, so retrying any of them is safe.
818 pub fn stat(&mut self, path: impl AsRef<[u8]>) -> Result<Stat, Error> {
819 let r = self.call(OP_STAT_FILE, path.as_ref(), &[], self.secs())?;
820 let b = r.body();
821 // [status, exists, size:u64, is_dir] after the tag - 11 bytes, 12 with the tag. `body` has
822 // stripped the tag and the status, leaving [exists, size, is_dir].
823 if b.len() < 10 {
824 return Err(Error::Malformed);
825 }
826 if b[0] != 1 {
827 return Err(Error::NotFound);
828 }
829 let size = u64::from_le_bytes([b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8]]);
830 Ok(Stat { size, is_dir: b[9] == 1 })
831 }
832
833 /// Read at most `buf.len()` bytes from `offset`, and say how many arrived.
834 ///
835 /// The positional read. Use it to move a file larger than any buffer, or to resume where a
836 /// previous pass stopped; use [`read_into`](Fs::read_into) when the whole file fits and you want
837 /// it in one call.
838 ///
839 /// Returns 0 at end of file. One call moves at most [`IO_CHUNK`] bytes however large `buf` is,
840 /// so a caller wanting more must loop - and the loop is the caller's, because only the caller
841 /// knows whether a short read means "done" or "keep going".
842 ///
843 /// **Blocks** up to [`call::DEFAULT_SECS`], or this handle's
844 /// [`patience_secs`](Fs::patience_secs) if it was given one. **Authority:** the caller's existing `fs` capability.
845 ///
846 /// # Errors
847 /// - [`Error::NotFound`] - no such file, or it is a directory. **Also a data block that failed
848 /// its CRC**: `services/fs` answers every read failure with `FS_NOTFOUND`, so a damaged file
849 /// reads as an absent one. [`stat`](Fs::stat) tells the two apart.
850 /// - A read changes nothing, so **every no-answer error here is safe to retry** - which is what
851 /// makes a resumable copy possible at all. See [`Error::retry_is_safe`].
852 pub fn read_at(&mut self, path: impl AsRef<[u8]>, offset: u64, buf: &mut [u8]) -> Result<usize, Error> {
853 let want = buf.len().min(IO_CHUNK);
854 if want == 0 {
855 return Ok(0);
856 }
857 let mut tail = [0u8; 12];
858 tail[..8].copy_from_slice(&offset.to_le_bytes());
859 tail[8..].copy_from_slice(&(want as u32).to_le_bytes());
860 let r = self.call(OP_READ_AT, path.as_ref(), &tail, self.secs())?;
861 let b = r.body();
862 // `[n:u32, bytes..]` after the tag and status the call already checked.
863 if b.len() < 4 {
864 return Err(Error::Malformed);
865 }
866 let n = u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize;
867 if n > want || b.len() < 4 + n {
868 return Err(Error::Malformed);
869 }
870 buf[..n].copy_from_slice(&b[4..4 + n]);
871 Ok(n)
872 }
873
874 /// Read a whole file into `buf`, returning how many bytes were written.
875 ///
876 /// Streams in [`IO_CHUNK`] pieces, so a file far larger than one IPC message reads correctly.
877 ///
878 /// **Blocks**, once per chunk. **Authority:** the caller's existing `fs` capability.
879 ///
880 /// # Errors
881 /// - [`Error::NotFound`] - no such file, or it is a directory, or (as for
882 /// [`read_at`](Fs::read_at)) a data block failed its CRC part way through.
883 /// - [`Error::BufferTooSmall`] - the file does not fit. **Nothing is written**; call again with
884 /// room, having learned the size from [`stat`](Fs::stat).
885 /// - A read is idempotent, so every no-answer error here may safely be retried.
886 pub fn read_into(&mut self, path: impl AsRef<[u8]>, buf: &mut [u8]) -> Result<usize, Error> {
887 // Bound once: `impl AsRef<[u8]>` is not `Copy`, and this uses the path twice (a stat, then a
888 // chunk loop). A `&[u8]` is `Copy`, so taking the reference first is all it needs.
889 let path = path.as_ref();
890 let st = self.stat(path)?;
891 if st.is_dir {
892 return Err(Error::NotFound);
893 }
894 let size = st.size as usize;
895 if size > buf.len() {
896 return Err(Error::BufferTooSmall);
897 }
898 let mut off = 0usize;
899 while off < size {
900 let want = (size - off).min(IO_CHUNK);
901 let mut tail = [0u8; 12];
902 tail[..8].copy_from_slice(&(off as u64).to_le_bytes());
903 tail[8..].copy_from_slice(&(want as u32).to_le_bytes());
904 let r = self.call(OP_READ_AT, path.as_ref(), &tail, self.secs())?;
905 let b = r.body();
906 if b.len() < 4 {
907 return Err(Error::Malformed);
908 }
909 let n = u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize;
910 if n == 0 || n > want || b.len() < 4 + n {
911 return Err(Error::Malformed);
912 }
913 buf[off..off + n].copy_from_slice(&b[4..4 + n]);
914 off += n;
915 }
916 Ok(off)
917 }
918
919 /// Create or replace a file with `data`.
920 ///
921 /// **Blocks**. **Authority:** the caller's existing `fs` capability.
922 ///
923 /// # This operation CHANGES STATE
924 ///
925 /// If it returns [`Error::OutcomeUnknown`], the write may have happened. Do not call it again
926 /// to "make sure": read the file back, or report the uncertainty. [`Error::retry_is_safe`]
927 /// answers this for you, and says `false` for that case on purpose.
928 ///
929 /// # Larger than one message
930 ///
931 /// `data` longer than [`IO_CHUNK`] is written as the file at its whole size first
932 /// ([`create_sized`](Fs::create_sized)), then filled chunk by chunk from offset 0. A failure
933 /// part way leaves a file of the full size whose tail is not yet written, and is reported.
934 /// (Until 2026-10-10 the file was created from its first chunk alone, sized for that chunk, so
935 /// `services/fs` refused the second as "write past extent" - `backlog/fixed/80` G1.)
936 pub fn write(&mut self, path: impl AsRef<[u8]>, data: &[u8]) -> Result<(), Error> {
937 if data.len() <= IO_CHUNK {
938 self.call(OP_WRITE_FILE, path.as_ref(), data, self.secs())?;
939 return Ok(());
940 }
941 // Larger than one message: size it, then fill it positionally. `OP_WRITE_AT` at a fixed
942 // offset is positionally idempotent - the same bytes at the same offset land the same way
943 // twice - which is what makes a chunked write safe to resume at all.
944 self.create_sized(path.as_ref(), data.len() as u64)?;
945 let mut off = 0;
946 while off < data.len() {
947 let n = (data.len() - off).min(IO_CHUNK);
948 self.write_at(path.as_ref(), off as u64, &data[off..off + n])?;
949 off += n;
950 }
951 Ok(())
952 }
953
954 /// Create a directory. Fails if the parent does not exist, or if the name already does
955 /// ([`Error::Failed`], reason "path not found" or "already exists").
956 ///
957 /// **Blocks**. Changes state: see the note on [`write`] about [`Error::OutcomeUnknown`].
958 pub fn create_dir(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error> {
959 self.call(OP_MKDIR, path.as_ref(), &[], self.secs())?;
960 Ok(())
961 }
962
963 /// Delete one file or one empty directory.
964 ///
965 /// **Blocks**. **Destructive, and not idempotent in the way that matters**: on
966 /// [`Error::OutcomeUnknown`] the file may already be gone, and a second delete would fail
967 /// ([`Error::Failed`], reason "not found") for work that succeeded. Report the uncertainty; do
968 /// not re-send. A non-empty directory is refused with reason "directory not empty".
969 pub fn delete(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error> {
970 self.call(OP_DELETE, path.as_ref(), &[], self.secs())?;
971 Ok(())
972 }
973
974 /// Allocate a file of `capacity` bytes without writing content into it.
975 ///
976 /// The extent is reserved up front, so later [`write_at`](Fs::write_at) calls land in space that is already
977 /// the file's. That is what makes a long append-style writer bounded: it cannot run out of room
978 /// halfway and leave a half-file behind.
979 ///
980 /// **An existing file at `path` is REPLACED**: `services/fs` truncates it to a fresh extent of
981 /// this size (a sealed file is refused). A directory there is refused.
982 ///
983 /// **Blocks. Changes state**: on [`Error::OutcomeUnknown`] the file may exist. Use
984 /// [`exists`](Fs::exists) to find out rather than calling this again, which would succeed and
985 /// replace the file a second time, discarding anything written into it in between.
986 ///
987 /// Added because `services/recorder` needed it during migration. It is a real filesystem
988 /// operation, so it belongs in the typed surface rather than behind an opcode escape hatch.
989 pub fn create_sized(&mut self, path: impl AsRef<[u8]>, capacity: u64) -> Result<(), Error> {
990 self.call(OP_WRITE_NEW, path.as_ref(), &capacity.to_le_bytes(), self.secs())?;
991 Ok(())
992 }
993
994 /// Write `data` at a byte `offset` into an existing file.
995 ///
996 /// **Blocks. Changes state.** Unusually among the write operations, a positional write at a
997 /// FIXED offset into an already-allocated extent is idempotent: repeating it puts the same bytes
998 /// in the same place. It is safe to re-send because it is positional, not appending, which is
999 /// what makes a chunked write resumable at all.
1000 ///
1001 /// **`offset` must be a multiple of 508** (one data block's payload; [`IO_CHUNK`] is seven of
1002 /// them), and `offset + data.len()` must lie inside the file's existing extent - `fs` does not
1003 /// grow a file here. Either violation is [`Error::Failed`], reason "unaligned offset" or
1004 /// "write past extent". Size the file first with [`create_sized`](Fs::create_sized).
1005 ///
1006 /// Even so this returns [`Error::OutcomeUnknown`] honestly on a timeout, because the DECISION to
1007 /// re-send belongs to the caller who knows the offset is fixed - not to a library that would be
1008 /// guessing.
1009 pub fn write_at(&mut self, path: impl AsRef<[u8]>, offset: u64, data: &[u8]) -> Result<(), Error> {
1010 if data.len() > IO_CHUNK {
1011 return Err(Error::InvalidInput);
1012 }
1013 let mut tail = [0u8; 8 + IO_CHUNK];
1014 tail[..8].copy_from_slice(&offset.to_le_bytes());
1015 tail[8..8 + data.len()].copy_from_slice(data);
1016 self.call(OP_WRITE_AT, path.as_ref(), &tail[..8 + data.len()], self.secs())?;
1017 Ok(())
1018 }
1019
1020 /// Rename a file within its directory. `new_name` is a bare name, not a path.
1021 ///
1022 /// **Blocks. Changes state, and is NOT idempotent**: a second rename after a successful one
1023 /// fails with [`Error::Failed`], reason "entry not found", because the source is already gone. On
1024 /// [`Error::OutcomeUnknown`] check with [`exists`](Fs::exists) rather than re-sending - this is precisely
1025 /// the case where a retry reports failure for work that succeeded.
1026 pub fn rename(&mut self, path: impl AsRef<[u8]>, new_name: impl AsRef<[u8]>) -> Result<(), Error> {
1027 self.call(OP_RENAME, path.as_ref(), new_name.as_ref(), self.secs())?;
1028 Ok(())
1029 }
1030
1031 /// Does this path exist? A convenience over [`stat`](Fs::stat), and read-only.
1032 pub fn exists(&mut self, path: impl AsRef<[u8]>) -> Result<bool, Error> {
1033 match self.stat(path) {
1034 Ok(_) => Ok(true),
1035 Err(Error::NotFound) => Ok(false),
1036 Err(e) => Err(e),
1037 }
1038 }
1039}
1040
1041/// A checked reply: the tag matched and the status said OK.
1042struct Reply {
1043 msg: Message,
1044}
1045
1046impl Reply {
1047 /// Everything after the tag and the status byte.
1048 fn body(&self) -> &[u8] {
1049 let b = self.msg.payload_bytes();
1050 if b.len() > 2 { &b[2..] } else { &[] }
1051 }
1052}