//! Bounded, owner-scoped temporary filesystem with transactional quota checks. pub const MAX_FILES: usize = 2; pub const MAX_NAME_BYTES: usize = 12; pub const MAX_FILE_BYTES: usize = 32; pub const MAX_TOTAL_BYTES: usize = 64; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { InvalidName, InvalidOwner, Exists, NoEntry, NoSpace, Stale, Overflow, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Token {generation: u64, inode: u64} impl Token { pub const fn inode(self) -> u64 { self.inode } pub const fn generation(self) -> u64 { self.generation } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Name {bytes: [u8; MAX_NAME_BYTES], len: u8} impl Name { fn new(raw: &[u8]) -> Result { if raw.is_empty() || raw.len() > MAX_NAME_BYTES || raw.contains(&0) || raw.contains(&b'/') || core::str::from_utf8(raw).is_err() { return Err(Error::InvalidName); } let mut value = Self {bytes: [0; MAX_NAME_BYTES], len: raw.len() as u8}; value.bytes[..raw.len()].copy_from_slice(raw); Ok(value) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct File { inode: u64, owner: u64, name: Name, len: u8, data: [u8; MAX_FILE_BYTES], } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Tmpfs { generation: u64, next_inode: u64, files: [Option; MAX_FILES], quota: u8, used: u8, } impl Tmpfs { pub fn new(generation: u64, quota: usize) -> Result { if generation == 0 || quota == 0 || quota > MAX_TOTAL_BYTES { return Err(Error::NoSpace); } Ok(Self {generation, next_inode: 2, files: [None; MAX_FILES], quota: quota as u8, used: 0}) } pub const fn used(&self) -> usize { self.used as usize } pub const fn generation(&self) -> u64 { self.generation } pub fn create(&mut self, owner: u64, raw_name: &[u8]) -> Result { if owner == 0 { return Err(Error::InvalidOwner); } let name = Name::new(raw_name)?; if self.files.iter().flatten().any(|file| file.name == name) { return Err(Error::Exists); } let slot = self.files.iter().position(Option::is_none) .ok_or(Error::NoSpace)?; let next = self.next_inode.checked_add(1).ok_or(Error::Overflow)?; let token = Token {generation: self.generation, inode: self.next_inode}; self.files[slot] = Some(File {inode: token.inode, owner, name, len: 0, data: [0; MAX_FILE_BYTES]}); self.next_inode = next; Ok(token) } pub fn lookup(&self, owner: u64, raw_name: &[u8]) -> Result { if owner == 0 { return Err(Error::InvalidOwner); } let name = Name::new(raw_name)?; let file = self.files.iter().flatten().find(|file| file.name == name) .ok_or(Error::NoEntry)?; if file.owner != owner { return Err(Error::InvalidOwner); } Ok(Token {generation: self.generation, inode: file.inode}) } fn file_index(&self, owner: u64, token: Token) -> Result { if token.generation != self.generation { return Err(Error::Stale); } if owner == 0 { return Err(Error::InvalidOwner); } let index = self.files.iter().position(|slot| slot.is_some_and(|file| file.inode == token.inode)) .ok_or(Error::NoEntry)?; if self.files[index].unwrap().owner != owner { return Err(Error::InvalidOwner); } Ok(index) } pub fn len(&self, owner: u64, token: Token) -> Result { let index = self.file_index(owner, token)?; Ok(self.files[index].unwrap().len as usize) } pub fn write_at(&mut self, owner: u64, token: Token, offset: usize, bytes: &[u8]) -> Result { let index = self.file_index(owner, token)?; let end = offset.checked_add(bytes.len()).ok_or(Error::Overflow)?; if end > MAX_FILE_BYTES { return Err(Error::NoSpace); } let mut file = self.files[index].unwrap(); let next_len = (file.len as usize).max(end); let delta = next_len - file.len as usize; let next_used = (self.used as usize).checked_add(delta) .ok_or(Error::Overflow)?; if next_used > self.quota as usize { return Err(Error::NoSpace); } file.data[offset..end].copy_from_slice(bytes); file.len = next_len as u8; self.files[index] = Some(file); self.used = next_used as u8; Ok(bytes.len()) } pub fn read_at(&self, owner: u64, token: Token, offset: usize, output: &mut [u8]) -> Result { let index = self.file_index(owner, token)?; let file = self.files[index].unwrap(); let available = (file.len as usize).saturating_sub(offset); let actual = output.len().min(available); if actual != 0 { output[..actual].copy_from_slice(&file.data[offset..offset + actual]); } Ok(actual) } pub fn unlink(&mut self, owner: u64, raw_name: &[u8]) -> Result<(), Error> { let token = self.lookup(owner, raw_name)?; let index = self.file_index(owner, token)?; let file = self.files[index].unwrap(); self.files[index] = None; self.used -= file.len; Ok(()) } pub fn reset(&mut self) -> Result { let next = self.generation.checked_add(1).ok_or(Error::Overflow)?; self.files = [None; MAX_FILES]; self.used = 0; self.generation = next; Ok(next) } }