use turkos_os_user_runtime_io::io::{Fault, Writer}; use turkos_userspace::core_tools::{Cat, Error, FileError, ReadOnlyFiles}; use turkos_userspace::core_tools::directory::{ Entry, Error as DirectoryError, Kind, Ls, ReadOnlyDirectories, }; use turkos_userspace::core_tools::process_view::{ ClockSample, Error as ProcessError, ProcessKey, Ps, ReadOnlyProcessView, Request, Row, State, Uptime, UptimeTracker, }; const DISK: &[u8; 2048 * 512] = include_bytes!("fixtures/core-tools.raw"); #[derive(Debug)] struct Handle { bytes: Vec, cursor: usize } struct Files { disk: Vec, live: usize, read_cap: usize, bad_read: bool, fail_close_once: bool, reads: usize, } impl Default for Files { fn default() -> Self { Self {disk: DISK.to_vec(), live: 0, read_cap: 256, bad_read: false, fail_close_once: false, reads: 0} } } impl ReadOnlyFiles for Files { type Handle = Handle; fn open(&mut self, path: &str) -> Result { let block = 512; if &self.disk[16 * block..16 * block + 8] != b"TURKFS\0\0" || u32::from_le_bytes(self.disk[16 * block + 32..16 * block + 36] .try_into().unwrap()) != 5 { return Err(FileError::Io); } let entry = (0..5).map(|index| 17 * block + index * 32) .find(|at| { let len = self.disk[*at + 1] as usize; self.disk[*at] == 1 && len == path.len() && self.disk[*at + 8..*at + 8 + len] == *path.as_bytes() }).ok_or(FileError::NotFound)?; let id = u32::from_le_bytes(self.disk[entry + 4..entry + 8].try_into().unwrap()); if !(1..=5).contains(&id) { return Err(FileError::Io); } let inode = 18 * block + (id as usize - 1) * 32; let recorded = u32::from_le_bytes(self.disk[inode..inode + 4].try_into().unwrap()); let kind = u32::from_le_bytes(self.disk[inode + 4..inode + 8].try_into().unwrap()); let size = u64::from_le_bytes(self.disk[inode + 8..inode + 16].try_into().unwrap()) as usize; let lba = u64::from_le_bytes(self.disk[inode + 16..inode + 24].try_into().unwrap()) as usize; if recorded != id || kind != 0 || size > 1024 || size > 0 && (lba < 19 || lba * block + size > self.disk.len()) { return Err(FileError::Io); } let bytes = if size == 0 { Vec::new() } else { self.disk[lba * block..lba * block + size].to_vec() }; self.live += 1; Ok(Handle {bytes, cursor: 0}) } fn read(&mut self, handle: &mut Handle, output: &mut [u8]) -> Result { self.reads += 1; if self.bad_read { return Err(FileError::BadFd); } let count = output.len().min(self.read_cap) .min(handle.bytes.len() - handle.cursor); output[..count].copy_from_slice(&handle.bytes[handle.cursor..handle.cursor + count]); handle.cursor += count; Ok(count) } fn close(&mut self, handle: Handle) -> Result<(), (FileError, Handle)> { if self.fail_close_once { self.fail_close_once = false; return Err((FileError::Io, handle)); } self.live -= 1; Ok(()) } } struct Output { bytes: Vec, cap: usize, calls: usize, fail_on: Option } impl Default for Output { fn default() -> Self { Self {bytes: Vec::new(), cap: 256, calls: 0, fail_on: None} } } impl Writer for Output { fn write_once(&mut self, bytes: &[u8]) -> Result { self.calls += 1; if self.fail_on == Some(self.calls) { return Err(Fault::Wire); } let count = bytes.len().min(self.cap); self.bytes.extend_from_slice(&bytes[..count]); Ok(count) } } #[test] fn os_core_tools_read_file_positive() { for (path, expected_len) in [("README", 0), ("ONE", 1), ("TAIL", 513)] { let mut files = Files::default(); let disk_before = files.disk.clone(); let mut output = Output {cap: 17, ..Output::default()}; let mut cat = Cat::::open(&mut files, path).unwrap(); assert_eq!(files.live, 1); let progress = cat.pump(&mut files, &mut output, 100).unwrap(); assert!(cat.complete()); assert_eq!(progress.read_bytes, expected_len); assert_eq!(progress.written_bytes, expected_len); assert_eq!(output.bytes.len(), expected_len as usize); assert_eq!(cat.close(&mut files).unwrap(), progress); assert_eq!(files.live, 0); assert_eq!(files.disk, disk_before); if path == "ONE" { assert_eq!(output.bytes, [0x42]); } if path == "TAIL" { assert_eq!(output.bytes, DISK[22 * 512..22 * 512 + 513]); assert_eq!(progress.read_calls, 4); // 256 + 256 + 1 + EOF } } } #[test] fn os_core_tools_read_file_negative() { let mut files = Files::default(); assert!(matches!(Cat::::open(&mut files, "MISSING"), Err(Error::File(FileError::NotFound)))); assert_eq!(files.live, 0); let mut output = Output::default(); let mut cat = Cat::::open(&mut files, "ONE").unwrap(); files.bad_read = true; assert_eq!(cat.pump(&mut files, &mut output, 3), Err(Error::File(FileError::BadFd))); assert!(output.bytes.is_empty()); assert_eq!(cat.progress().written_bytes, 0); assert_eq!(files.live, 1); cat.close(&mut files).unwrap(); assert_eq!(files.live, 0); } #[test] fn short_read_is_data_and_only_zero_read_is_eof() { let mut files = Files {read_cap: 1, ..Files::default()}; let mut output = Output::default(); let mut cat = Cat::::open(&mut files, "ONE").unwrap(); assert_eq!(cat.pump(&mut files, &mut output, 2), Err(Error::Budget)); assert_eq!(output.bytes, [0x42]); assert!(!cat.complete()); assert_eq!(cat.pump(&mut files, &mut output, 2).unwrap().read_calls, 2); assert!(cat.complete()); cat.close(&mut files).unwrap(); assert_eq!(files.live, 0); } #[test] fn injected_write_and_close_errors_keep_owner_for_retry() { let mut files = Files {fail_close_once: true, ..Files::default()}; let mut output = Output {cap: 2, fail_on: Some(2), ..Output::default()}; let mut cat = Cat::::open(&mut files, "TAIL").unwrap(); assert_eq!(cat.pump(&mut files, &mut output, 10), Err(Error::Output(Fault::Wire))); assert_eq!(cat.progress().written_bytes, 2); assert_eq!(files.live, 1); output.fail_on = None; output.cap = 256; assert_eq!(cat.pump(&mut files, &mut output, 10).unwrap().written_bytes, 513); assert_eq!(output.bytes, DISK[22 * 512..22 * 512 + 513]); let (_, cat) = cat.close(&mut files).err().unwrap(); assert_eq!(files.live, 1); cat.close(&mut files).unwrap(); assert_eq!(files.live, 0); } #[derive(Clone, Copy, Debug)] struct DirectoryHandle { path: u8, cursor: usize } struct Directories { allow_child: bool, live: usize, fail_read: bool, fail_close_once: bool, duplicate: bool, } impl Directories { fn root_only() -> Self { Self {allow_child: false, live: 0, fail_read: false, fail_close_once: false, duplicate: false} } } impl ReadOnlyDirectories for Directories { type Handle = DirectoryHandle; fn open(&mut self, path: &str) -> Result { let id = match path { "/" => 0, "/SUB" if self.allow_child => 1, "/SUB" => return Err(FileError::Access), _ => return Err(FileError::NotFound), }; self.live += 1; Ok(DirectoryHandle {path: id, cursor: 0}) } fn next(&mut self, handle: &mut DirectoryHandle) -> Result, FileError> { if self.fail_read { return Err(FileError::BadFd); } let name: Option<&[u8]> = match (handle.path, handle.cursor) { (0, 0) => Some(b"PUBLIC"), (0, 1) if self.duplicate => Some(b"PUBLIC"), (0, 1) => Some(b"HIDDEN"), (1, 0) => Some(b"INNER"), _ => None, }; handle.cursor += 1; name.map(|name| Entry::new(name, handle.cursor as u32, Kind::File) .map_err(|_| FileError::Io)).transpose() } fn visible(&self, entry: &Entry) -> bool { entry.name() != "HIDDEN" } fn close(&mut self, handle: DirectoryHandle) -> Result<(), (FileError, DirectoryHandle)> { if self.fail_close_once { self.fail_close_once = false; return Err((FileError::Io, handle)); } self.live -= 1; Ok(()) } } #[test] fn os_core_tools_directory_positive() { let mut backend = Directories {allow_child: true, ..Directories::root_only()}; let mut root = Ls::::open(&mut backend, "/").unwrap(); root.collect(&mut backend, 16).unwrap(); let root = root.finish(&mut backend).unwrap(); assert_eq!(root.entries().iter().map(Entry::name).collect::>(), ["PUBLIC"]); assert_eq!(backend.live, 0); let mut child = Ls::::open(&mut backend, "/SUB").unwrap(); child.collect(&mut backend, 16).unwrap(); let child = child.finish(&mut backend).unwrap(); assert_eq!(child.entries().iter().map(Entry::name).collect::>(), ["INNER"]); assert_ne!(root.entries(), child.entries()); assert_eq!(backend.live, 0); } #[test] fn os_core_tools_directory_negative() { let mut backend = Directories::root_only(); assert!(matches!(Ls::::open(&mut backend, "/SUB"), Err(DirectoryError::File(FileError::Access)))); assert_eq!(backend.live, 0); let mut root = Ls::::open(&mut backend, "/").unwrap(); backend.fail_read = true; assert_eq!(root.collect(&mut backend, 2), Err(DirectoryError::File(FileError::BadFd))); assert_eq!(root.buffered(), 0); assert_eq!(backend.live, 1); root.abort(&mut backend).unwrap(); assert_eq!(backend.live, 0); backend.fail_read = false; let mut root = Ls::::open(&mut backend, "/").unwrap(); root.collect(&mut backend, 3).unwrap(); assert_eq!(root.finish(&mut backend).unwrap().entries()[0].name(), "PUBLIC"); assert_eq!(backend.live, 0); } #[test] fn directory_duplicate_and_close_error_keep_owner_without_output() { let mut backend = Directories {duplicate: true, fail_close_once: true, ..Directories::root_only()}; let mut list = Ls::::open(&mut backend, "/").unwrap(); assert_eq!(list.collect(&mut backend, 3), Err(DirectoryError::DuplicateName)); assert_eq!(list.buffered(), 1); assert_eq!(backend.live, 1); let (error, list) = list.abort(&mut backend).err().unwrap(); assert_eq!(error, FileError::Io); assert_eq!(backend.live, 1); list.abort(&mut backend).unwrap(); assert_eq!(backend.live, 0); } #[test] fn directory_publish_waits_for_successful_close() { let mut backend = Directories {fail_close_once: true, ..Directories::root_only()}; let mut list = Ls::::open(&mut backend, "/").unwrap(); list.collect(&mut backend, 3).unwrap(); let (error, list) = list.finish(&mut backend).err().unwrap(); assert_eq!(error, DirectoryError::File(FileError::Io)); assert_eq!(list.buffered(), 1); assert_eq!(backend.live, 1); let output = list.finish(&mut backend).unwrap(); assert_eq!(output.entries()[0].name(), "PUBLIC"); assert_eq!(backend.live, 0); } const LS_DISK: &[u8; 2048 * 512] = include_bytes!("fixtures/core-tools-ls.raw"); #[derive(Clone, Copy, Debug)] struct DiskDirectoryHandle { lba: usize, count: usize, cursor: usize } struct DiskDirectories { disk: Vec, allow_child: bool, live: usize, } impl DiskDirectories { fn new(allow_child: bool) -> Self { Self {disk: LS_DISK.to_vec(), allow_child, live: 0} } } impl ReadOnlyDirectories for DiskDirectories { type Handle = DiskDirectoryHandle; fn open(&mut self, path: &str) -> Result { let block = 512; if &self.disk[16 * block..16 * block + 8] != b"TURKFS\0\0" { return Err(FileError::Io); } let root_count = self.disk[16 * block + 32] as usize; let (lba, count) = match path { "/" if root_count == 6 => (17, root_count), "/SUB" if !self.allow_child => return Err(FileError::Access), "/SUB" => { let root = 17 * block + 5 * 32; let inode = 18 * block + 5 * 32; if self.disk[root] != 1 || &self.disk[root + 8..root + 11] != b"SUB" || self.disk[inode + 4] != 2 { return Err(FileError::Io); } let lba = u64::from_le_bytes( self.disk[inode + 16..inode + 24].try_into().unwrap()) as usize; let count = self.disk[inode + 24] as usize; if lba != 25 || count != 2 { return Err(FileError::Io); } (lba, count) } _ => return Err(FileError::NotFound), }; self.live += 1; Ok(DiskDirectoryHandle {lba, count, cursor: 0}) } fn next(&mut self, handle: &mut Self::Handle) -> Result, FileError> { if handle.cursor == handle.count { return Ok(None); } if handle.cursor > handle.count || handle.count > 16 { return Err(FileError::Io); } let at = handle.lba * 512 + handle.cursor * 32; let len = self.disk[at + 1] as usize; if self.disk[at] != 1 || len == 0 || len > 24 { return Err(FileError::Io); } let id = u32::from_le_bytes(self.disk[at + 4..at + 8].try_into().unwrap()); let inode = 18 * 512 + (id.checked_sub(1).ok_or(FileError::Io)? as usize) * 32; if inode + 8 > self.disk.len() || u32::from_le_bytes(self.disk[inode..inode + 4].try_into().unwrap()) != id { return Err(FileError::Io); } let kind = match u32::from_le_bytes( self.disk[inode + 4..inode + 8].try_into().unwrap()) { 0 => Kind::File, 2 => Kind::Directory, _ => return Err(FileError::Io), }; let entry = Entry::new(&self.disk[at + 8..at + 8 + len], id, kind) .map_err(|_| FileError::Io)?; handle.cursor += 1; Ok(Some(entry)) } fn visible(&self, entry: &Entry) -> bool { entry.name() != "HIDDEN" } fn close(&mut self, _handle: Self::Handle) -> Result<(), (FileError, Self::Handle)> { self.live -= 1; Ok(()) } } #[test] fn os_core_tools_directory_disk_positive() { let mut backend = DiskDirectories::new(true); let disk_before = backend.disk.clone(); let mut root = Ls::::open(&mut backend, "/").unwrap(); root.collect(&mut backend, 16).unwrap(); let root = root.finish(&mut backend).unwrap(); assert_eq!(root.entries().iter().map(Entry::name).collect::>(), ["README", "KERNEL", "MULTI", "TAIL", "ONE", "SUB"]); assert_eq!(root.entries()[5].kind(), Kind::Directory); let mut child = Ls::::open(&mut backend, "/SUB").unwrap(); child.collect(&mut backend, 16).unwrap(); let child = child.finish(&mut backend).unwrap(); assert_eq!(child.entries().iter().map(Entry::name).collect::>(), ["INNER"]); assert_eq!(backend.live, 0); assert_eq!(backend.disk, disk_before); } #[test] fn os_core_tools_directory_disk_negative_then_healthy() { let mut backend = DiskDirectories::new(false); let disk_before = backend.disk.clone(); assert!(matches!( Ls::::open(&mut backend, "/SUB"), Err(DirectoryError::File(FileError::Access)))); assert_eq!(backend.live, 0); let mut root = Ls::::open(&mut backend, "/").unwrap(); root.collect(&mut backend, 16).unwrap(); assert_eq!(root.finish(&mut backend).unwrap().entries().len(), 6); assert_eq!(backend.live, 0); assert_eq!(backend.disk, disk_before); } struct ProcessBackend { rows: Vec, samples: Vec, live: usize, opens: usize, fail_read_once: bool, fail_close_once: bool, } const PROCESS_FIXTURE: &[u8; 144] = include_bytes!("fixtures/core-tools-process.bin"); fn process_u32(at: usize) -> u32 { u32::from_le_bytes(PROCESS_FIXTURE[at..at + 4].try_into().unwrap()) } fn process_u64(at: usize) -> u64 { u64::from_le_bytes(PROCESS_FIXTURE[at..at + 8].try_into().unwrap()) } impl ProcessBackend { fn three() -> Self { assert_eq!(&PROCESS_FIXTURE[..8], b"TKPSV1\0\0"); assert_eq!(process_u32(40), 3); let rows = (0..3).map(|index| { let at = 48 + index * 32; let key = ProcessKey::new(process_u32(at), process_u32(at + 4)).unwrap(); let parent = match process_u32(at + 8) { 0 => None, slot => Some(ProcessKey::new(slot, process_u32(at + 12)).unwrap()), }; let state = match process_u32(at + 16) { 0 => State::Ready, 1 => State::Running, 2 => State::Waiting, 3 => State::Exited, _ => panic!("fixture state"), }; Row::new(key, parent, state, process_u64(at + 24)).unwrap() }).collect(); Self {rows, samples: vec![ ClockSample {boot_id: process_u64(8), ticks: process_u64(24), frequency: process_u64(16)}, ClockSample {boot_id: process_u64(8), ticks: process_u64(32), frequency: process_u64(16)}], live: 0, opens: 0, fail_read_once: false, fail_close_once: false} } } impl ReadOnlyProcessView for ProcessBackend { type Handle = usize; fn open(&mut self) -> Result { self.opens += 1; self.live += 1; Ok(0) } fn next(&mut self, handle: &mut usize) -> Result, FileError> { if self.fail_read_once { self.fail_read_once = false; return Err(FileError::Io); } if *handle >= self.rows.len() { return Ok(None); } let row = self.rows[*handle]; *handle += 1; Ok(Some(row)) } fn close(&mut self, handle: usize) -> Result<(), (FileError, usize)> { if self.fail_close_once { self.fail_close_once = false; return Err((FileError::Io, handle)); } self.live -= 1; Ok(()) } fn clock(&mut self) -> Result { if self.samples.is_empty() { return Err(FileError::Io); } Ok(self.samples.remove(0)) } } #[test] fn os_core_tools_process_view_positive() { let mut backend = ProcessBackend::three(); let mut ps = Ps::::open(&mut backend, Request::ListProcesses).unwrap(); assert_eq!(backend.live, 1); ps.collect(&mut backend, 4).unwrap(); assert!(ps.at_end()); let snapshot = ps.finish(&mut backend).unwrap(); assert_eq!(backend.live, 0); assert_eq!(snapshot.rows().len(), 3); assert_eq!(snapshot.rows().iter().map(|row| row.key.generation) .collect::>(), [1, 7, 2]); assert_eq!(snapshot.rows()[2].parent, Some(snapshot.rows()[1].key)); assert_eq!(snapshot.rows()[0].state, State::Running); let mut tracker = UptimeTracker::default(); assert_eq!(tracker.observe(&mut backend, Request::Uptime), Ok(Uptime {boot_id: 9, seconds: 10, nanos: 0})); assert_eq!(tracker.observe(&mut backend, Request::Uptime), Ok(Uptime {boot_id: 9, seconds: 11, nanos: 500_000_000})); assert_eq!(backend.opens, 1); } #[test] fn os_core_tools_process_view_negative() { let mut backend = ProcessBackend::three(); assert!(matches!(Ps::::open(&mut backend, Request::RequestAddress(0xffff_0000_0000_1000)), Err(ProcessError::Access))); let mut tracker = UptimeTracker::default(); assert_eq!(tracker.observe(&mut backend, Request::RequestAddress(0xffff_0000_0000_1000)), Err(ProcessError::Access)); assert_eq!((backend.opens, backend.live), (0, 0)); let mut ps = Ps::::open(&mut backend, Request::ListProcesses).unwrap(); ps.collect(&mut backend, 4).unwrap(); assert_eq!(ps.finish(&mut backend).unwrap().rows().len(), 3); assert_eq!(tracker.observe(&mut backend, Request::Uptime).unwrap().seconds, 10); assert_eq!((backend.opens, backend.live), (1, 0)); } #[test] fn process_view_read_and_close_errors_keep_owner() { let mut backend = ProcessBackend::three(); backend.fail_read_once = true; let mut ps = Ps::::open(&mut backend, Request::ListProcesses).unwrap(); assert_eq!(ps.collect(&mut backend, 4), Err(ProcessError::File(FileError::Io))); assert_eq!(backend.live, 1); ps.abort(&mut backend).unwrap(); assert_eq!(backend.live, 0); let mut ps = Ps::::open(&mut backend, Request::ListProcesses).unwrap(); ps.collect(&mut backend, 4).unwrap(); backend.fail_close_once = true; let (error, ps) = ps.finish(&mut backend).unwrap_err(); assert_eq!(error, ProcessError::File(FileError::Io)); assert_eq!(backend.live, 1); assert_eq!(ps.finish(&mut backend).unwrap().rows().len(), 3); assert_eq!(backend.live, 0); } #[test] fn process_view_duplicate_and_clock_regression_do_not_publish() { let mut backend = ProcessBackend::three(); backend.rows[2] = backend.rows[1]; let mut ps = Ps::::open(&mut backend, Request::ListProcesses).unwrap(); assert_eq!(ps.collect(&mut backend, 4), Err(ProcessError::DuplicateKey)); assert_eq!(backend.live, 1); ps.abort(&mut backend).unwrap(); assert_eq!(backend.live, 0); let mut tracker = UptimeTracker::default(); tracker.observe(&mut backend, Request::Uptime).unwrap(); backend.samples[0].ticks = 99; assert_eq!(tracker.observe(&mut backend, Request::Uptime), Err(ProcessError::ClockRegression)); backend.samples.push(ClockSample {boot_id: 10, ticks: 1, frequency: 2}); assert_eq!(tracker.observe(&mut backend, Request::Uptime), Ok(Uptime {boot_id: 10, seconds: 0, nanos: 500_000_000})); } #[test] fn process_view_capacity_budget_and_generation_boundaries() { let mut backend = ProcessBackend::three(); assert!(matches!(Ps::::open(&mut backend, Request::Uptime), Err(ProcessError::InvalidRequest))); assert!(matches!(Ps::::open(&mut backend, Request::ListProcesses), Err(ProcessError::InvalidCapacity))); assert!(matches!(Ps::::open(&mut backend, Request::ListProcesses), Err(ProcessError::InvalidCapacity))); assert_eq!(backend.opens, 0); assert_eq!(ProcessKey::new(0, 1), Err(ProcessError::InvalidRow)); assert_eq!(ProcessKey::new(1, 0), Err(ProcessError::InvalidRow)); let mut ps = Ps::::open(&mut backend, Request::ListProcesses).unwrap(); assert_eq!(ps.collect(&mut backend, 2), Err(ProcessError::Budget)); assert_eq!((ps.buffered(), backend.live), (2, 1)); ps.collect(&mut backend, 2).unwrap(); assert_eq!(ps.finish(&mut backend).unwrap().rows().len(), 3); assert_eq!(backend.live, 0); let mut ps = Ps::::open(&mut backend, Request::ListProcesses).unwrap(); assert_eq!(ps.collect(&mut backend, 4), Err(ProcessError::TooManyRows)); assert_eq!(backend.live, 1); ps.abort(&mut backend).unwrap(); assert_eq!(backend.live, 0); } #[test] fn process_view_frequency_change_is_rejected_without_poisoning_clock() { let mut backend = ProcessBackend::three(); backend.samples = vec![ ClockSample {boot_id: 9, ticks: 100, frequency: 10}, ClockSample {boot_id: 9, ticks: 110, frequency: 20}, ClockSample {boot_id: 9, ticks: 110, frequency: 10}, ]; let mut tracker = UptimeTracker::default(); assert_eq!(tracker.observe(&mut backend, Request::Uptime).unwrap().seconds, 10); assert_eq!(tracker.observe(&mut backend, Request::Uptime), Err(ProcessError::InvalidClock)); assert_eq!(tracker.observe(&mut backend, Request::Uptime).unwrap().seconds, 11); assert_eq!(backend.opens, 0); }