sprawling-storage 0.0.8

The append-only, hash-chained Ledger of a sprawling city, its content store, checkpoints and worktrees.
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (c) 2026 2youg1 and the sprawling contributors

//! The filesystem seam this crate reaches disk through, and nothing else.
//!
//! One trait, two adapters: [`crate::real_fs::RealFs`] over `std::fs` in
//! production, and `FaultFs` over a deterministic power-loss
//! model. The second adapter is what makes this a seam rather than a
//! layer of indirection — a power cut is a property of filesystem
//! semantics, so it is injected here and every module above stays free
//! of test hooks (`crates/storage/Spec.lean` §10, design A).
//!
//! **The seam stays inner.** `Vfs` is `pub(crate)`: `JsonlLedger` holds
//! a `Box<dyn Vfs>` and never names it in a public signature, because a
//! private trait in a public signature does not compile (E0445) and
//! because what a city stores its history on is not a decision this
//! crate delegates outward.

use std::io;
use std::path::{Path, PathBuf};

/// Crate-internal filesystem seam. Two adapters:
/// [`crate::real_fs::RealFs`] (std) and `FaultFs` (fault injection).
/// Never public: the seam stays inner. `Send`, because a store
/// built on it is handed to the thread that drives a run
/// (`crates/sprawling/Spec.lean` §8-44).
pub(crate) trait Vfs: Send {
    fn create_dir_all(&mut self, dir: &Path) -> io::Result<()>;
    /// Files only, sorted by path: deterministic traversal.
    fn list(&self, dir: &Path) -> io::Result<Vec<PathBuf>>;
    /// Subdirectories only, sorted. Shallow like `list`, so a caller
    /// that wants a tree walks it with an explicit worklist rather than
    /// by recursion, and cannot overflow a stack on a deep city.
    fn list_dirs(&self, dir: &Path) -> io::Result<Vec<PathBuf>>;
    fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
    /// How many bytes `path` holds right now.
    ///
    /// Separate from [`Vfs::read`] because the side index compares a
    /// segment's length against the length it has already folded, and
    /// lifting the segment to learn its length would read the whole
    /// ledger on every refresh.
    fn size(&self, path: &Path) -> io::Result<u64>;
    /// At most `len` bytes of `path`, starting at `offset`, leaving
    /// everything before `offset` on the disk.
    ///
    /// A short answer means the file ends there, which is how a caller
    /// asking past the end learns it did: this seam reports the file as
    /// it is and refuses nothing, so the meaning of an out-of-range
    /// request stays with the module that owns the request's grammar
    /// (`crates/storage/spec/Cas.lean` §8-3).
    fn read_at(&self, path: &Path, offset: u64, len: u64) -> io::Result<Vec<u8>>;
    /// Creates the file when absent.
    fn append(&mut self, path: &Path, bytes: &[u8]) -> io::Result<()>;
    fn truncate(&mut self, path: &Path, len: u64) -> io::Result<()>;
    fn sync_data(&mut self, path: &Path) -> io::Result<()>;
    /// Atomic replace; durability of the new entry still needs sync_dir.
    fn rename(&mut self, from: &Path, to: &Path) -> io::Result<()>;
    /// Durability of directory entries; explicit no-op on Windows (no
    /// directory-handle sync primitive there — `crates/storage/Spec.lean` §3, item 3).
    fn sync_dir(&mut self, dir: &Path) -> io::Result<()>;
    fn remove_file(&mut self, path: &Path) -> io::Result<()>;
    /// Gives `to` the permissions `from` carries, so a file written
    /// anew keeps what the file it stands for allowed.
    fn copy_permissions(&mut self, from: &Path, to: &Path) -> io::Result<()>;
    fn exists(&self, path: &Path) -> bool;
}