Skip to content
Closed
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
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/shared-vfs/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# shared-vfs

Generic virtual filesystem machinery: the permanent bottom of the dependency stack.

- std only. No dependencies, workspace or external. The manifest test enforces this; never weaken it.
- No promptforge policy: no /_promptforge paths, no Store, no run concepts.
- The public surface is load-bearing: add defaulted methods, never change existing signatures. Every edit rebuilds the whole stack.
16 changes: 16 additions & 0 deletions crates/shared-vfs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "shared-vfs"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish = false

description = "PromptForge shared virtual filesystem machinery: canonical interned paths, claims, routing, and backends"

# Zero-dependency rule: std only. No dependencies, workspace or external.
# The manifest test enforces this; never weaken it.
[dependencies]

[lints]
workspace = true
51 changes: 51 additions & 0 deletions crates/shared-vfs/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! The error type shared by every VFS layer.

use std::fmt;

/// The one error type returned by every virtual filesystem operation.
///
/// `#[non_exhaustive]` so new kinds can ship without breaking match arms
/// in downstream crates; the public surface of this crate is load-bearing.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VfsError {
/// The path does not exist in the serving backend.
NotFound(String),
/// The operation is not permitted: a read-only mount or a policy denial.
PermissionDenied(String),
/// The path already exists where creation required absence.
AlreadyExists(String),
/// The path is malformed or escapes the virtual namespace root.
InvalidPath(String),
/// A directory operation named a non-directory.
NotADirectory(String),
/// A file operation named a directory.
IsADirectory(String),
/// A directory removal without `recursive` named a non-empty directory.
DirectoryNotEmpty(String),
/// The serving backend does not implement the operation.
Unsupported(String),
/// The operation conflicts with another live identity's claim.
Conflict(String),
/// The serving backend failed for any other reason.
Backend(String),
}

impl fmt::Display for VfsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound(m) => write!(f, "not found: {m}"),
Self::PermissionDenied(m) => write!(f, "permission denied: {m}"),
Self::AlreadyExists(m) => write!(f, "already exists: {m}"),
Self::InvalidPath(m) => write!(f, "invalid path: {m}"),
Self::NotADirectory(m) => write!(f, "not a directory: {m}"),
Self::IsADirectory(m) => write!(f, "is a directory: {m}"),
Self::DirectoryNotEmpty(m) => write!(f, "directory not empty: {m}"),
Self::Unsupported(m) => write!(f, "unsupported operation: {m}"),
Self::Conflict(m) => write!(f, "conflicting claim: {m}"),
Self::Backend(m) => write!(f, "backend failure: {m}"),
}
}
}

impl std::error::Error for VfsError {}
50 changes: 50 additions & 0 deletions crates/shared-vfs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Generic virtual filesystem machinery: canonical interned paths, the
//! claims model, the mount router, and backends.
//!
//! This crate is the permanent bottom of the dependency stack: std only,
//! no workspace or external crates, and no promptforge policy (no
//! `/_promptforge` paths, no Store, no run concepts).

mod error;
mod path;
mod traits;
mod types;

pub use error::VfsError;
pub use path::{VfsPath, VfsPathBuf};
pub use traits::{AllowAll, ExecId, Op, Policy, Verdict, Vfs, VfsAccess};
pub use types::{Entry, FileType, GrepMatch, GrepQuery, GrepResults, Stat};

#[cfg(test)]
mod tests {
/// The zero-dependency rule is load-bearing: this crate compiles alone
/// and never rebuilds for a dependency rev, so the manifest must never
/// declare a dependency. This test reads the crate's own Cargo.toml and
/// fails if any dependency table carries an entry.
#[test]
fn the_manifest_declares_no_dependencies() -> Result<(), std::io::Error> {
let manifest = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"),
)?;
let mut section = String::new();
for raw_line in manifest.lines() {
let line = raw_line.trim();
if line.starts_with('[') {
section = line.trim_matches(['[', ']']).to_owned();
continue;
}
if line.is_empty() || line.starts_with('#') {
continue;
}
let is_dependency_table = section == "dependencies"
|| section == "dev-dependencies"
|| section == "build-dependencies"
|| (section.starts_with("target.") && section.ends_with(".dependencies"));
assert!(
!is_dependency_table,
"zero-dependency rule violated: [{section}] declares `{line}`"
);
}
Ok(())
}
}
244 changes: 244 additions & 0 deletions crates/shared-vfs/src/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
//! Canonical, interned virtual paths.
//!
//! Paths are canonicalized at the moment the API receives them and interned,
//! so claim lookups are pointer-cheap and aliases cannot slip past the
//! claims tables. The only way to form a [`VfsPath`] is through
//! `canonicalize`, which is crate-private: canonicalization at receipt is
//! enforced by visibility, not convention.

use std::collections::HashMap;
use std::fmt;
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};

use crate::error::VfsError;

/// Hand-rolled string interner on std. Strings are leaked once each, so
/// resolution is a vector index and equality is an integer compare.
struct Interner {
ids: HashMap<&'static str, u32>,
strings: Vec<&'static str>,
}

impl Interner {
fn new() -> Self {
Self {
ids: HashMap::new(),
strings: Vec::new(),
}
}

fn intern(&mut self, s: &str) -> u32 {
if let Some(&id) = self.ids.get(s) {
return id;
}
let leaked: &'static str = Box::leak(s.into());
let Ok(id) = u32::try_from(self.strings.len()) else {
panic!("vfs path interner exhausted")
};
self.strings.push(leaked);
self.ids.insert(leaked, id);
id
}

fn resolve(&self, id: u32) -> &'static str {
match self.strings.get(id as usize) {
Some(s) => s,
None => panic!("vfs path id {id} was never interned"),
}
}
}

/// Poison-safe lock: each guard scope is one complete mutation, so a
/// panicking writer cannot leave the tables half-updated and recovery is
/// safe.
fn interner() -> MutexGuard<'static, Interner> {
static INTERNER: OnceLock<Mutex<Interner>> = OnceLock::new();
INTERNER
.get_or_init(|| Mutex::new(Interner::new()))
.lock()
.unwrap_or_else(PoisonError::into_inner)
}

/// Canonical, interned virtual path. Produced by `canonicalize` at the
/// moment the API receives a path; interning makes claim lookups
/// pointer-cheap and guarantees alias detection.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct VfsPath {
id: u32,
}

impl VfsPath {
/// Returns the canonical string for this path.
#[must_use]
pub fn as_str(&self) -> &'static str {
interner().resolve(self.id)
}

/// Returns an owned copy of this path.
#[must_use]
pub fn to_buf(&self) -> VfsPathBuf {
VfsPathBuf(self.as_str().into())
}
}

impl fmt::Debug for VfsPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "VfsPath({:?})", self.as_str())
}
}

impl fmt::Display for VfsPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}

/// Owned canonical virtual path, for places that outlive an interned
/// reference or arrive owned (grep roots, symlink targets).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct VfsPathBuf(String);

impl VfsPathBuf {
/// Returns the canonical string.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}

impl From<VfsPath> for VfsPathBuf {
fn from(path: VfsPath) -> Self {
path.to_buf()
}
}

impl fmt::Display for VfsPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}

/// Canonicalizes a virtual path at API receipt.
///
/// The internal namespace is POSIX-shaped: rooted, forward slashes, strict.
/// The lexical rules are: backslashes from Windows hosts count as
/// separators; duplicate separators collapse; `.` segments vanish; `..`
/// pops exactly one segment and popping past the root is rejected; a
/// trailing slash is dropped; the root canonicalizes to itself. Case is
/// preserved and significant (POSIX semantics): paths differing only in
/// case are distinct. Relative and empty paths are rejected.
pub(crate) fn canonicalize(path: &str) -> Result<VfsPath, VfsError> {
if path.is_empty() {
return Err(VfsError::InvalidPath("empty path".into()));
}
let normalized = path.replace('\\', "/");
if !normalized.starts_with('/') {
return Err(VfsError::InvalidPath(format!(
"relative path is not in the virtual namespace: {path:?}"
)));
}
let mut segments: Vec<&str> = Vec::new();
for segment in normalized.split('/') {
match segment {
"" | "." => {}
".." => {
if segments.pop().is_none() {
return Err(VfsError::InvalidPath(format!(
"path escapes the namespace root: {path:?}"
)));
}
}
_ => segments.push(segment),
}
}
let canonical = if segments.is_empty() {
"/".to_owned()
} else {
let mut s = String::with_capacity(normalized.len() + 1);
for segment in &segments {
s.push('/');
s.push_str(segment);
}
s
};
let id = interner().intern(&canonical);
Ok(VfsPath { id })
}

#[cfg(test)]
mod tests {
use super::{VfsPath, canonicalize};
use crate::VfsError;

fn canonical(path: &str) -> Result<String, VfsError> {
Ok(canonicalize(path)?.as_str().to_owned())
}

#[test]
fn the_namespace_root_canonicalizes_to_itself() -> Result<(), VfsError> {
assert_eq!(canonical("/")?, "/");
Ok(())
}

#[test]
fn duplicate_separators_collapse_to_one() -> Result<(), VfsError> {
assert_eq!(canonical("/a//b///c")?, "/a/b/c");
Ok(())
}

#[test]
fn dot_segments_are_removed() -> Result<(), VfsError> {
assert_eq!(canonical("/a/./b/./c")?, "/a/b/c");
Ok(())
}

#[test]
fn dotdot_pops_exactly_one_segment() -> Result<(), VfsError> {
assert_eq!(canonical("/a/b/../c")?, "/a/c");
assert_eq!(canonical("/a/..")?, "/");
Ok(())
}

#[test]
fn a_trailing_slash_is_dropped() -> Result<(), VfsError> {
assert_eq!(canonical("/a/b/")?, "/a/b");
Ok(())
}

#[test]
fn backslashes_from_windows_hosts_are_separators() -> Result<(), VfsError> {
assert_eq!(canonical("/a\\b/c")?, "/a/b/c");
Ok(())
}

#[test]
fn traversal_past_the_root_is_rejected() {
assert!(canonicalize("/..").is_err());
assert!(canonicalize("/a/../../b").is_err());
}

#[test]
fn relative_and_empty_paths_are_rejected() {
assert!(canonicalize("").is_err());
assert!(canonicalize("a/b").is_err());
assert!(canonicalize("./a").is_err());
}

#[test]
fn case_is_preserved_and_significant() -> Result<(), VfsError> {
assert_eq!(canonical("/ReadMe.md")?, "/ReadMe.md");
let upper: VfsPath = canonicalize("/ReadMe.md")?;
let lower: VfsPath = canonicalize("/readme.md")?;
assert_ne!(upper, lower);
Ok(())
}

#[test]
fn identical_paths_intern_to_one_entry() -> Result<(), VfsError> {
let first = canonicalize("/a/b")?;
let second = canonicalize("/a/./b/")?;
assert_eq!(first, second);
assert!(std::ptr::eq(first.as_str(), second.as_str()));
Ok(())
}
}
Loading
Loading