use crate::block_io::BlockIo; use crate::btree::{self, BTreeIter}; use crate::cache::BlockCache; use crate::error::FsError; use crate::integrity::xxhash64; use crate::pool::NodePool; use lancer_core::fs::{BLOCK_SIZE_MIN_LOG2, BlockRef, BlockType, Compression, Inode}; use zerocopy::IntoBytes; const MAX_COLLISION_SLOTS: u64 = 8; const INLINE_NAME_MAX: usize = 34; const DIR_INLINE_NAME: u8 = 0x01; const DIR_COLLISION: u8 = 0x02; const NAME_REGIONS: [(usize, usize); 4] = [(24, 16), (40, 4), (49, 1), (51, 13)]; pub fn dir_entry_key(name: &[u8]) -> u64 { xxhash64(name) & !0xF } pub fn new_dir_entry(child_phys_addr: u64, name: &[u8], txn: u64, collision: bool) -> BlockRef { let flags = match (name.len() <= INLINE_NAME_MAX, collision) { (true, true) => DIR_INLINE_NAME | DIR_COLLISION, (true, false) => DIR_INLINE_NAME, (false, true) => DIR_COLLISION, (false, false) => 0, }; let mut entry = BlockRef::new( child_phys_addr & !0xF, BLOCK_SIZE_MIN_LOG2, 0, txn, 0u128, 0u32, BlockType::Directory, Compression::None, name.len() as u32, flags, ); if name.len() <= INLINE_NAME_MAX { pack_inline_name(&mut entry, name); } entry } pub fn pack_inline_name(entry: &mut BlockRef, name: &[u8]) { let bytes = entry.as_mut_bytes(); NAME_REGIONS.iter().fold(0usize, |offset, &(start, len)| { let remaining = name.len().saturating_sub(offset); let n = remaining.min(len); match n { 0 => offset, _ => { bytes[start..start + n].copy_from_slice(&name[offset..offset + n]); offset + n } } }); } pub fn unpack_inline_name(entry: &BlockRef, buf: &mut [u8]) -> usize { let bytes = entry.as_bytes(); let name_len = (entry.logical_size as usize) .min(INLINE_NAME_MAX) .min(buf.len()); NAME_REGIONS.iter().fold(0usize, |written, &(start, len)| { let remaining = name_len.saturating_sub(written); let n = remaining.min(len); match n { 0 => written, _ => { buf[written..written + n].copy_from_slice(&bytes[start..start + n]); written + n } } }) } pub fn dir_entry_matches( entry: &BlockRef, name: &[u8], cache: &mut BlockCache, bio: &mut BlockIo, ) -> Result { let stored_len = entry.logical_size as usize; match stored_len != name.len() { true => Ok(false), false => match entry.flags & DIR_INLINE_NAME != 0 { true => { let mut buf = [0u8; INLINE_NAME_MAX]; let unpacked = unpack_inline_name(entry, &mut buf); Ok(&buf[..unpacked] == name) } false => { let block_num = crate::blockref_block_num(entry); let data = cache.cache_read(bio, block_num)?; let inode: Inode = unsafe { core::ptr::read_unaligned(data.as_ptr() as *const Inode) }; Ok(&inode.extended_attrs[..stored_len] == name) } }, } } enum LookupSignal { Found(BlockRef), Fs(FsError), } pub fn dir_lookup( pool: &mut NodePool, cache: &mut BlockCache, bio: &mut BlockIo, dir_inode: &Inode, name: &[u8], txn_filter: Option, ) -> Result, FsError> { let base = dir_entry_key(name); let root = dir_inode.direct[0]; scan_collision_slots(pool, cache, bio, &root, name, base, 0, txn_filter) } #[allow(clippy::too_many_arguments)] fn scan_collision_slots( pool: &mut NodePool, cache: &mut BlockCache, bio: &mut BlockIo, root: &BlockRef, name: &[u8], base_key: u64, start_slot: u64, txn_filter: Option, ) -> Result, FsError> { let result = (start_slot..MAX_COLLISION_SLOTS).try_fold((), |(), slot| { let entry = btree::btree_lookup(pool, cache, bio, root, base_key | slot, txn_filter) .map_err(LookupSignal::Fs)?; match entry { None => Ok(()), Some(e) => match dir_entry_matches(&e, name, cache, bio).map_err(LookupSignal::Fs)? { true => Err(LookupSignal::Found(e)), false => Ok(()), }, } }); match result { Ok(()) => Ok(None), Err(LookupSignal::Found(e)) => Ok(Some(e)), Err(LookupSignal::Fs(e)) => Err(e), } } fn find_insert_slot( pool: &mut NodePool, cache: &mut BlockCache, bio: &mut BlockIo, root: &BlockRef, name: &[u8], base_key: u64, ) -> Result { match root.is_null() { true => Ok(0), false => match btree::btree_lookup(pool, cache, bio, root, base_key, None)? { None => Ok(0), Some(entry) => match dir_entry_matches(&entry, name, cache, bio)? { true => Err(FsError::FileExists), false => scan_for_free_slot(pool, cache, bio, root, name, base_key, 1), }, }, } } fn scan_for_free_slot( pool: &mut NodePool, cache: &mut BlockCache, bio: &mut BlockIo, root: &BlockRef, name: &[u8], base_key: u64, start_slot: u64, ) -> Result { (start_slot..MAX_COLLISION_SLOTS) .try_fold(None::, |first_free, slot| { match btree::btree_lookup(pool, cache, bio, root, base_key | slot, None)? { None => Ok(first_free.or(Some(slot))), Some(entry) => match dir_entry_matches(&entry, name, cache, bio)? { true => Err(FsError::FileExists), false => Ok(first_free), }, } })? .ok_or(FsError::TreeFull) } pub fn dir_insert( pool: &mut NodePool, cache: &mut BlockCache, bio: &mut BlockIo, dir_inode: &Inode, name: &[u8], child_phys_addr: u64, txn: u64, ) -> Result { let base = dir_entry_key(name); let root = dir_inode.direct[0]; let slot = find_insert_slot(pool, cache, bio, &root, name, base)?; let entry = new_dir_entry(child_phys_addr, name, txn, slot > 0); let new_root = btree::btree_insert(pool, cache, bio, &root, base | slot, entry, txn)?; let mut updated = dir_inode.clone(); updated.direct[0] = new_root; updated.transaction_id = txn; Ok(updated) } pub fn dir_remove( pool: &mut NodePool, cache: &mut BlockCache, bio: &mut BlockIo, dir_inode: &Inode, name: &[u8], txn: u64, ) -> Result { let base = dir_entry_key(name); let root = dir_inode.direct[0]; let (slot, _entry) = find_matching_slot(pool, cache, bio, &root, name, base)?; let new_root = btree::btree_delete(pool, cache, bio, &root, base | slot, txn)?.unwrap_or(BlockRef::ZERO); let mut updated = dir_inode.clone(); updated.direct[0] = new_root; updated.transaction_id = txn; Ok(updated) } enum MatchSignal { Found(u64, BlockRef), Fs(FsError), } fn find_matching_slot( pool: &mut NodePool, cache: &mut BlockCache, bio: &mut BlockIo, root: &BlockRef, name: &[u8], base_key: u64, ) -> Result<(u64, BlockRef), FsError> { let result = (0..MAX_COLLISION_SLOTS).try_fold((), |(), slot| { let entry = btree::btree_lookup(pool, cache, bio, root, base_key | slot, None) .map_err(MatchSignal::Fs)?; match entry { None => Ok(()), Some(e) => match dir_entry_matches(&e, name, cache, bio).map_err(MatchSignal::Fs)? { true => Err(MatchSignal::Found(slot, e)), false => Ok(()), }, } }); match result { Err(MatchSignal::Found(slot, entry)) => Ok((slot, entry)), Err(MatchSignal::Fs(e)) => Err(e), Ok(()) => Err(FsError::NotFound), } } pub fn dir_list(dir_inode: &Inode, txn_filter: Option) -> BTreeIter { btree::btree_range(&dir_inode.direct[0], 0, u64::MAX, txn_filter) } #[cfg(test)] mod tests { use super::*; #[test] fn dir_entry_key_deterministic() { assert_eq!(dir_entry_key(b"hello"), dir_entry_key(b"hello")); } #[test] fn dir_entry_key_different_names_differ() { assert_ne!(dir_entry_key(b"file_a"), dir_entry_key(b"file_b")); } #[test] fn dir_entry_key_low_nibble_cleared() { let key = dir_entry_key(b"anything"); assert_eq!(key & 0xF, 0); } #[test] fn pack_unpack_short_name() { let name = b"hi"; let entry = new_dir_entry(0x1000, name, 1, false); let mut buf = [0u8; INLINE_NAME_MAX]; let len = unpack_inline_name(&entry, &mut buf); assert_eq!(len, 2); assert_eq!(&buf[..len], b"hi"); } #[test] fn pack_unpack_max_inline_name() { let name: Vec = (0..INLINE_NAME_MAX) .map(|i| (i as u8 % 26) + b'a') .collect(); let entry = new_dir_entry(0x2000, &name, 1, false); let mut buf = [0u8; INLINE_NAME_MAX]; let len = unpack_inline_name(&entry, &mut buf); assert_eq!(len, INLINE_NAME_MAX); assert_eq!(&buf[..len], &name[..]); } #[test] fn pack_unpack_single_byte_name() { let name = b"x"; let entry = new_dir_entry(0x3000, name, 1, false); let mut buf = [0u8; INLINE_NAME_MAX]; let len = unpack_inline_name(&entry, &mut buf); assert_eq!(len, 1); assert_eq!(buf[0], b'x'); } #[test] fn pack_unpack_name_at_each_region_boundary() { [1, 16, 17, 20, 21, 22, 34].iter().for_each(|&name_len| { let name: Vec = (0..name_len).map(|i| (i as u8 % 26) + b'A').collect(); let entry = new_dir_entry(0x4000, &name, 1, false); let mut buf = [0u8; INLINE_NAME_MAX]; let len = unpack_inline_name(&entry, &mut buf); assert_eq!(len, name_len, "wrong len for name_len={}", name_len); assert_eq!(&buf[..len], &name[..], "mismatch for name_len={}", name_len); }); } #[test] fn new_dir_entry_sets_inline_flag() { let short = new_dir_entry(0x1000, b"short", 1, false); assert_ne!(short.flags & DIR_INLINE_NAME, 0); } #[test] fn new_dir_entry_collision_flag() { let no_col = new_dir_entry(0x1000, b"test", 1, false); assert_eq!(no_col.flags & DIR_COLLISION, 0); let with_col = new_dir_entry(0x1000, b"test", 1, true); assert_ne!(with_col.flags & DIR_COLLISION, 0); } #[test] fn new_dir_entry_long_name_no_inline_flag() { let long: Vec = (0..INLINE_NAME_MAX + 1) .map(|i| (i as u8 % 26) + b'a') .collect(); let entry = new_dir_entry(0x5000, &long, 1, false); assert_eq!(entry.flags & DIR_INLINE_NAME, 0); } #[test] fn new_dir_entry_stores_logical_size() { let entry = new_dir_entry(0x1000, b"hello", 1, false); assert_eq!(entry.logical_size, 5); } #[test] fn dir_entry_matches_inline_match() { let mut bio = crate::test_helpers::make_bio(32); let mut cache = crate::test_helpers::make_cache(); let name = b"test.txt"; let entry = new_dir_entry(0x1000, name, 1, false); assert!(dir_entry_matches(&entry, name, &mut cache, &mut bio).unwrap()); } #[test] fn dir_entry_matches_inline_mismatch() { let mut bio = crate::test_helpers::make_bio(32); let mut cache = crate::test_helpers::make_cache(); let entry = new_dir_entry(0x1000, b"alpha", 1, false); assert!(!dir_entry_matches(&entry, b"omega", &mut cache, &mut bio).unwrap()); } #[test] fn dir_entry_matches_length_mismatch() { let mut bio = crate::test_helpers::make_bio(32); let mut cache = crate::test_helpers::make_cache(); let entry = new_dir_entry(0x1000, b"ab", 1, false); assert!(!dir_entry_matches(&entry, b"abc", &mut cache, &mut bio).unwrap()); } } pub fn dir_is_empty( pool: &mut NodePool, cache: &mut BlockCache, bio: &mut BlockIo, dir_inode: &Inode, ) -> Result { let mut iter = btree::btree_range(&dir_inode.direct[0], 0, u64::MAX, None); match iter.next(pool, cache, bio)? { None => Ok(true), Some(_) => Ok(false), } }