use crate::Ext4;
use crate::block_index::{FileBlockIndex, FsBlockIndex};
use crate::dir_block::DirBlock;
use crate::dir_entry::{DirEntry, DirEntryName};
use crate::dir_entry_hash::HashAlg;
use crate::error::{CorruptKind, Ext4Error};
use crate::extent::Extent;
use crate::inode::{Inode, InodeFlags, InodeIndex};
use crate::iters::extents::Extents;
use crate::iters::file_blocks::FileBlocks;
use crate::path::PathBuf;
use crate::util::{read_u16le, read_u32le, usize_from_u32};
use alloc::rc::Rc;
use alloc::vec;
type DirHash = u32;
struct InternalNode<'a> {
entries: &'a [u8],
}
impl<'a> InternalNode<'a> {
const ENTRY_SIZE: usize = 8;
fn from_root_block(
block: &'a [u8],
inode: InodeIndex,
) -> Result<Self, Ext4Error> {
Self::new(&block[0x20..], inode)
}
fn from_non_root_block(
block: &'a [u8],
inode: InodeIndex,
) -> Result<Self, Ext4Error> {
Self::new(&block[0x8..], inode)
}
fn new(mut bytes: &'a [u8], inode: InodeIndex) -> Result<Self, Ext4Error> {
let err = CorruptKind::DirEntry(inode).into();
if bytes.len() < Self::ENTRY_SIZE {
return Err(err);
}
let count = usize::from(read_u16le(bytes, 2));
let end_byte: usize = Self::ENTRY_SIZE.checked_mul(count).unwrap();
bytes = bytes.get(..end_byte).ok_or(err)?;
Ok(Self { entries: bytes })
}
fn get_entry(&self, index: usize) -> (DirHash, FileBlockIndex) {
let offset: usize = Self::ENTRY_SIZE.checked_mul(index).unwrap();
let block_offset: usize = offset.checked_add(4).unwrap();
let block = read_u32le(self.entries, block_offset);
let hash = if index == 0 {
0
} else {
read_u32le(self.entries, offset)
};
(hash, block)
}
fn num_entries(&self) -> usize {
self.entries.len() / Self::ENTRY_SIZE
}
fn lookup_block_by_hash(
&self,
lookup_hash: DirHash,
) -> Option<FileBlockIndex> {
let mut left = 0;
let mut right = self.num_entries().checked_sub(1)?;
while left <= right {
let mid = left.checked_add(right)? / 2;
let mid_hash = self.get_entry(mid).0;
if mid_hash <= lookup_hash {
left = mid.checked_add(1)?;
} else {
right = mid.checked_sub(1)?;
}
}
let index = left.checked_sub(1)?;
Some(self.get_entry(index).1)
}
}
fn read_root_block(
fs: &Ext4,
inode: &Inode,
block: &mut [u8],
) -> Result<(), Ext4Error> {
let mut file_blocks = FileBlocks::new(fs.clone(), inode)?;
let block_index = file_blocks
.next()
.ok_or(CorruptKind::DirEntry(inode.index))??;
let dir_block = DirBlock {
fs,
dir_inode: inode.index,
block_index,
is_first: true,
has_htree: true,
checksum_base: inode.checksum_base.clone(),
};
dir_block.read(block)
}
fn read_dot_or_dotdot(
fs: Ext4,
inode: &Inode,
name: DirEntryName<'_>,
block: &[u8],
) -> Result<Option<DirEntry>, Ext4Error> {
let corrupt = || CorruptKind::DirEntry(inode.index).into();
let offset = if name == "." {
0
} else if name == ".." {
12
} else {
return Ok(None);
};
let (entry, _size) = DirEntry::from_bytes(
fs,
&block[offset..],
inode.index,
Rc::new(PathBuf::empty()),
)?;
let entry = entry.ok_or_else(corrupt)?;
if entry.file_name() == name {
Ok(Some(entry))
} else {
Err(corrupt())
}
}
fn find_extent_for_block(
fs: &Ext4,
inode: &Inode,
block: FileBlockIndex,
) -> Result<Extent, Ext4Error> {
for extent in Extents::new(fs.clone(), inode)? {
let extent = extent?;
let start = extent.block_within_file;
let end = start
.checked_add(u32::from(extent.num_blocks))
.ok_or(CorruptKind::DirEntry(inode.index))?;
if block >= start && block < end {
return Ok(extent);
}
}
Err(CorruptKind::DirEntry(inode.index).into())
}
fn block_from_file_block(
fs: &Ext4,
inode: &Inode,
relative_block: FileBlockIndex,
) -> Result<FsBlockIndex, Ext4Error> {
if inode.flags.contains(InodeFlags::EXTENTS) {
let extent = find_extent_for_block(fs, inode, relative_block)?;
let block_within_extent = relative_block
.checked_sub(extent.block_within_file)
.ok_or(CorruptKind::DirEntry(inode.index))?;
let absolute_block = extent
.start_block
.checked_add(u64::from(block_within_extent))
.ok_or(CorruptKind::DirEntry(inode.index))?;
Ok(absolute_block)
} else {
let mut block_map = FileBlocks::new(fs.clone(), inode)?;
block_map
.nth(usize_from_u32(relative_block))
.ok_or(CorruptKind::DirEntry(inode.index))?
}
}
fn find_leaf_node(
fs: &Ext4,
inode: &Inode,
name: DirEntryName<'_>,
block: &mut [u8],
) -> Result<(), Ext4Error> {
let hash_alg = HashAlg::from_u8(block[0x1c])?;
let depth = block[0x1e];
let root_node = InternalNode::from_root_block(block, inode.index)?;
let hash = hash_alg.hash(name, &fs.0.superblock.htree_hash_seed);
let mut child_block_relative = root_node
.lookup_block_by_hash(hash)
.ok_or(CorruptKind::DirEntry(inode.index))?;
for level in 0..=depth {
let block_index =
block_from_file_block(fs, inode, child_block_relative)?;
let dir_block = DirBlock {
fs,
dir_inode: inode.index,
block_index,
is_first: false,
has_htree: true,
checksum_base: inode.checksum_base.clone(),
};
dir_block.read(block)?;
if level != depth {
let inner_node =
InternalNode::from_non_root_block(block, inode.index)?;
child_block_relative = inner_node
.lookup_block_by_hash(hash)
.ok_or(CorruptKind::DirEntry(inode.index))?;
}
}
Ok(())
}
pub(crate) fn get_dir_entry_via_htree(
fs: &Ext4,
inode: &Inode,
name: DirEntryName<'_>,
) -> Result<DirEntry, Ext4Error> {
assert!(inode.flags.contains(InodeFlags::DIRECTORY_HTREE));
let block_size = fs.0.superblock.block_size;
let mut block = vec![0; block_size.to_usize()];
read_root_block(fs, inode, &mut block)?;
if let Some(entry) = read_dot_or_dotdot(fs.clone(), inode, name, &block)? {
return Ok(entry);
}
find_leaf_node(fs, inode, name, &mut block)?;
let path = Rc::new(PathBuf::empty());
let mut offset_within_block = 0;
while offset_within_block < block.len() {
let (dir_entry, entry_size) = DirEntry::from_bytes(
fs.clone(),
&block[offset_within_block..],
inode.index,
path.clone(),
)?;
offset_within_block = offset_within_block
.checked_add(entry_size)
.ok_or(CorruptKind::DirEntry(inode.index))?;
let Some(dir_entry) = dir_entry else {
continue;
};
if dir_entry.file_name() == name {
return Ok(dir_entry);
}
}
Err(Ext4Error::NotFound)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "std")]
use crate::{FollowSymlinks, Path, ReadDir};
#[test]
fn test_internal_node() {
let inode = InodeIndex::new(1).unwrap();
let mut bytes = Vec::new();
let add_entry =
|bytes: &mut Vec<u8>, hash: DirHash, block: FileBlockIndex| {
bytes.extend(hash.to_le_bytes());
bytes.extend(block.to_le_bytes());
};
bytes.extend(20u16.to_le_bytes()); bytes.extend(11u16.to_le_bytes()); bytes.extend(100u32.to_le_bytes());
add_entry(&mut bytes, 2, 199);
add_entry(&mut bytes, 4, 198);
add_entry(&mut bytes, 6, 197);
add_entry(&mut bytes, 8, 196);
add_entry(&mut bytes, 10, 195);
add_entry(&mut bytes, 12, 194);
add_entry(&mut bytes, 14, 193);
add_entry(&mut bytes, 16, 192);
add_entry(&mut bytes, 18, 191);
add_entry(&mut bytes, 20, 190);
let node = InternalNode::new(&bytes, inode).unwrap();
assert_eq!(node.num_entries(), 11);
assert_eq!(node.get_entry(0), (0, 100));
assert_eq!(node.get_entry(10), (20, 190));
assert_eq!(node.lookup_block_by_hash(0), Some(100));
assert_eq!(node.lookup_block_by_hash(9), Some(196));
assert_eq!(node.lookup_block_by_hash(10), Some(195));
assert_eq!(node.lookup_block_by_hash(11), Some(195));
assert_eq!(node.lookup_block_by_hash(12), Some(194));
assert_eq!(node.lookup_block_by_hash(20), Some(190));
assert_eq!(node.lookup_block_by_hash(30), Some(190));
bytes[2..4].copy_from_slice(&12u16.to_le_bytes()); add_entry(&mut bytes, 30, 189);
let node = InternalNode::new(&bytes, inode).unwrap();
assert_eq!(node.num_entries(), 12);
assert_eq!(node.lookup_block_by_hash(0), Some(100));
assert_eq!(node.lookup_block_by_hash(9), Some(196));
assert_eq!(node.lookup_block_by_hash(10), Some(195));
assert_eq!(node.lookup_block_by_hash(11), Some(195));
assert_eq!(node.lookup_block_by_hash(12), Some(194));
assert_eq!(node.lookup_block_by_hash(20), Some(190));
assert_eq!(node.lookup_block_by_hash(30), Some(189));
}
#[cfg(feature = "std")]
#[test]
fn test_read_dot_or_dotdot() {
let fs = crate::test_util::load_test_disk1();
let mut block = vec![0; fs.0.superblock.block_size.to_usize()];
let inode = fs
.path_to_inode("/big_dir".try_into().unwrap(), FollowSymlinks::All)
.unwrap();
read_root_block(&fs, &inode, &mut block).unwrap();
let entry = read_dot_or_dotdot(
fs.clone(),
&inode,
".".try_into().unwrap(),
&block,
)
.unwrap()
.unwrap();
assert_eq!(entry.file_name(), ".");
let entry = read_dot_or_dotdot(
fs.clone(),
&inode,
"..".try_into().unwrap(),
&block,
)
.unwrap()
.unwrap();
assert_eq!(entry.file_name(), "..");
assert!(
read_dot_or_dotdot(
fs.clone(),
&inode,
"somename".try_into().unwrap(),
&block
)
.unwrap()
.is_none()
);
}
#[cfg(feature = "std")]
#[track_caller]
fn compare_all_entries(fs: &Ext4, dir: Path<'_>) -> usize {
let dir_inode = fs.path_to_inode(dir, FollowSymlinks::All).unwrap();
let iter =
ReadDir::new(fs.clone(), &dir_inode, PathBuf::from(dir)).unwrap();
let mut count = 0;
for iter_entry in iter {
let iter_entry = iter_entry.unwrap();
let htree_entry =
get_dir_entry_via_htree(fs, &dir_inode, iter_entry.file_name())
.unwrap();
assert_eq!(htree_entry.file_name(), iter_entry.file_name());
assert_eq!(htree_entry.inode, iter_entry.inode);
count += 1;
}
count
}
#[cfg(feature = "std")]
#[test]
fn test_get_dir_entry_via_htree() {
let fs = crate::test_util::load_test_disk1();
let medium_dir = Path::new("/medium_dir");
assert_eq!(compare_all_entries(&fs, medium_dir), 1_002);
let big_dir = Path::new("/big_dir");
assert_eq!(compare_all_entries(&fs, big_dir), 10_002);
}
#[cfg(feature = "std")]
#[test]
fn test_block_from_file_block() {
let fs = crate::test_util::load_test_disk1();
let mut extents = Vec::new();
extents.extend(&0xf30au16.to_le_bytes());
extents.extend(&2u16.to_le_bytes());
extents.extend(&2u16.to_le_bytes());
extents.extend(&0u16.to_le_bytes());
extents.extend(&0u32.to_le_bytes());
extents.extend(&0u32.to_le_bytes());
extents.extend(&23u16.to_le_bytes());
extents.extend(0u16.to_le_bytes());
extents.extend(2543u32.to_le_bytes());
extents.extend(&23u32.to_le_bytes());
extents.extend(&47u16.to_le_bytes());
extents.extend(0u16.to_le_bytes());
extents.extend(11u32.to_le_bytes());
extents.resize(60usize, 0u8);
let mut inode = fs
.path_to_inode(
"/medium_dir".try_into().unwrap(),
FollowSymlinks::All,
)
.unwrap();
inode.inline_data.copy_from_slice(&extents);
let extents: Vec<_> = Extents::new(fs.clone(), &inode)
.unwrap()
.map(|e| e.unwrap())
.collect();
assert_eq!(
extents,
[
Extent {
start_block: 2543,
num_blocks: 23,
block_within_file: 0,
},
Extent {
start_block: 11,
num_blocks: 47,
block_within_file: 23,
}
]
);
assert_eq!(block_from_file_block(&fs, &inode, 0).unwrap(), 2543);
assert_eq!(block_from_file_block(&fs, &inode, 1).unwrap(), 2544);
assert_eq!(block_from_file_block(&fs, &inode, 22).unwrap(), 2565);
assert_eq!(block_from_file_block(&fs, &inode, 23).unwrap(), 11);
assert_eq!(block_from_file_block(&fs, &inode, 24).unwrap(), 12);
assert_eq!(block_from_file_block(&fs, &inode, 69).unwrap(), 57);
assert!(block_from_file_block(&fs, &inode, 70).is_err());
}
}