//! Bounded read-only cat state for a native EL0 file backend. use turkos_os_user_runtime_io::io::{Fault as WriteFault, Writer}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FileError { NotFound, BadFd, Access, Interrupted, Io } pub trait ReadOnlyFiles { type Handle; fn open(&mut self, path: &str) -> Result; fn read(&mut self, handle: &mut Self::Handle, output: &mut [u8]) -> Result; fn close(&mut self, handle: Self::Handle) -> Result<(), (FileError, Self::Handle)>; } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Progress { pub read_bytes: u64, pub written_bytes: u64, pub read_calls: u32, pub write_calls: u32, pub eof: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { EmptyBuffer, File(FileError), Output(WriteFault), InvalidCount, Stalled, Budget, } #[derive(Debug)] pub struct Cat { handle: Option, buffer: [u8; N], filled: usize, sent: usize, progress: Progress, } impl Cat { pub fn open>(files: &mut B, path: &str) -> Result { if N == 0 { return Err(Error::EmptyBuffer); } let handle = files.open(path).map_err(Error::File)?; Ok(Self {handle: Some(handle), buffer: [0; N], filled: 0, sent: 0, progress: Progress {read_bytes: 0, written_bytes: 0, read_calls: 0, write_calls: 0, eof: false}}) } pub const fn progress(&self) -> Progress { self.progress } pub fn remaining(&self) -> &[u8] { &self.buffer[self.sent..self.filled] } pub fn complete(&self) -> bool { self.progress.eof && self.sent == self.filled } /// Short reads keep the file open. Only a zero read marks EOF. /// Every attempt consumes one operation from the caller's budget. pub fn pump, W: Writer>( &mut self, files: &mut B, output: &mut W, max_operations: usize, ) -> Result { let mut operations = 0; let mut consecutive_zero_writes = 0; while !self.complete() { if operations == max_operations { return Err(Error::Budget); } operations += 1; if self.sent == self.filled { self.filled = 0; self.sent = 0; self.progress.read_calls += 1; let count = match files.read( self.handle.as_mut().expect("owned file handle"), &mut self.buffer, ) { Ok(count) => count, Err(FileError::Interrupted) => continue, Err(error) => return Err(Error::File(error)), }; if count > N { return Err(Error::InvalidCount); } if count == 0 { self.progress.eof = true; break; } self.filled = count; self.progress.read_bytes += count as u64; continue; } self.progress.write_calls += 1; match output.write_once(self.remaining()) { Ok(0) => { consecutive_zero_writes += 1; if consecutive_zero_writes == 2 { return Err(Error::Stalled); } } Ok(count) if count <= self.remaining().len() => { self.sent += count; self.progress.written_bytes += count as u64; consecutive_zero_writes = 0; } Ok(_) => return Err(Error::InvalidCount), Err(WriteFault::Interrupted) => continue, Err(error) => return Err(Error::Output(error)), } } Ok(self.progress) } /// Closing consumes the owner. A backend close failure returns it for retry. pub fn close>(mut self, files: &mut B) -> Result { let handle = self.handle.take().expect("owned file handle"); match files.close(handle) { Ok(()) => Ok(self.progress), Err((error, handle)) => { self.handle = Some(handle); Err((error, self)) } } } } pub mod directory { use super::FileError; pub const MAX_ENTRIES: usize = 16; pub const MAX_NAME: usize = 24; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Kind { File, Directory } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Entry { name: [u8; MAX_NAME], length: u8, inode: u32, kind: Kind, } impl Entry { pub fn new(name: &[u8], inode: u32, kind: Kind) -> Result { if name.is_empty() || name.len() > MAX_NAME || inode == 0 || core::str::from_utf8(name).is_err() || name.iter().any(|byte| *byte < 0x20 || *byte == 0x7f || *byte == b'/') { return Err(Error::InvalidEntry); } let mut owned = [0u8; MAX_NAME]; owned[..name.len()].copy_from_slice(name); Ok(Self {name: owned, length: name.len() as u8, inode, kind}) } pub fn name(&self) -> &str { // The constructor validates UTF-8 and fields cannot be mutated. core::str::from_utf8(&self.name[..self.length as usize]) .expect("validated directory name") } pub const fn inode(&self) -> u32 { self.inode } pub const fn kind(&self) -> Kind { self.kind } } impl Default for Entry { fn default() -> Self { Self {name: [0; MAX_NAME], length: 0, inode: 0, kind: Kind::File} } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { InvalidPath, InvalidCapacity, InvalidEntry, DuplicateName, TooManyEntries, Incomplete, Budget, File(FileError), } pub trait ReadOnlyDirectories { type Handle; fn open(&mut self, path: &str) -> Result; fn next(&mut self, handle: &mut Self::Handle) -> Result, FileError>; fn visible(&self, entry: &Entry) -> bool; fn close(&mut self, handle: Self::Handle) -> Result<(), (FileError, Self::Handle)>; } /// Private entries become visible to the caller only after EOF and close. #[derive(Debug)] pub struct Ls { handle: Option, entries: [Entry; N], count: usize, eof: bool, } #[derive(Debug)] pub struct Listing { entries: [Entry; N], count: usize, } impl Listing { pub fn entries(&self) -> &[Entry] { &self.entries[..self.count] } } impl Ls { pub fn open>( backend: &mut B, path: &str, ) -> Result { if N == 0 || N > MAX_ENTRIES { return Err(Error::InvalidCapacity); } if path.is_empty() || path.as_bytes().contains(&0) { return Err(Error::InvalidPath); } let handle = backend.open(path).map_err(Error::File)?; Ok(Self {handle: Some(handle), entries: [Entry::default(); N], count: 0, eof: false}) } pub const fn buffered(&self) -> usize { self.count } pub const fn at_eof(&self) -> bool { self.eof } /// Every read attempt consumes budget. Interrupted reads retain owner. pub fn collect>( &mut self, backend: &mut B, budget: usize, ) -> Result<(), Error> { let mut attempts = 0; while !self.eof { if attempts == budget { return Err(Error::Budget); } attempts += 1; let handle = self.handle.as_mut().expect("owned directory"); match backend.next(handle) { Ok(None) => self.eof = true, Ok(Some(entry)) => { if !backend.visible(&entry) { continue; } if self.entries[..self.count].iter() .any(|prior| prior.name() == entry.name()) { return Err(Error::DuplicateName); } if self.count == N { return Err(Error::TooManyEntries); } self.entries[self.count] = entry; self.count += 1; } Err(FileError::Interrupted) => continue, Err(error) => return Err(Error::File(error)), } } Ok(()) } pub fn finish>( mut self, backend: &mut B, ) -> Result, (Error, Self)> { if !self.eof { return Err((Error::Incomplete, self)); } let handle = self.handle.take().expect("owned directory"); if let Err((error, handle)) = backend.close(handle) { self.handle = Some(handle); return Err((Error::File(error), self)); } Ok(Listing {entries: self.entries, count: self.count}) } /// Cleanup after a read or validation error cannot publish entries. pub fn abort>( mut self, backend: &mut B, ) -> Result<(), (FileError, Self)> { let handle = self.handle.take().expect("owned directory"); if let Err((error, handle)) = backend.close(handle) { self.handle = Some(handle); return Err((error, self)); } Ok(()) } } } pub mod process_view { use super::FileError; pub const MAX_ROWS: usize = 16; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ProcessKey { pub slot: u32, pub generation: u32 } impl ProcessKey { pub const fn new(slot: u32, generation: u32) -> Result { if slot == 0 || generation == 0 { return Err(Error::InvalidRow); } Ok(Self {slot, generation}) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum State { Ready, Running, Waiting, Exited } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Row { pub key: ProcessKey, pub parent: Option, pub state: State, pub elapsed_ticks: u64, } impl Row { pub fn new(key: ProcessKey, parent: Option, state: State, elapsed_ticks: u64) -> Result { if key.slot == 0 || key.generation == 0 { return Err(Error::InvalidRow); } if let Some(parent) = parent { if parent.slot == 0 || parent.generation == 0 || parent == key { return Err(Error::InvalidRow); } } Ok(Self {key, parent, state, elapsed_ticks}) } } impl Default for Row { fn default() -> Self { Self {key: ProcessKey {slot: 0, generation: 0}, parent: None, state: State::Ready, elapsed_ticks: 0} } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Request { ListProcesses, Uptime, RequestAddress(u64) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { Access, InvalidRequest, InvalidCapacity, InvalidRow, DuplicateKey, TooManyRows, Incomplete, Budget, InvalidClock, ClockRegression, File(FileError), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ClockSample { pub boot_id: u64, pub ticks: u64, pub frequency: u64 } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Uptime { pub boot_id: u64, pub seconds: u64, pub nanos: u32 } pub trait ReadOnlyProcessView { type Handle; fn open(&mut self) -> Result; fn next(&mut self, handle: &mut Self::Handle) -> Result, FileError>; fn close(&mut self, handle: Self::Handle) -> Result<(), (FileError, Self::Handle)>; fn clock(&mut self) -> Result; } #[derive(Debug)] pub struct Ps { handle: Option, rows: [Row; N], count: usize, end: bool, } #[derive(Debug)] pub struct Snapshot { rows: [Row; N], count: usize } impl Snapshot { pub fn rows(&self) -> &[Row] { &self.rows[..self.count] } } impl Ps { pub fn open>( backend: &mut B, request: Request, ) -> Result { match request { Request::ListProcesses => {}, Request::RequestAddress(_) => return Err(Error::Access), Request::Uptime => return Err(Error::InvalidRequest), } if N == 0 || N > MAX_ROWS { return Err(Error::InvalidCapacity); } let handle = backend.open().map_err(Error::File)?; Ok(Self {handle: Some(handle), rows: [Row::default(); N], count: 0, end: false}) } pub const fn buffered(&self) -> usize { self.count } pub const fn at_end(&self) -> bool { self.end } pub fn collect>( &mut self, backend: &mut B, budget: usize, ) -> Result<(), Error> { let mut attempts = 0; while !self.end { if attempts == budget { return Err(Error::Budget); } attempts += 1; let handle = self.handle.as_mut().expect("owned process view"); match backend.next(handle) { Ok(None) => self.end = true, Ok(Some(row)) => { if row.key.slot == 0 || row.key.generation == 0 || row.parent.is_some_and(|parent| parent.slot == 0 || parent.generation == 0 || parent == row.key) { return Err(Error::InvalidRow); } if self.rows[..self.count].iter().any(|old| old.key == row.key) { return Err(Error::DuplicateKey); } if self.count == N { return Err(Error::TooManyRows); } self.rows[self.count] = row; self.count += 1; } Err(FileError::Interrupted) => continue, Err(error) => return Err(Error::File(error)), } } Ok(()) } pub fn finish>( mut self, backend: &mut B, ) -> Result, (Error, Self)> { if !self.end { return Err((Error::Incomplete, self)); } let handle = self.handle.take().expect("owned process view"); if let Err((error, handle)) = backend.close(handle) { self.handle = Some(handle); return Err((Error::File(error), self)); } Ok(Snapshot {rows: self.rows, count: self.count}) } pub fn abort>( mut self, backend: &mut B, ) -> Result<(), (FileError, Self)> { let handle = self.handle.take().expect("owned process view"); if let Err((error, handle)) = backend.close(handle) { self.handle = Some(handle); return Err((error, self)); } Ok(()) } } #[derive(Default)] pub struct UptimeTracker { previous: Option } impl UptimeTracker { pub fn observe(&mut self, backend: &mut B, request: Request) -> Result { match request { Request::Uptime => {}, Request::RequestAddress(_) => return Err(Error::Access), Request::ListProcesses => return Err(Error::InvalidRequest), } let sample = backend.clock().map_err(Error::File)?; if sample.boot_id == 0 || sample.frequency == 0 { return Err(Error::InvalidClock); } if self.previous.is_some_and(|old| old.boot_id == sample.boot_id && sample.ticks < old.ticks) { return Err(Error::ClockRegression); } if self.previous.is_some_and(|old| old.boot_id == sample.boot_id && sample.frequency != old.frequency) { return Err(Error::InvalidClock); } let seconds = sample.ticks / sample.frequency; let remainder = sample.ticks % sample.frequency; let nanos = (u128::from(remainder) * 1_000_000_000 / u128::from(sample.frequency)) as u32; self.previous = Some(sample); Ok(Uptime {boot_id: sample.boot_id, seconds, nanos}) } } }