//! Experimental, read-only TurkFS disk layout. K361 establishes the version //! field, K362 checks the format signature, K363 checks geometry, and K364 //! reads a bounded root directory; K367–K369 check known data-block shapes, //! and K373 rejects overlap among their data extents. //! General file and path traversal remain open. use sha2::{Digest, Sha256}; pub const SUPERBLOCK_LBA: u64 = 16; pub const BLOCK_BYTES: usize = 512; pub const MAGIC: [u8; 8] = *b"TURKFS\0\0"; pub const FORMAT_VERSION: u32 = 1; pub const ROOT_ENTRY_BYTES: usize = 32; pub const ROOT_NAME_BYTES: usize = 24; pub const MAX_ROOT_ENTRIES: usize = BLOCK_BYTES / ROOT_ENTRY_BYTES; pub const KERNEL_BLOCK_SHA256: [u8; 32] = [ 0x8e, 0x6d, 0x10, 0xd6, 0xc9, 0x1d, 0xba, 0x67, 0xb2, 0x87, 0x6e, 0xc3, 0xc8, 0x1f, 0xfd, 0x7f, 0xf7, 0x6a, 0xd0, 0x9c, 0xca, 0xbf, 0x8c, 0xf7, 0x9c, 0xb4, 0x1d, 0x87, 0x9b, 0x0e, 0xd2, 0x26, ]; /// K380 hashes exactly the logical file bytes, not unused tail-sector bytes. pub fn file_sha256(logical_bytes: &[u8]) -> [u8; 32] { Sha256::digest(logical_bytes).into() } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { WrongBlockSize, UnsupportedVersion, WrongSignature, InvalidBlockGeometry, ArithmeticOverflow, CapacityExceeded, InvalidRootDescriptor, InvalidRootEntry, InvalidUtf8Name, InvalidInodeNumber, InvalidInodeTable, OverlappingExtent, InvalidChildDirectory, DirectoryCycle, InvalidPath, PathEscapesRoot, NotEmptyFile, InvalidSingleBlockFile, WrongFileHash, InvalidMultiBlockFile, WrongBlockOrder, InvalidPartialFile, WrongPartialData, OutputTooSmall, InvalidSeek, InvalidOpenFile, ReadOnlyFilesystem, InvalidBitmap, UnallocatedExtent, OrphanAllocatedBlock, NoSpace, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BlockGeometry { pub block_bytes: u32, pub block_count: u64, pub span_bytes: u64, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct RootEntry { pub inode: u32, pub name_len: u8, pub name: [u8; ROOT_NAME_BYTES], } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct RootDirectory { pub lba: u64, pub count: usize, pub entries: [RootEntry; MAX_ROOT_ENTRIES], } impl RootDirectory { pub fn find(&self, name: &[u8]) -> Option { self.entries[..self.count] .iter() .find(|entry| { entry.name_len as usize == name.len() && &entry.name[..name.len()] == name }) .copied() } pub fn contains(&self, name: &[u8]) -> bool { self.find(name).is_some() } } /// The version is a little-endian u32 at byte 8 of the 512-byte superblock. /// This deliberately does not yet accept the disk as a mounted filesystem. pub fn require_format_version(block: &[u8]) -> Result { if block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let version = u32::from_le_bytes([block[8], block[9], block[10], block[11]]); if version != FORMAT_VERSION { return Err(Error::UnsupportedVersion); } Ok(version) } /// K362: a matching version alone must not turn arbitrary disk bytes into a /// recognized TurkFS superblock. This is a format discriminator, not an /// authenticity check or a filesystem mount operation. pub fn require_superblock_signature(block: &[u8]) -> Result<(), Error> { if block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } if block[..MAGIC.len()] != MAGIC { return Err(Error::WrongSignature); } Ok(()) } /// K363: bytes 12..16 are the little-endian logical block size and bytes /// 16..24 are the block count. A format claim cannot exceed the real device. pub fn require_block_geometry(block: &[u8], disk_bytes: u64) -> Result { if block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let block_bytes = u32::from_le_bytes([block[12], block[13], block[14], block[15]]); let block_count = u64::from_le_bytes([ block[16], block[17], block[18], block[19], block[20], block[21], block[22], block[23], ]); if block_bytes != BLOCK_BYTES as u32 || block_count == 0 { return Err(Error::InvalidBlockGeometry); } let span_bytes = block_count .checked_mul(u64::from(block_bytes)) .ok_or(Error::ArithmeticOverflow)?; if span_bytes > disk_bytes { return Err(Error::CapacityExceeded); } Ok(BlockGeometry { block_bytes, block_count, span_bytes, }) } /// K364: root LBA and entry count occupy superblock bytes 24..36. Only one /// directory block is defined in v1; LBA16 itself cannot be the root block. pub fn root_directory_location( block: &[u8], geometry: BlockGeometry, ) -> Result<(u64, usize), Error> { if block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let lba = u64::from_le_bytes([ block[24], block[25], block[26], block[27], block[28], block[29], block[30], block[31], ]); let count = u32::from_le_bytes([block[32], block[33], block[34], block[35]]) as usize; if lba <= SUPERBLOCK_LBA || lba >= geometry.block_count || count == 0 || count > MAX_ROOT_ENTRIES { return Err(Error::InvalidRootDescriptor); } Ok((lba, count)) } /// K365: names are raw bytes at this layer. The length must fit the fixed /// 24-byte slot before any slice is formed or copied. pub fn checked_root_name_len(raw: u8) -> Result { let length = usize::from(raw); if length == 0 || length > ROOT_NAME_BYTES { return Err(Error::InvalidRootEntry); } Ok(length) } /// Each 32-byte root entry has a present byte, name length, inode number and /// up to 24 raw name bytes. UTF-8/path semantics are separate later gates. pub fn parse_root_directory(lba: u64, count: usize, block: &[u8]) -> Result { if block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } if count == 0 || count > MAX_ROOT_ENTRIES { return Err(Error::InvalidRootDescriptor); } let mut entries = [RootEntry::default(); MAX_ROOT_ENTRIES]; for (index, entry) in entries[..count].iter_mut().enumerate() { let offset = index * ROOT_ENTRY_BYTES; let name_len = checked_root_name_len(block[offset + 1])?; if block[offset] != 1 { return Err(Error::InvalidRootEntry); } let inode = u32::from_le_bytes([ block[offset + 4], block[offset + 5], block[offset + 6], block[offset + 7], ]); if inode == 0 { return Err(Error::InvalidRootEntry); } if inode as usize > MAX_ROOT_ENTRIES { return Err(Error::InvalidInodeNumber); } entry.inode = inode; entry.name_len = name_len as u8; entry .name .copy_from_slice(&block[offset + 8..offset + ROOT_ENTRY_BYTES]); } Ok(RootDirectory { lba, count, entries, }) } /// K377 keeps the raw on-disk name parser separate from the UTF-8 policy. /// Check both the public count and each length before forming a byte slice; /// malformed names are classified without formatting or panicking. pub fn require_utf8_names(directory: &RootDirectory) -> Result { if directory.count == 0 || directory.count > MAX_ROOT_ENTRIES { return Err(Error::InvalidRootDescriptor); } for entry in &directory.entries[..directory.count] { let length = checked_root_name_len(entry.name_len)?; if core::str::from_utf8(&entry.name[..length]).is_err() { return Err(Error::InvalidUtf8Name); } } Ok(directory.count) } /// K366: one fixed 512-byte inode table at superblock bytes 36..44. pub fn inode_table_location( block: &[u8], geometry: BlockGeometry, root_lba: u64, ) -> Result { if block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let lba = u64::from_le_bytes([ block[36], block[37], block[38], block[39], block[40], block[41], block[42], block[43], ]); if lba <= root_lba || lba >= geometry.block_count { return Err(Error::InvalidInodeTable); } Ok(lba) } /// Return zero bytes for a named zero-length file without reading a data LBA. pub fn read_known_empty_file( root: &RootDirectory, inode_block: &[u8], name: &[u8], ) -> Result { if inode_block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let entry = root.find(name).ok_or(Error::InvalidRootEntry)?; let slot = usize::try_from(entry.inode.checked_sub(1).ok_or(Error::InvalidInodeTable)?) .map_err(|_| Error::InvalidInodeTable)?; if slot >= MAX_ROOT_ENTRIES { return Err(Error::InvalidInodeTable); } let offset = slot * ROOT_ENTRY_BYTES; let recorded_id = u32::from_le_bytes([ inode_block[offset], inode_block[offset + 1], inode_block[offset + 2], inode_block[offset + 3], ]); let size = u64::from_le_bytes([ inode_block[offset + 8], inode_block[offset + 9], inode_block[offset + 10], inode_block[offset + 11], inode_block[offset + 12], inode_block[offset + 13], inode_block[offset + 14], inode_block[offset + 15], ]); let data_lba = u64::from_le_bytes([ inode_block[offset + 16], inode_block[offset + 17], inode_block[offset + 18], inode_block[offset + 19], inode_block[offset + 20], inode_block[offset + 21], inode_block[offset + 22], inode_block[offset + 23], ]); if recorded_id != entry.inode { return Err(Error::InvalidInodeTable); } if size != 0 || data_lba != 0 { return Err(Error::NotEmptyFile); } Ok(0) } /// K367: only one named 512-byte file, in the single fixed inode table. /// Validate the on-disk pointer before submitting a fifth device request. pub fn known_single_block_lba( root: &RootDirectory, inode_block: &[u8], name: &[u8], geometry: BlockGeometry, inode_lba: u64, ) -> Result { if inode_block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let entry = root.find(name).ok_or(Error::InvalidRootEntry)?; let slot = usize::try_from(entry.inode.checked_sub(1).ok_or(Error::InvalidInodeTable)?) .map_err(|_| Error::InvalidInodeTable)?; if slot >= MAX_ROOT_ENTRIES { return Err(Error::InvalidInodeTable); } let offset = slot * ROOT_ENTRY_BYTES; let recorded_id = u32::from_le_bytes(inode_block[offset..offset + 4].try_into().unwrap()); if recorded_id != entry.inode { return Err(Error::InvalidInodeTable); } let size = u64::from_le_bytes(inode_block[offset + 8..offset + 16].try_into().unwrap()); let data_lba = u64::from_le_bytes(inode_block[offset + 16..offset + 24].try_into().unwrap()); if size != BLOCK_BYTES as u64 || data_lba <= inode_lba || data_lba >= geometry.block_count { return Err(Error::InvalidSingleBlockFile); } Ok(data_lba) } /// K411: resolve one named, non-empty regular file without trusting a disk /// pointer or size until both have been checked against the device geometry. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SingleBlockFile { pub lba: u64, pub size: usize, } pub fn bounded_single_block_file( root: &RootDirectory, inode_block: &[u8], name: &[u8], geometry: BlockGeometry, inode_lba: u64, ) -> Result { if inode_block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let entry = root.find(name).ok_or(Error::InvalidRootEntry)?; let slot = usize::try_from(entry.inode.checked_sub(1).ok_or(Error::InvalidInodeTable)?) .map_err(|_| Error::InvalidInodeTable)?; if slot >= MAX_ROOT_ENTRIES { return Err(Error::InvalidInodeTable); } let offset = slot * ROOT_ENTRY_BYTES; let recorded_id = u32::from_le_bytes(inode_block[offset..offset + 4].try_into().unwrap()); let kind = u32::from_le_bytes(inode_block[offset + 4..offset + 8].try_into().unwrap()); if recorded_id != entry.inode || kind != 0 { return Err(Error::InvalidInodeTable); } let size = u64::from_le_bytes(inode_block[offset + 8..offset + 16].try_into().unwrap()); let data_lba = u64::from_le_bytes(inode_block[offset + 16..offset + 24].try_into().unwrap()); if size == 0 || size > BLOCK_BYTES as u64 || data_lba <= inode_lba || data_lba >= geometry.block_count || data_lba == root.lba { return Err(Error::InvalidSingleBlockFile); } Ok(SingleBlockFile { lba: data_lba, size: size as usize, }) } pub fn verify_known_single_block_data(block: &[u8]) -> Result<[u8; 32], Error> { let sector: &[u8; BLOCK_BYTES] = block.try_into().map_err(|_| Error::WrongBlockSize)?; let digest = crate::virtio_ring::sector_sha256(sector); if digest != KERNEL_BLOCK_SHA256 { return Err(Error::WrongFileHash); } Ok(digest) } /// K368 preflight only: the live two-sector I/O and on-disk negative fixtures /// are separate acceptance gates, so this function alone is not a PASS. pub fn known_two_block_lba( root: &RootDirectory, inode_block: &[u8], name: &[u8], geometry: BlockGeometry, inode_lba: u64, ) -> Result { if inode_block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let entry = root.find(name).ok_or(Error::InvalidRootEntry)?; let slot = usize::try_from(entry.inode.checked_sub(1).ok_or(Error::InvalidInodeTable)?) .map_err(|_| Error::InvalidInodeTable)?; if slot >= MAX_ROOT_ENTRIES { return Err(Error::InvalidInodeTable); } let offset = slot * ROOT_ENTRY_BYTES; let recorded_id = u32::from_le_bytes(inode_block[offset..offset + 4].try_into().unwrap()); if recorded_id != entry.inode { return Err(Error::InvalidInodeTable); } let size = u64::from_le_bytes(inode_block[offset + 8..offset + 16].try_into().unwrap()); let data_lba = u64::from_le_bytes(inode_block[offset + 16..offset + 24].try_into().unwrap()); if size != (2 * BLOCK_BYTES) as u64 || data_lba <= inode_lba || data_lba .checked_add(2) .filter(|end| *end <= geometry.block_count) .is_none() { return Err(Error::InvalidMultiBlockFile); } Ok(data_lba) } pub fn verify_known_two_block_order(data: &[u8]) -> Result<(), Error> { if data.len() != 2 * BLOCK_BYTES { return Err(Error::WrongBlockSize); } for index in 0..BLOCK_BYTES { if data[index] != (index as u8 ^ 0x3c) || data[BLOCK_BYTES + index] != (index as u8 ^ 0xc3) { return Err(Error::WrongBlockOrder); } } Ok(()) } /// K369: a two-sector extent may represent only 513 logical file bytes. /// Sector capacity is not the byte count returned to the caller. pub fn known_partial_file_lba( root: &RootDirectory, inode_block: &[u8], name: &[u8], geometry: BlockGeometry, inode_lba: u64, ) -> Result { if inode_block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let entry = root.find(name).ok_or(Error::InvalidRootEntry)?; let slot = usize::try_from(entry.inode.checked_sub(1).ok_or(Error::InvalidInodeTable)?) .map_err(|_| Error::InvalidInodeTable)?; if slot >= MAX_ROOT_ENTRIES { return Err(Error::InvalidInodeTable); } let offset = slot * ROOT_ENTRY_BYTES; let recorded_id = u32::from_le_bytes(inode_block[offset..offset + 4].try_into().unwrap()); if recorded_id != entry.inode { return Err(Error::InvalidInodeTable); } let size = u64::from_le_bytes(inode_block[offset + 8..offset + 16].try_into().unwrap()); let data_lba = u64::from_le_bytes(inode_block[offset + 16..offset + 24].try_into().unwrap()); if size != (BLOCK_BYTES + 1) as u64 || data_lba <= inode_lba || data_lba .checked_add(2) .filter(|end| *end <= geometry.block_count) .is_none() { return Err(Error::InvalidPartialFile); } Ok(data_lba) } /// K373: the three currently supported named data extents must have one /// owner per logical block. Endpoints are exclusive, so adjacent files pass. /// The caller first validates each inode's size and disk bounds. pub fn require_distinct_known_extents( kernel_lba: u64, multi_lba: u64, tail_lba: u64, ) -> Result<(), Error> { let extents = [(kernel_lba, 1u64), (multi_lba, 2), (tail_lba, 2)]; for first in 0..extents.len() { let first_end = extents[first] .0 .checked_add(extents[first].1) .ok_or(Error::ArithmeticOverflow)?; for second in first + 1..extents.len() { let second_end = extents[second] .0 .checked_add(extents[second].1) .ok_or(Error::ArithmeticOverflow)?; if extents[first].0 < second_end && extents[second].0 < first_end { return Err(Error::OverlappingExtent); } } } Ok(()) } /// K395: one 512-byte allocation map for this fixed 2048-sector fixture. /// Metadata 0..=18 and the bitmap's own sector 26 are reserved. Only the /// three known files may own data sectors; an extra set bit is not reclaimed. pub fn validate_known_allocation_bitmap( bitmap: &[u8], block_count: u64, kernel_lba: u64, multi_lba: u64, tail_lba: u64, ) -> Result<(), Error> { if bitmap.len() != BLOCK_BYTES || block_count != 2048 { return Err(Error::InvalidBitmap); } require_distinct_known_extents(kernel_lba, multi_lba, tail_lba)?; for (start, length) in [(kernel_lba, 1), (multi_lba, 2), (tail_lba, 2)] { if start < 19 || start.checked_add(length).ok_or(Error::ArithmeticOverflow)? > block_count || (start <= 26 && 26 < start + length) { return Err(Error::InvalidBitmap); } } for lba in 0..4096u64 { let allocated = bitmap[(lba / 8) as usize] & (1 << (lba % 8)) != 0; let expected = lba <= 18 || lba == 26 || lba == kernel_lba || (multi_lba..multi_lba + 2).contains(&lba) || (tail_lba..tail_lba + 2).contains(&lba); if allocated != expected { return Err(if expected { Error::UnallocatedExtent } else { Error::OrphanAllocatedBlock }); } } Ok(()) } /// K396: local allocation planning only. The caller must still provide a /// validated ownership map and an atomic on-disk update protocol before any /// chosen block can become a persistent file extent. pub fn reserve_first_free_block( bitmap: &mut [u8], first_data_lba: u64, block_count: u64, ) -> Result { if bitmap.len() != BLOCK_BYTES || first_data_lba >= block_count || block_count > 4096 { return Err(Error::InvalidBitmap); } for lba in first_data_lba..block_count { let byte = &mut bitmap[(lba / 8) as usize]; let bit = 1u8 << (lba % 8); if *byte & bit == 0 { *byte |= bit; return Ok(lba); } } Err(Error::NoSpace) } /// K374: one additional, read-only 512-byte directory block in this fixture. /// The first four known files own LBA19..23; a child directory must not alias /// their data or the superblock/root/inode metadata. pub fn known_child_directory_descriptor( root: &RootDirectory, inode_block: &[u8], name: &[u8], geometry: BlockGeometry, ) -> Result<(u64, usize), Error> { if inode_block.len() != BLOCK_BYTES { return Err(Error::WrongBlockSize); } let entry = root.find(name).ok_or(Error::InvalidRootEntry)?; let slot = usize::try_from(entry.inode.checked_sub(1).ok_or(Error::InvalidInodeTable)?) .map_err(|_| Error::InvalidInodeTable)?; if slot >= MAX_ROOT_ENTRIES { return Err(Error::InvalidInodeTable); } let offset = slot * ROOT_ENTRY_BYTES; let recorded_id = u32::from_le_bytes(inode_block[offset..offset + 4].try_into().unwrap()); let kind = u32::from_le_bytes(inode_block[offset + 4..offset + 8].try_into().unwrap()); let size = u64::from_le_bytes(inode_block[offset + 8..offset + 16].try_into().unwrap()); let lba = u64::from_le_bytes(inode_block[offset + 16..offset + 24].try_into().unwrap()); let count = u32::from_le_bytes(inode_block[offset + 24..offset + 28].try_into().unwrap()) as usize; if recorded_id != entry.inode || kind != 2 || size != BLOCK_BYTES as u64 || lba < 24 || lba >= geometry.block_count || count == 0 || count > MAX_ROOT_ENTRIES { return Err(Error::InvalidChildDirectory); } Ok((lba, count)) } /// Only one child directory block is loaded. A child pointing back at its /// own directory inode is rejected before another lookup or disk request. pub fn resolve_known_child_entry( root: &RootDirectory, child: &RootDirectory, directory_name: &[u8], entry_name: &[u8], ) -> Result { let directory = root.find(directory_name).ok_or(Error::InvalidRootEntry)?; let entry = child.find(entry_name).ok_or(Error::InvalidRootEntry)?; if entry.inode == directory.inode { return Err(Error::DirectoryCycle); } Ok(entry.inode) } /// K375: bounded dot/dot-dot semantics for the one loaded SUB directory. /// A parent step from root is rejected. This does not yet accept absolute /// paths or provide a general multi-directory path service. pub fn resolve_known_dot_path( root: &RootDirectory, child: &RootDirectory, path: &[u8], ) -> Result { resolve_known_components(root, child, path, false) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WorkingDirectory { Root, Child, } /// K376: one leading slash fixes the start at root regardless of cwd; /// otherwise the caller's known root/child working directory is used. pub fn resolve_known_scoped_path( root: &RootDirectory, child: &RootDirectory, cwd: WorkingDirectory, path: &[u8], ) -> Result { if path.is_empty() || path.len() > 128 { return Err(Error::InvalidPath); } if path[0] == b'/' { resolve_known_components(root, child, &path[1..], false) } else { resolve_known_components(root, child, path, cwd == WorkingDirectory::Child) } } fn resolve_known_components( root: &RootDirectory, child: &RootDirectory, path: &[u8], starts_in_child: bool, ) -> Result { if path.is_empty() || path.len() > 128 { return Err(Error::InvalidPath); } if path .split(|byte| *byte == b'/') .any(|component| component.is_empty()) { return Err(Error::InvalidPath); } let child_inode = root.find(b"SUB").ok_or(Error::InvalidRootEntry)?.inode; let mut in_child = starts_in_child; let mut parts = path.split(|byte| *byte == b'/').peekable(); let mut component_count = 0; while let Some(component) = parts.next() { component_count += 1; if component_count > 8 || component.is_empty() { return Err(Error::InvalidPath); } if component == b"." { continue; } if component == b".." { if !in_child { return Err(Error::PathEscapesRoot); } in_child = false; continue; } let entry = if in_child { child } else { root } .find(component) .ok_or(Error::InvalidRootEntry)?; if in_child && entry.inode == child_inode { return Err(Error::DirectoryCycle); } if parts.peek().is_none() { return Ok(entry.inode); } if !in_child && entry.inode == child_inode { in_child = true; } else { return Err(Error::InvalidPath); } } Err(Error::InvalidPath) } /// Copy exactly the logical 513 bytes, leaving every destination byte after /// that limit untouched even though the device transferred 1024 bytes. pub fn copy_known_partial_file_data(device_data: &[u8], output: &mut [u8]) -> Result { if device_data.len() != 2 * BLOCK_BYTES { return Err(Error::WrongBlockSize); } let logical_size = BLOCK_BYTES + 1; if output.len() < logical_size { return Err(Error::OutputTooSmall); } for index in 0..BLOCK_BYTES { if device_data[index] != (index as u8 ^ 0x7e) { return Err(Error::WrongPartialData); } } if device_data[BLOCK_BYTES] != 0x42 { return Err(Error::WrongPartialData); } output[..logical_size].copy_from_slice(&device_data[..logical_size]); Ok(logical_size) } /// K370–K371: a bounded cursor over already-validated read-only file bytes. /// Seeking past EOF is allowed; reads there return zero without touching the /// caller's output. Relative seeks may not wrap below zero or above u64::MAX. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FileCursor { logical_len: u64, position: u64, } impl FileCursor { pub const fn new(logical_len: u64) -> Self { Self { logical_len, position: 0, } } pub const fn position(&self) -> u64 { self.position } pub fn seek_absolute(&mut self, position: u64) { self.position = position; } pub fn seek_relative(&mut self, delta: i64) -> Result { let next = if delta >= 0 { self.position.checked_add(delta as u64) } else { self.position.checked_sub(delta.unsigned_abs()) } .ok_or(Error::InvalidSeek)?; self.position = next; Ok(next) } pub fn read(&mut self, bytes: &[u8], output: &mut [u8]) -> Result { if self.logical_len > bytes.len() as u64 { return Err(Error::WrongBlockSize); } if self.position >= self.logical_len || output.is_empty() { return Ok(0); } let available = usize::try_from(self.logical_len - self.position) .map_err(|_| Error::ArithmeticOverflow)?; let start = usize::try_from(self.position).map_err(|_| Error::ArithmeticOverflow)?; let take = core::cmp::min(available, output.len()); output[..take].copy_from_slice(&bytes[start..start + take]); self.position += take as u64; Ok(take) } } /// K378: each open owns its own cursor while borrowing already-validated /// read-only bytes. This is not yet a process file-descriptor table or a /// concurrent I/O cache; the caller owns the backing data for both opens. pub struct ReadOnlyOpenFile<'a> { inode: u32, bytes: &'a [u8], cursor: FileCursor, } impl<'a> ReadOnlyOpenFile<'a> { pub fn open(inode: u32, bytes: &'a [u8], logical_len: usize) -> Result { if inode == 0 || logical_len > bytes.len() { return Err(Error::InvalidOpenFile); } Ok(Self { inode, bytes, cursor: FileCursor::new(logical_len as u64), }) } pub const fn inode(&self) -> u32 { self.inode } pub const fn position(&self) -> u64 { self.cursor.position() } pub fn seek_absolute(&mut self, position: u64) { self.cursor.seek_absolute(position); } pub fn read(&mut self, output: &mut [u8]) -> Result { self.cursor.read(self.bytes, output) } /// K379: reject a file-level write before touching either cursor or data. /// The borrowed backing bytes cannot be mutated through this handle. pub fn write(&mut self, _input: &[u8]) -> Result { Err(Error::ReadOnlyFilesystem) } } #[cfg(test)] mod tests { extern crate std; use super::*; use std::collections::BTreeSet; use std::vec::Vec; fn k443_fixtures() -> ([u8; BLOCK_BYTES], [u8; BLOCK_BYTES]) { let mut superblock = [0u8; BLOCK_BYTES]; superblock[..8].copy_from_slice(&MAGIC); superblock[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes()); superblock[12..16].copy_from_slice(&(BLOCK_BYTES as u32).to_le_bytes()); superblock[16..24].copy_from_slice(&2048u64.to_le_bytes()); superblock[24..32].copy_from_slice(&17u64.to_le_bytes()); superblock[32..36].copy_from_slice(&2u32.to_le_bytes()); let mut root = [0u8; BLOCK_BYTES]; root[0] = 1; root[1] = 6; root[4..8].copy_from_slice(&1u32.to_le_bytes()); root[8..14].copy_from_slice(b"README"); root[32] = 1; root[33] = 6; root[36..40].copy_from_slice(&2u32.to_le_bytes()); root[40..46].copy_from_slice(b"KERNEL"); (superblock, root) } #[test] fn p15_k585_turkfs_boundary_corpus_v2() { let corpus = include_str!("../tests/corpus/turkfs-v2.txt"); let mut count = 0; for line in corpus .lines() .filter(|s| !s.is_empty() && !s.starts_with('#')) { let fields: Vec<_> = line.split('|').collect(); assert_eq!(fields.len(), 4); let (mut bytes, _) = k443_fixtures(); let offset: usize = fields[1].parse().unwrap(); for (index, pair) in fields[2].as_bytes().chunks_exact(2).enumerate() { bytes[offset + index] = u8::from_str_radix(core::str::from_utf8(pair).unwrap(), 16).unwrap(); } let result = require_superblock_signature(&bytes) .and_then(|_| require_format_version(&bytes).map(|_| ())) .and_then(|_| require_block_geometry(&bytes, 2048 * BLOCK_BYTES as u64)) .and_then(|geometry| root_directory_location(&bytes, geometry).map(|_| ())) .unwrap_err(); assert_eq!(std::format!("{result:?}"), fields[3]); let hash = Sha256::digest(bytes) .iter() .map(|b| std::format!("{b:02x}")) .collect::(); std::eprintln!("K585 turkfs {} {:?} {}", fields[0], result, hash); count += 1; } assert_eq!(count, 4); } #[test] fn k443_replays_versioned_turkfs_metadata_corpus() { let corpus = include_str!("../tests/corpus/turkfs-v1.txt"); assert!(corpus.starts_with("# TurkOS K443 TurkFS metadata corpus v1.")); let mut seen = BTreeSet::new(); let mut accepted = 0; let mut rejected = 0; for line in corpus .lines() .filter(|line| !line.starts_with('#') && !line.is_empty()) { let fields: Vec<_> = line.split('|').collect(); assert_eq!(fields.len(), 6, "malformed TurkFS corpus record: {line}"); let [id, api, operation, offset, bytes, expected]: [&str; 6] = fields.try_into().unwrap(); assert!(seen.insert(id), "duplicate TurkFS corpus id: {id}"); let (superblock, root) = k443_fixtures(); let mut image = match api { "superblock" => superblock.to_vec(), "root_directory" => root.to_vec(), _ => panic!("unknown TurkFS metadata API: {id}"), }; let offset = offset.parse::().expect("numeric mutation offset"); match operation { "write" => { assert!(!bytes.is_empty() && bytes.len() % 2 == 0, "bad hex: {id}"); let end = offset + bytes.len() / 2; assert!(end <= image.len(), "out-of-bounds TurkFS mutation: {id}"); for (index, chunk) in bytes.as_bytes().chunks_exact(2).enumerate() { let hex = core::str::from_utf8(chunk).unwrap(); image[offset + index] = u8::from_str_radix(hex, 16).expect("hex byte"); } } "cut" => { assert_eq!(offset, 0, "cut offset must be zero: {id}"); let length = bytes.parse::().expect("numeric cut length"); assert!(length < image.len(), "cut must shorten metadata: {id}"); image.truncate(length); } _ => panic!("unknown TurkFS mutation operation: {id}"), } let observed = match api { "superblock" => require_superblock_signature(&image) .and_then(|_| require_format_version(&image).map(|_| ())) .and_then(|_| require_block_geometry(&image, 2048 * BLOCK_BYTES as u64)) .and_then(|geometry| root_directory_location(&image, geometry).map(|_| ())), "root_directory" => parse_root_directory(17, 2, &image).map(|_| ()), _ => unreachable!(), }; let expected_result = match expected { "ok" => Ok(()), "WrongBlockSize" => Err(Error::WrongBlockSize), "WrongSignature" => Err(Error::WrongSignature), "UnsupportedVersion" => Err(Error::UnsupportedVersion), "InvalidBlockGeometry" => Err(Error::InvalidBlockGeometry), "ArithmeticOverflow" => Err(Error::ArithmeticOverflow), "CapacityExceeded" => Err(Error::CapacityExceeded), "InvalidRootDescriptor" => Err(Error::InvalidRootDescriptor), "InvalidRootEntry" => Err(Error::InvalidRootEntry), "InvalidInodeNumber" => Err(Error::InvalidInodeNumber), _ => panic!("unknown TurkFS expected result: {id}"), }; assert_eq!(observed, expected_result, "TurkFS corpus case {id}"); if observed.is_ok() { accepted += 1; } else { rejected += 1; } let digest = Sha256::digest(&image) .iter() .map(|byte| std::format!("{byte:02x}")) .collect::(); std::eprintln!("K443 {id} {api} {expected} {digest}"); } assert_eq!(seen.len(), 31, "K443 corpus completeness changed"); assert_eq!((accepted, rejected), (2, 29)); } #[test] fn k361_versioned_superblock_layout_rejects_old_new_and_short_blocks() { let mut block = [0u8; BLOCK_BYTES]; block[..8].copy_from_slice(&MAGIC); block[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes()); assert_eq!(require_format_version(&block), Ok(1)); assert_eq!( require_format_version(&block[..511]), Err(Error::WrongBlockSize) ); for version in [0u32, 2, u32::MAX] { block[8..12].copy_from_slice(&version.to_le_bytes()); assert_eq!( require_format_version(&block), Err(Error::UnsupportedVersion) ); } // This K361-only function is deliberately version-only; K362 separately // rejects the same block through require_superblock_signature. block[..8].fill(0); block[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes()); assert_eq!(require_format_version(&block), Ok(1)); } #[test] fn k362_superblock_signature_rejects_random_or_one_byte_corrupt_magic() { let mut block = [0u8; BLOCK_BYTES]; block[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes()); assert_eq!(require_format_version(&block), Ok(1)); assert_eq!( require_superblock_signature(&block), Err(Error::WrongSignature) ); block[..8].copy_from_slice(&MAGIC); assert_eq!(require_superblock_signature(&block), Ok(())); assert_eq!( require_superblock_signature(&block[..511]), Err(Error::WrongBlockSize) ); for offset in [0usize, 5, 7] { block[offset] ^= 1; assert_eq!( require_superblock_signature(&block), Err(Error::WrongSignature) ); block[offset] ^= 1; } } #[test] fn k363_block_geometry_rejects_wrong_size_zero_count_overflow_and_disk_escape() { let mut block = [0u8; BLOCK_BYTES]; block[12..16].copy_from_slice(&512u32.to_le_bytes()); block[16..24].copy_from_slice(&2048u64.to_le_bytes()); assert_eq!( require_block_geometry(&block, 1_048_576), Ok(BlockGeometry { block_bytes: 512, block_count: 2048, span_bytes: 1_048_576, }) ); assert_eq!( require_block_geometry(&block, 1_048_575), Err(Error::CapacityExceeded) ); assert_eq!( require_block_geometry(&block[..511], 1_048_576), Err(Error::WrongBlockSize) ); for size in [0u32, 256, 1024, u32::MAX] { block[12..16].copy_from_slice(&size.to_le_bytes()); assert_eq!( require_block_geometry(&block, 1_048_576), Err(Error::InvalidBlockGeometry) ); } block[12..16].copy_from_slice(&512u32.to_le_bytes()); block[16..24].copy_from_slice(&0u64.to_le_bytes()); assert_eq!( require_block_geometry(&block, 1_048_576), Err(Error::InvalidBlockGeometry) ); block[16..24].copy_from_slice(&2049u64.to_le_bytes()); assert_eq!( require_block_geometry(&block, 1_048_576), Err(Error::CapacityExceeded) ); block[16..24].copy_from_slice(&u64::MAX.to_le_bytes()); assert_eq!( require_block_geometry(&block, 1_048_576), Err(Error::ArithmeticOverflow) ); } #[test] fn k364_root_directory_location_and_known_names_reject_bad_pointer_or_entry() { let geometry = BlockGeometry { block_bytes: 512, block_count: 2048, span_bytes: 1_048_576, }; let mut superblock = [0u8; BLOCK_BYTES]; superblock[24..32].copy_from_slice(&17u64.to_le_bytes()); superblock[32..36].copy_from_slice(&2u32.to_le_bytes()); assert_eq!(root_directory_location(&superblock, geometry), Ok((17, 2))); let mut root = [0u8; BLOCK_BYTES]; root[0] = 1; root[1] = 6; root[4..8].copy_from_slice(&1u32.to_le_bytes()); root[8..14].copy_from_slice(b"README"); root[32] = 1; root[33] = 6; root[36..40].copy_from_slice(&2u32.to_le_bytes()); root[40..46].copy_from_slice(b"KERNEL"); let listing = parse_root_directory(17, 2, &root).unwrap(); assert!(listing.contains(b"README")); assert!(listing.contains(b"KERNEL")); assert!(!listing.contains(b"MISSING")); assert_eq!( parse_root_directory(17, 2, &root[..511]), Err(Error::WrongBlockSize) ); root[1] = 25; assert_eq!( parse_root_directory(17, 2, &root), Err(Error::InvalidRootEntry) ); root[1] = 6; root[8] ^= 1; assert!( !parse_root_directory(17, 2, &root) .unwrap() .contains(b"README") ); superblock[24..32].copy_from_slice(&2048u64.to_le_bytes()); assert_eq!( root_directory_location(&superblock, geometry), Err(Error::InvalidRootDescriptor) ); superblock[24..32].copy_from_slice(&16u64.to_le_bytes()); assert_eq!( root_directory_location(&superblock, geometry), Err(Error::InvalidRootDescriptor) ); superblock[24..32].copy_from_slice(&17u64.to_le_bytes()); superblock[32..36].copy_from_slice(&0u32.to_le_bytes()); assert_eq!( root_directory_location(&superblock, geometry), Err(Error::InvalidRootDescriptor) ); } #[test] fn k365_name_length_accepts_exact_slot_and_rejects_zero_overflow_and_max_byte() { assert_eq!(checked_root_name_len(1), Ok(1)); assert_eq!(checked_root_name_len(24), Ok(24)); for length in [0u8, 25, 32, 255] { assert_eq!(checked_root_name_len(length), Err(Error::InvalidRootEntry)); } let mut block = [0u8; BLOCK_BYTES]; block[0] = 1; block[1] = 24; block[4..8].copy_from_slice(&1u32.to_le_bytes()); let exact = *b"ABCDEFGHIJKLMNOPQRSTUVWX"; block[8..32].copy_from_slice(&exact); let listing = parse_root_directory(17, 1, &block).unwrap(); assert!(listing.contains(&exact)); assert!(!listing.contains(b"ABCDEFGHIJKLMNOPQRSTUVWXZ")); for length in [0u8, 25, 255] { block[1] = length; assert_eq!( parse_root_directory(17, 1, &block), Err(Error::InvalidRootEntry) ); } } #[test] fn k366_known_empty_file_returns_zero_without_data_lba_and_rejects_bad_inode() { let geometry = BlockGeometry { block_bytes: 512, block_count: 2048, span_bytes: 1_048_576, }; let mut superblock = [0u8; BLOCK_BYTES]; superblock[36..44].copy_from_slice(&18u64.to_le_bytes()); assert_eq!(inode_table_location(&superblock, geometry, 17), Ok(18)); let mut root_block = [0u8; BLOCK_BYTES]; root_block[0] = 1; root_block[1] = 6; root_block[4..8].copy_from_slice(&1u32.to_le_bytes()); root_block[8..14].copy_from_slice(b"README"); let root = parse_root_directory(17, 1, &root_block).unwrap(); let mut inode_block = [0u8; BLOCK_BYTES]; inode_block[0..4].copy_from_slice(&1u32.to_le_bytes()); assert_eq!(read_known_empty_file(&root, &inode_block, b"README"), Ok(0)); assert_eq!( read_known_empty_file(&root, &inode_block, b"MISSING"), Err(Error::InvalidRootEntry) ); assert_eq!( read_known_empty_file(&root, &inode_block[..511], b"README"), Err(Error::WrongBlockSize) ); inode_block[8..16].copy_from_slice(&1u64.to_le_bytes()); assert_eq!( read_known_empty_file(&root, &inode_block, b"README"), Err(Error::NotEmptyFile) ); inode_block[8..16].copy_from_slice(&0u64.to_le_bytes()); inode_block[16..24].copy_from_slice(&19u64.to_le_bytes()); assert_eq!( read_known_empty_file(&root, &inode_block, b"README"), Err(Error::NotEmptyFile) ); inode_block[16..24].copy_from_slice(&0u64.to_le_bytes()); inode_block[0..4].copy_from_slice(&2u32.to_le_bytes()); assert_eq!( read_known_empty_file(&root, &inode_block, b"README"), Err(Error::InvalidInodeTable) ); superblock[36..44].copy_from_slice(&2048u64.to_le_bytes()); assert_eq!( inode_table_location(&superblock, geometry, 17), Err(Error::InvalidInodeTable) ); } #[test] fn k367_single_block_file_checks_inode_pointer_size_and_full_sha256() { let geometry = BlockGeometry { block_bytes: 512, block_count: 2048, span_bytes: 1_048_576, }; let mut root_block = [0u8; BLOCK_BYTES]; root_block[0] = 1; root_block[1] = 6; root_block[4..8].copy_from_slice(&2u32.to_le_bytes()); root_block[8..14].copy_from_slice(b"KERNEL"); let root = parse_root_directory(17, 1, &root_block).unwrap(); let mut inodes = [0u8; BLOCK_BYTES]; inodes[32..36].copy_from_slice(&2u32.to_le_bytes()); inodes[40..48].copy_from_slice(&512u64.to_le_bytes()); inodes[48..56].copy_from_slice(&19u64.to_le_bytes()); assert_eq!( known_single_block_lba(&root, &inodes, b"KERNEL", geometry, 18), Ok(19) ); assert_eq!( known_single_block_lba(&root, &inodes, b"MISSING", geometry, 18), Err(Error::InvalidRootEntry) ); assert_eq!( known_single_block_lba(&root, &inodes[..511], b"KERNEL", geometry, 18), Err(Error::WrongBlockSize) ); inodes[32..36].copy_from_slice(&3u32.to_le_bytes()); assert_eq!( known_single_block_lba(&root, &inodes, b"KERNEL", geometry, 18), Err(Error::InvalidInodeTable) ); inodes[32..36].copy_from_slice(&2u32.to_le_bytes()); for size in [0u64, 511, 513, u64::MAX] { inodes[40..48].copy_from_slice(&size.to_le_bytes()); assert_eq!( known_single_block_lba(&root, &inodes, b"KERNEL", geometry, 18), Err(Error::InvalidSingleBlockFile) ); } inodes[40..48].copy_from_slice(&512u64.to_le_bytes()); for lba in [0u64, 18, 2048, u64::MAX] { inodes[48..56].copy_from_slice(&lba.to_le_bytes()); assert_eq!( known_single_block_lba(&root, &inodes, b"KERNEL", geometry, 18), Err(Error::InvalidSingleBlockFile) ); } let mut data = [0u8; BLOCK_BYTES]; for (index, byte) in data.iter_mut().enumerate() { *byte = index as u8 ^ 0x5a; } assert_eq!( verify_known_single_block_data(&data), Ok(KERNEL_BLOCK_SHA256) ); assert_eq!( verify_known_single_block_data(&data[..511]), Err(Error::WrongBlockSize) ); data[511] ^= 1; assert_eq!( verify_known_single_block_data(&data), Err(Error::WrongFileHash) ); } #[test] fn k368_preflight_bounds_two_blocks_and_detects_reversed_sector_order() { let geometry = BlockGeometry { block_bytes: 512, block_count: 2048, span_bytes: 1_048_576, }; let mut root_block = [0u8; BLOCK_BYTES]; root_block[0] = 1; root_block[1] = 5; root_block[4..8].copy_from_slice(&3u32.to_le_bytes()); root_block[8..13].copy_from_slice(b"MULTI"); let root = parse_root_directory(17, 1, &root_block).unwrap(); let mut inodes = [0u8; BLOCK_BYTES]; inodes[64..68].copy_from_slice(&3u32.to_le_bytes()); inodes[72..80].copy_from_slice(&1024u64.to_le_bytes()); inodes[80..88].copy_from_slice(&20u64.to_le_bytes()); assert_eq!( known_two_block_lba(&root, &inodes, b"MULTI", geometry, 18), Ok(20) ); assert_eq!( known_two_block_lba(&root, &inodes, b"MISSING", geometry, 18), Err(Error::InvalidRootEntry) ); assert_eq!( known_two_block_lba(&root, &inodes[..511], b"MULTI", geometry, 18), Err(Error::WrongBlockSize) ); inodes[64..68].copy_from_slice(&4u32.to_le_bytes()); assert_eq!( known_two_block_lba(&root, &inodes, b"MULTI", geometry, 18), Err(Error::InvalidInodeTable) ); inodes[64..68].copy_from_slice(&3u32.to_le_bytes()); for size in [0u64, 512, 1023, 1025, u64::MAX] { inodes[72..80].copy_from_slice(&size.to_le_bytes()); assert_eq!( known_two_block_lba(&root, &inodes, b"MULTI", geometry, 18), Err(Error::InvalidMultiBlockFile) ); } inodes[72..80].copy_from_slice(&1024u64.to_le_bytes()); for lba in [0u64, 18, 2047, 2048, u64::MAX] { inodes[80..88].copy_from_slice(&lba.to_le_bytes()); assert_eq!( known_two_block_lba(&root, &inodes, b"MULTI", geometry, 18), Err(Error::InvalidMultiBlockFile) ); } let mut data = [0u8; 2 * BLOCK_BYTES]; for index in 0..BLOCK_BYTES { data[index] = index as u8 ^ 0x3c; data[BLOCK_BYTES + index] = index as u8 ^ 0xc3; } assert_eq!(verify_known_two_block_order(&data), Ok(())); assert_eq!( verify_known_two_block_order(&data[..1023]), Err(Error::WrongBlockSize) ); data[511] ^= 1; assert_eq!( verify_known_two_block_order(&data), Err(Error::WrongBlockOrder) ); data[511] ^= 1; data.swap(0, 512); assert_eq!( verify_known_two_block_order(&data), Err(Error::WrongBlockOrder) ); } #[test] fn k369_partial_last_block_returns_only_513_bytes_and_preserves_guard() { let geometry = BlockGeometry { block_bytes: 512, block_count: 2048, span_bytes: 1_048_576, }; let mut root_block = [0u8; BLOCK_BYTES]; root_block[0] = 1; root_block[1] = 4; root_block[4..8].copy_from_slice(&4u32.to_le_bytes()); root_block[8..12].copy_from_slice(b"TAIL"); let root = parse_root_directory(17, 1, &root_block).unwrap(); let mut inodes = [0u8; BLOCK_BYTES]; inodes[96..100].copy_from_slice(&4u32.to_le_bytes()); inodes[104..112].copy_from_slice(&513u64.to_le_bytes()); inodes[112..120].copy_from_slice(&22u64.to_le_bytes()); assert_eq!( known_partial_file_lba(&root, &inodes, b"TAIL", geometry, 18), Ok(22) ); assert_eq!( known_partial_file_lba(&root, &inodes, b"MISSING", geometry, 18), Err(Error::InvalidRootEntry) ); assert_eq!( known_partial_file_lba(&root, &inodes[..511], b"TAIL", geometry, 18), Err(Error::WrongBlockSize) ); inodes[96..100].copy_from_slice(&5u32.to_le_bytes()); assert_eq!( known_partial_file_lba(&root, &inodes, b"TAIL", geometry, 18), Err(Error::InvalidInodeTable) ); inodes[96..100].copy_from_slice(&4u32.to_le_bytes()); for size in [0u64, 512, 514, 1024, u64::MAX] { inodes[104..112].copy_from_slice(&size.to_le_bytes()); assert_eq!( known_partial_file_lba(&root, &inodes, b"TAIL", geometry, 18), Err(Error::InvalidPartialFile) ); } inodes[104..112].copy_from_slice(&513u64.to_le_bytes()); for lba in [0u64, 18, 2047, 2048, u64::MAX] { inodes[112..120].copy_from_slice(&lba.to_le_bytes()); assert_eq!( known_partial_file_lba(&root, &inodes, b"TAIL", geometry, 18), Err(Error::InvalidPartialFile) ); } let mut data = [0u8; 2 * BLOCK_BYTES]; for index in 0..BLOCK_BYTES { data[index] = index as u8 ^ 0x7e; data[BLOCK_BYTES + index] = index as u8 ^ 0x42; } let mut output = [0xa5u8; BLOCK_BYTES + 2]; assert_eq!(copy_known_partial_file_data(&data, &mut output), Ok(513)); assert_eq!(&output[..513], &data[..513]); assert_eq!(output[513], 0xa5); data[513] ^= 1; assert_eq!(copy_known_partial_file_data(&data, &mut output), Ok(513)); assert_eq!(output[513], 0xa5); data[512] ^= 1; assert_eq!( copy_known_partial_file_data(&data, &mut output), Err(Error::WrongPartialData) ); assert_eq!( copy_known_partial_file_data(&data[..1023], &mut output), Err(Error::WrongBlockSize) ); assert_eq!( copy_known_partial_file_data(&data, &mut output[..512]), Err(Error::OutputTooSmall) ); } #[test] fn k370_seek_reads_exact_subslice_across_sector_boundary_without_overrun() { let mut file = [0u8; BLOCK_BYTES + 1]; for (index, byte) in file[..BLOCK_BYTES].iter_mut().enumerate() { *byte = index as u8 ^ 0x7e; } file[BLOCK_BYTES] = 0x42; let mut cursor = FileCursor::new(file.len() as u64); cursor.seek_absolute(510); let mut output = [0xa5u8; 4]; assert_eq!(cursor.read(&file, &mut output[..3]), Ok(3)); assert_eq!(output, [0x80, 0x81, 0x42, 0xa5]); assert_eq!(cursor.position(), 513); assert_eq!(cursor.seek_relative(-3), Ok(510)); output[..3].fill(0); assert_eq!(cursor.read(&file, &mut output[..3]), Ok(3)); assert_eq!(output, [0x80, 0x81, 0x42, 0xa5]); assert_eq!(cursor.seek_relative(-514), Err(Error::InvalidSeek)); assert_eq!(cursor.position(), 513); cursor.seek_absolute(u64::MAX); assert_eq!(cursor.seek_relative(1), Err(Error::InvalidSeek)); assert_eq!(cursor.position(), u64::MAX); } #[test] fn k371_eof_and_beyond_return_zero_without_touching_output() { let file = [0x42u8; BLOCK_BYTES + 1]; let mut cursor = FileCursor::new(file.len() as u64); let mut output = [0xa5u8; 4]; for position in [513u64, 514, u64::MAX] { cursor.seek_absolute(position); assert_eq!(cursor.read(&file, &mut output), Ok(0)); assert_eq!(output, [0xa5; 4]); assert_eq!(cursor.position(), position); } cursor.seek_absolute(512); assert_eq!(cursor.read(&file, &mut output), Ok(1)); assert_eq!(output, [0x42, 0xa5, 0xa5, 0xa5]); cursor.seek_absolute(u64::MAX); assert_eq!( cursor.read(&file[..512], &mut output), Err(Error::WrongBlockSize) ); } #[test] fn k372_root_inode_number_must_fit_single_16_slot_table_before_io() { let mut block = [0u8; BLOCK_BYTES]; block[0] = 1; block[1] = 4; block[8..12].copy_from_slice(b"TAIL"); for inode in [1u32, 4, 16] { block[4..8].copy_from_slice(&inode.to_le_bytes()); assert_eq!( parse_root_directory(17, 1, &block) .unwrap() .find(b"TAIL") .unwrap() .inode, inode ); } for inode in [17u32, u32::MAX] { block[4..8].copy_from_slice(&inode.to_le_bytes()); assert_eq!( parse_root_directory(17, 1, &block), Err(Error::InvalidInodeNumber) ); } block[4..8].copy_from_slice(&0u32.to_le_bytes()); assert_eq!( parse_root_directory(17, 1, &block), Err(Error::InvalidRootEntry) ); } #[test] fn k373_known_data_extents_reject_all_pairwise_overlap_and_allow_adjacency() { assert_eq!(require_distinct_known_extents(19, 20, 22), Ok(())); assert_eq!( require_distinct_known_extents(19, 19, 22), Err(Error::OverlappingExtent) ); assert_eq!( require_distinct_known_extents(20, 20, 22), Err(Error::OverlappingExtent) ); assert_eq!( require_distinct_known_extents(19, 20, 21), Err(Error::OverlappingExtent) ); assert_eq!(require_distinct_known_extents(19, 22, 20), Ok(())); assert_eq!( require_distinct_known_extents(u64::MAX, 20, 22), Err(Error::ArithmeticOverflow) ); assert_eq!( require_distinct_known_extents(19, u64::MAX, 22), Err(Error::ArithmeticOverflow) ); assert_eq!( require_distinct_known_extents(19, 20, u64::MAX), Err(Error::ArithmeticOverflow) ); } #[test] fn k395_bitmap_has_exactly_one_owner_per_data_block_and_no_orphans() { let mut bitmap = [0u8; BLOCK_BYTES]; for lba in (0..=23).chain(core::iter::once(26)) { bitmap[lba / 8] |= 1 << (lba % 8); } assert_eq!( validate_known_allocation_bitmap(&bitmap, 2048, 19, 20, 22), Ok(()) ); assert_eq!( validate_known_allocation_bitmap(&bitmap, 2048, 19, 19, 22), Err(Error::OverlappingExtent) ); bitmap[20 / 8] &= !(1 << (20 % 8)); assert_eq!( validate_known_allocation_bitmap(&bitmap, 2048, 19, 20, 22), Err(Error::UnallocatedExtent) ); bitmap[20 / 8] |= 1 << (20 % 8); bitmap[25 / 8] |= 1 << (25 % 8); assert_eq!( validate_known_allocation_bitmap(&bitmap, 2048, 19, 20, 22), Err(Error::OrphanAllocatedBlock) ); bitmap[25 / 8] &= !(1 << (25 % 8)); bitmap[2048 / 8] |= 1; assert_eq!( validate_known_allocation_bitmap(&bitmap, 2048, 19, 20, 22), Err(Error::OrphanAllocatedBlock) ); assert_eq!( validate_known_allocation_bitmap(&bitmap[..511], 2048, 19, 20, 22), Err(Error::InvalidBitmap) ); assert_eq!( validate_known_allocation_bitmap(&bitmap, 2049, 19, 20, 22), Err(Error::InvalidBitmap) ); assert_eq!( validate_known_allocation_bitmap(&bitmap, 2048, 19, 26, 22), Err(Error::InvalidBitmap) ); assert_eq!( validate_known_allocation_bitmap(&bitmap, 2048, u64::MAX, 20, 22), Err(Error::ArithmeticOverflow) ); } #[test] fn k396_full_bitmap_returns_no_space_without_partial_mutation() { let mut full = [0u8; BLOCK_BYTES]; full[..256].fill(0xff); let before = full; assert_eq!( reserve_first_free_block(&mut full, 27, 2048), Err(Error::NoSpace) ); assert_eq!(full, before); full[27 / 8] &= !(1 << (27 % 8)); assert_eq!(reserve_first_free_block(&mut full, 27, 2048), Ok(27)); let after_one = full; assert_eq!( reserve_first_free_block(&mut full, 27, 2048), Err(Error::NoSpace) ); assert_eq!(full, after_one); assert_eq!( reserve_first_free_block(&mut full[..511], 27, 2048), Err(Error::InvalidBitmap) ); assert_eq!(full, after_one); assert_eq!( reserve_first_free_block(&mut full, 2048, 2048), Err(Error::InvalidBitmap) ); assert_eq!(full, after_one); assert_eq!( reserve_first_free_block(&mut full, 27, 4097), Err(Error::InvalidBitmap) ); assert_eq!(full, after_one); } #[test] fn k374_real_directory_block_descriptor_bounds_self_cycle_and_finite_child() { let geometry = BlockGeometry { block_bytes: 512, block_count: 2048, span_bytes: 1_048_576, }; let mut root_block = [0u8; BLOCK_BYTES]; root_block[0] = 1; root_block[1] = 3; root_block[4..8].copy_from_slice(&5u32.to_le_bytes()); root_block[8..11].copy_from_slice(b"SUB"); let root = parse_root_directory(17, 1, &root_block).unwrap(); let mut inode_block = [0u8; BLOCK_BYTES]; let offset = 4 * ROOT_ENTRY_BYTES; inode_block[offset..offset + 4].copy_from_slice(&5u32.to_le_bytes()); inode_block[offset + 4..offset + 8].copy_from_slice(&2u32.to_le_bytes()); inode_block[offset + 8..offset + 16].copy_from_slice(&512u64.to_le_bytes()); inode_block[offset + 16..offset + 24].copy_from_slice(&24u64.to_le_bytes()); inode_block[offset + 24..offset + 28].copy_from_slice(&2u32.to_le_bytes()); assert_eq!( known_child_directory_descriptor(&root, &inode_block, b"SUB", geometry), Ok((24, 2)) ); let mut child_block = [0u8; BLOCK_BYTES]; child_block[0] = 1; child_block[1] = 6; child_block[4..8].copy_from_slice(&2u32.to_le_bytes()); child_block[8..14].copy_from_slice(b"KERNEL"); child_block[32] = 1; child_block[33] = 4; child_block[36..40].copy_from_slice(&2u32.to_le_bytes()); child_block[40..44].copy_from_slice(b"SELF"); let child = parse_root_directory(24, 2, &child_block).unwrap(); assert_eq!( resolve_known_child_entry(&root, &child, b"SUB", b"KERNEL"), Ok(2) ); assert_eq!( resolve_known_child_entry(&root, &child, b"SUB", b"SELF"), Ok(2) ); assert_eq!( resolve_known_child_entry(&root, &child, b"SUB", b"MISSING"), Err(Error::InvalidRootEntry) ); child_block[36..40].copy_from_slice(&5u32.to_le_bytes()); let cycle = parse_root_directory(24, 2, &child_block).unwrap(); assert_eq!( resolve_known_child_entry(&root, &cycle, b"SUB", b"SELF"), Err(Error::DirectoryCycle) ); inode_block[offset + 16..offset + 24].copy_from_slice(&23u64.to_le_bytes()); assert_eq!( known_child_directory_descriptor(&root, &inode_block, b"SUB", geometry), Err(Error::InvalidChildDirectory) ); inode_block[offset + 16..offset + 24].copy_from_slice(&24u64.to_le_bytes()); inode_block[offset + 24..offset + 28].copy_from_slice(&0u32.to_le_bytes()); assert_eq!( known_child_directory_descriptor(&root, &inode_block, b"SUB", geometry), Err(Error::InvalidChildDirectory) ); } #[test] fn k375_dot_and_parent_components_stay_inside_known_root() { let mut root_entries = [RootEntry::default(); MAX_ROOT_ENTRIES]; root_entries[0].inode = 5; root_entries[0].name_len = 3; root_entries[0].name[..3].copy_from_slice(b"SUB"); root_entries[1].inode = 2; root_entries[1].name_len = 6; root_entries[1].name[..6].copy_from_slice(b"KERNEL"); let root = RootDirectory { lba: 17, count: 2, entries: root_entries, }; let mut child_entries = [RootEntry::default(); MAX_ROOT_ENTRIES]; child_entries[0].inode = 2; child_entries[0].name_len = 6; child_entries[0].name[..6].copy_from_slice(b"KERNEL"); child_entries[1].inode = 2; child_entries[1].name_len = 4; child_entries[1].name[..4].copy_from_slice(b"SELF"); child_entries[2].inode = 5; child_entries[2].name_len = 2; child_entries[2].name[..2].copy_from_slice(b".."); let child = RootDirectory { lba: 24, count: 3, entries: child_entries, }; for path in [ b"./KERNEL".as_slice(), b"SUB/./KERNEL", b"SUB/../KERNEL", b"SUB/SELF", ] { assert_eq!(resolve_known_dot_path(&root, &child, path), Ok(2)); } for path in [b"../KERNEL".as_slice(), b"SUB/../../KERNEL", b"./../KERNEL"] { assert_eq!( resolve_known_dot_path(&root, &child, path), Err(Error::PathEscapesRoot) ); } assert_eq!( resolve_known_dot_path(&root, &child, b"KERNEL/../KERNEL"), Err(Error::InvalidPath) ); assert_eq!( resolve_known_dot_path(&root, &child, b"SUB//KERNEL"), Err(Error::InvalidPath) ); assert_eq!( resolve_known_dot_path(&root, &child, b""), Err(Error::InvalidPath) ); child_entries[1].inode = 5; let cycle = RootDirectory { lba: 24, count: 3, entries: child_entries, }; assert_eq!( resolve_known_dot_path(&root, &cycle, b"SUB/SELF"), Err(Error::DirectoryCycle) ); } #[test] fn k376_absolute_paths_ignore_cwd_and_relative_paths_use_it() { let mut root_entries = [RootEntry::default(); MAX_ROOT_ENTRIES]; for (index, inode, name) in [ (0, 1, b"README".as_slice()), (1, 2, b"KERNEL"), (2, 5, b"SUB"), ] { root_entries[index].inode = inode; root_entries[index].name_len = name.len() as u8; root_entries[index].name[..name.len()].copy_from_slice(name); } let root = RootDirectory { lba: 17, count: 3, entries: root_entries, }; let mut child_entries = [RootEntry::default(); MAX_ROOT_ENTRIES]; child_entries[0].inode = 2; child_entries[0].name_len = 6; child_entries[0].name[..6].copy_from_slice(b"KERNEL"); let child = RootDirectory { lba: 24, count: 1, entries: child_entries, }; for (cwd, path, expected) in [ (WorkingDirectory::Root, b"/KERNEL".as_slice(), 2), (WorkingDirectory::Child, b"/KERNEL", 2), (WorkingDirectory::Root, b"KERNEL", 2), (WorkingDirectory::Child, b"KERNEL", 2), (WorkingDirectory::Child, b"/SUB/KERNEL", 2), (WorkingDirectory::Child, b"../KERNEL", 2), (WorkingDirectory::Child, b"/README", 1), (WorkingDirectory::Child, b"/SUB/../README", 1), ] { assert_eq!( resolve_known_scoped_path(&root, &child, cwd, path), Ok(expected) ); } assert_eq!( resolve_known_scoped_path(&root, &child, WorkingDirectory::Child, b"README"), Err(Error::InvalidRootEntry) ); assert_eq!( resolve_known_scoped_path(&root, &child, WorkingDirectory::Child, b"SUB/KERNEL"), Err(Error::InvalidRootEntry) ); assert_eq!( resolve_known_scoped_path(&root, &child, WorkingDirectory::Root, b"../KERNEL"), Err(Error::PathEscapesRoot) ); assert_eq!( resolve_known_scoped_path(&root, &child, WorkingDirectory::Child, b"/../KERNEL"), Err(Error::PathEscapesRoot) ); for path in [b"".as_slice(), b"/", b"//KERNEL", b"SUB//KERNEL"] { assert_eq!( resolve_known_scoped_path(&root, &child, WorkingDirectory::Child, path), Err(Error::InvalidPath) ); } } #[test] fn k377_utf8_names_accept_valid_multibyte_and_classify_malformed_bytes_without_panic() { let mut block = [0u8; BLOCK_BYTES]; block[0] = 1; block[1] = 2; block[4..8].copy_from_slice(&2u32.to_le_bytes()); block[8..10].copy_from_slice(&[0xc4, 0xb0]); // valid U+0130 let directory = parse_root_directory(24, 1, &block).unwrap(); assert_eq!(require_utf8_names(&directory), Ok(1)); for bytes in [ [0xff, 0x80], // invalid leading byte [0xc3, 0x00], // incomplete two-byte scalar [0xc0, 0xaf], // overlong encoding [0xed, 0xa0], // incomplete surrogate prefix ] { block[8..10].copy_from_slice(&bytes); let raw = parse_root_directory(24, 1, &block).unwrap(); assert_eq!(require_utf8_names(&raw), Err(Error::InvalidUtf8Name)); } let mut bad_count = directory; bad_count.count = MAX_ROOT_ENTRIES + 1; assert_eq!( require_utf8_names(&bad_count), Err(Error::InvalidRootDescriptor) ); let mut bad_length = directory; bad_length.entries[0].name_len = 25; assert_eq!( require_utf8_names(&bad_length), Err(Error::InvalidRootEntry) ); } #[test] fn k378_two_open_files_keep_independent_offsets_during_interleaved_reads() { let mut multi = [0u8; 2 * BLOCK_BYTES]; for index in 0..BLOCK_BYTES { multi[index] = index as u8 ^ 0x3c; multi[BLOCK_BYTES + index] = index as u8 ^ 0xc3; } let mut tail = [0u8; BLOCK_BYTES + 1]; for (index, byte) in tail[..BLOCK_BYTES].iter_mut().enumerate() { *byte = index as u8 ^ 0x7e; } tail[BLOCK_BYTES] = 0x42; let mut a = ReadOnlyOpenFile::open(3, &multi, multi.len()).unwrap(); let mut b = ReadOnlyOpenFile::open(4, &tail, tail.len()).unwrap(); let mut output = [0xa5u8; 4]; assert_eq!(a.read(&mut output[..2]), Ok(2)); assert_eq!(output, [0x3c, 0x3d, 0xa5, 0xa5]); assert_eq!((a.position(), b.position()), (2, 0)); b.seek_absolute(510); assert_eq!(b.read(&mut output[..3]), Ok(3)); assert_eq!(output, [0x80, 0x81, 0x42, 0xa5]); assert_eq!((a.position(), b.position()), (2, 513)); a.seek_absolute(514); assert_eq!(a.read(&mut output[..2]), Ok(2)); assert_eq!(output[..2], [0xc1, 0xc0]); assert_eq!((a.position(), b.position()), (516, 513)); b.seek_absolute(0); assert_eq!(b.read(&mut output[..2]), Ok(2)); assert_eq!(output[..2], [0x7e, 0x7f]); assert_eq!((a.position(), b.position()), (516, 2)); let mut c = ReadOnlyOpenFile::open(3, &multi, multi.len()).unwrap(); let mut d = ReadOnlyOpenFile::open(3, &multi, multi.len()).unwrap(); assert_eq!(c.read(&mut output[..1]), Ok(1)); assert_eq!((c.position(), d.position()), (1, 0)); d.seek_absolute(512); assert_eq!(d.read(&mut output[..1]), Ok(1)); assert_eq!(output[0], 0xc3); assert_eq!((c.position(), d.position()), (1, 513)); assert!(matches!( ReadOnlyOpenFile::open(0, &multi, 1), Err(Error::InvalidOpenFile) )); assert!(matches!( ReadOnlyOpenFile::open(3, &multi, multi.len() + 1), Err(Error::InvalidOpenFile) )); let mut empty = ReadOnlyOpenFile::open(1, &[], 0).unwrap(); assert_eq!(empty.read(&mut output), Ok(0)); } #[test] fn k379_file_writes_reject_empty_and_nonempty_input_without_mutation() { let original = [0x3c, 0x3d, 0xc1, 0xc0]; let mut file = ReadOnlyOpenFile::open(3, &original, original.len()).unwrap(); file.seek_absolute(2); assert_eq!(file.write(&[0xde, 0xad]), Err(Error::ReadOnlyFilesystem)); assert_eq!(file.position(), 2); assert_eq!(file.write(&[]), Err(Error::ReadOnlyFilesystem)); assert_eq!(file.position(), 2); let mut output = [0xa5; 3]; assert_eq!(file.read(&mut output), Ok(2)); assert_eq!(output, [0xc1, 0xc0, 0xa5]); assert_eq!(file.position(), 4); assert_eq!(original, [0x3c, 0x3d, 0xc1, 0xc0]); } #[test] fn k380_hash_matrix_covers_empty_single_multi_and_partial_logical_files() { let mut kernel = [0u8; BLOCK_BYTES]; let mut multi = [0u8; 2 * BLOCK_BYTES]; let mut tail = [0u8; 2 * BLOCK_BYTES]; for index in 0..BLOCK_BYTES { kernel[index] = index as u8 ^ 0x5a; multi[index] = index as u8 ^ 0x3c; multi[BLOCK_BYTES + index] = index as u8 ^ 0xc3; tail[index] = index as u8 ^ 0x7e; tail[BLOCK_BYTES + index] = index as u8 ^ 0x42; } let hex = crate::program_hash::hex_lower; assert_eq!( hex(&file_sha256(&[])), *b"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ); assert_eq!(file_sha256(&kernel), KERNEL_BLOCK_SHA256); assert_eq!( hex(&file_sha256(&multi)), *b"3dc38e6a35d8c887a0499f3a3127df1bfc7310c1dc1b160491376db8be90b51e" ); assert_eq!( hex(&file_sha256(&tail[..513])), *b"8388c98b9062541ce387d98a910f520f4f9478c2cbe5622abadd35967fb0da9a" ); assert_ne!(file_sha256(&tail[..513]), file_sha256(&tail)); let original = file_sha256(&multi); multi[1023] ^= 1; assert_ne!(original, file_sha256(&multi)); } #[test] fn k411_named_single_block_file_rejects_bad_inode_size_kind_and_pointer() { let mut root_block = [0u8; BLOCK_BYTES]; root_block[0] = 1; root_block[1] = 4; root_block[4..8].copy_from_slice(&6u32.to_le_bytes()); root_block[8..12].copy_from_slice(b"MOTD"); let root = parse_root_directory(17, 1, &root_block).unwrap(); let mut inodes = [0u8; BLOCK_BYTES]; let at = 5 * 32; inodes[at..at + 4].copy_from_slice(&6u32.to_le_bytes()); inodes[at + 8..at + 16].copy_from_slice(&17u64.to_le_bytes()); inodes[at + 16..at + 24].copy_from_slice(&28u64.to_le_bytes()); let geometry = BlockGeometry { block_bytes: 512, block_count: 2048, span_bytes: 2048 * 512, }; let check = |block: &[u8]| bounded_single_block_file(&root, block, b"MOTD", geometry, 18); assert_eq!(check(&inodes), Ok(SingleBlockFile { lba: 28, size: 17 })); assert_eq!( bounded_single_block_file(&root, &inodes, b"NOPE", geometry, 18), Err(Error::InvalidRootEntry) ); inodes[at + 4..at + 8].copy_from_slice(&2u32.to_le_bytes()); assert_eq!(check(&inodes), Err(Error::InvalidInodeTable)); inodes[at + 4..at + 8].fill(0); inodes[at + 8..at + 16].copy_from_slice(&513u64.to_le_bytes()); assert_eq!(check(&inodes), Err(Error::InvalidSingleBlockFile)); inodes[at + 8..at + 16].copy_from_slice(&17u64.to_le_bytes()); inodes[at + 16..at + 24].copy_from_slice(&18u64.to_le_bytes()); assert_eq!(check(&inodes), Err(Error::InvalidSingleBlockFile)); inodes[at + 16..at + 24].copy_from_slice(&2048u64.to_le_bytes()); assert_eq!(check(&inodes), Err(Error::InvalidSingleBlockFile)); inodes[at + 16..at + 24].copy_from_slice(&28u64.to_le_bytes()); inodes[at..at + 4].copy_from_slice(&7u32.to_le_bytes()); assert_eq!(check(&inodes), Err(Error::InvalidInodeTable)); } }