//! Bounded read-only filesystem interface with mount-scoped node identity. pub const MAX_NAME_BYTES: usize = 24; pub const MAX_READ_BYTES: usize = 32; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { BadMount, Stale, InvalidName, NoEntry, IsDirectory, CorruptMetadata, ReadFault, TooLarge, Overflow, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Kind { Directory, Regular } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Metadata { pub inode: u64, pub kind: Kind, pub size: u64, pub allocated_bytes: u64, pub read_only: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct NodeRef { volume: u8, generation: u64, inode: u64, } impl NodeRef { pub const fn volume(self) -> u8 { self.volume } pub const fn generation(self) -> u64 { self.generation } pub const fn inode(self) -> u64 { self.inode } } pub trait FileSystem { fn root(&self) -> u64; fn lookup(&self, parent: u64, name: &[u8]) -> Result; fn metadata(&self, inode: u64) -> Result; fn read_at(&self, inode: u64, offset: u64, output: &mut [u8]) -> Result; } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Mount { volume: u8, generation: u64, filesystem: F, } impl Mount { pub fn new(volume: u8, generation: u64, filesystem: F) -> Result { let root = filesystem.root(); if volume == 0 || generation == 0 || root == 0 { return Err(Error::BadMount); } let mount = Self { volume, generation, filesystem }; if mount.validated_metadata(root)?.kind != Kind::Directory { return Err(Error::BadMount); } Ok(mount) } pub fn root(&self) -> NodeRef { NodeRef {volume: self.volume, generation: self.generation, inode: self.filesystem.root()} } pub fn remount(&mut self) -> Result { let next = self.generation.checked_add(1).ok_or(Error::Overflow)?; self.generation = next; Ok(next) } fn inode(&self, node: NodeRef) -> Result { if node.volume != self.volume || node.generation != self.generation { return Err(Error::Stale); } Ok(node.inode) } fn validated_metadata(&self, inode: u64) -> Result { let metadata = self.filesystem.metadata(inode)?; if metadata.inode != inode || inode == 0 || !metadata.read_only || metadata.allocated_bytes % 512 != 0 || metadata.size > metadata.allocated_bytes { return Err(Error::CorruptMetadata); } Ok(metadata) } pub fn lookup(&self, parent: NodeRef, name: &[u8]) -> Result { let parent_inode = self.inode(parent)?; if name.is_empty() || name.len() > MAX_NAME_BYTES || name.contains(&0) || name.contains(&b'/') || core::str::from_utf8(name).is_err() { return Err(Error::InvalidName); } if self.validated_metadata(parent_inode)?.kind != Kind::Directory { return Err(Error::NoEntry); } let inode = self.filesystem.lookup(parent_inode, name)?; self.validated_metadata(inode)?; Ok(NodeRef {volume: self.volume, generation: self.generation, inode}) } pub fn metadata(&self, node: NodeRef) -> Result { self.validated_metadata(self.inode(node)?) } pub fn read_at(&self, node: NodeRef, offset: u64, output: &mut [u8]) -> Result { let inode = self.inode(node)?; if output.len() > MAX_READ_BYTES { return Err(Error::TooLarge); } let metadata = self.validated_metadata(inode)?; if metadata.kind != Kind::Regular { return Err(Error::IsDirectory); } let remaining = metadata.size.saturating_sub(offset); let requested = output.len().min(remaining as usize); if requested == 0 { return Ok(0); } let mut scratch = [0u8; MAX_READ_BYTES]; let actual = self.filesystem.read_at(inode, offset, &mut scratch[..requested])?; if actual > requested { return Err(Error::ReadFault); } output[..actual].copy_from_slice(&scratch[..actual]); Ok(actual) } }