Skip to main content

fs_ext4/
htree.rs

1//! HTree (hash tree) directory lookup.
2//!
3//! Spec: kernel.org/doc/html/latest/filesystems/ext4/directory.html#hash-tree-directories
4//!
5//! Indexed directories (those with the `EXT4_INDEX_FL` inode flag) store the
6//! root of a B+ tree in the first directory block. Each interior node is
7//! itself a directory block with two leading "fake" entries (`.` and `..`)
8//! followed by an `dx_root_info` then an array of `dx_entry` records sorted
9//! by hash. The tree depth is at most 2 (root → optional intermediate → leaf).
10//!
11//! The leaf blocks contain regular `ext4_dir_entry_2` records that we parse
12//! with [`crate::dir::parse_block`].
13//!
14//! Lookup algorithm:
15//!   1. Compute h = name_hash(name, root_info.hash_version, sb.hash_seed)
16//!   2. At depth 0, binary-search dx_entry[] for largest entry with hash <= h
17//!   3. If indir_levels > 0, descend: read child block, scan its dx_entry[]
18//!   4. The final dx_entry.block is the logical block index (within the dir)
19//!      of the leaf containing entries with this hash range.
20//!   5. Caller does linear scan in that leaf block for the actual name match.
21
22use crate::error::{Error, Result};
23use crate::hash::{name_hash, HashVersion, NameHash};
24
25/// Magic check value for dx_root header reserved field (always 0).
26const DX_ROOT_RESERVED_ZERO: u32 = 0;
27
28/// Parsed dx_root_info (8 bytes immediately after the two fake "." and ".." entries).
29#[derive(Debug, Clone, Copy)]
30pub struct DxRootInfo {
31    pub reserved_zero: u32,
32    pub hash_version: HashVersion,
33    /// Length of the info structure in bytes (always 8).
34    pub info_length: u8,
35    /// 0 = entries[] are leaf pointers; 1 = entries[] point at intermediate nodes.
36    pub indirect_levels: u8,
37    pub unused_flags: u8,
38}
39
40/// Header of any dx node (root or intermediate). 8 bytes.
41#[derive(Debug, Clone, Copy)]
42pub struct DxCountLimit {
43    /// Maximum number of dx_entry records this block can hold.
44    pub limit: u16,
45    /// Current number of valid dx_entry records (including the leading "fake" one).
46    pub count: u16,
47}
48
49impl DxCountLimit {
50    pub fn parse(buf: &[u8]) -> Result<Self> {
51        if buf.len() < 4 {
52            return Err(Error::Corrupt("dx count_limit buffer too small"));
53        }
54        Ok(Self {
55            limit: u16::from_le_bytes(buf[0..2].try_into().unwrap()),
56            count: u16::from_le_bytes(buf[2..4].try_into().unwrap()),
57        })
58    }
59}
60
61/// One entry in a dx_node array — maps a hash bound to a child block.
62#[derive(Debug, Clone, Copy)]
63pub struct DxEntry {
64    /// Lower-bound hash for entries pointed to by `block` (0 for the first slot).
65    pub hash: u32,
66    /// Logical block index *within the directory* (i.e. relative to the
67    /// directory file, not absolute physical block).
68    pub block: u32,
69}
70
71impl DxEntry {
72    pub const SIZE: usize = 8;
73
74    pub fn parse(buf: &[u8]) -> Result<Self> {
75        if buf.len() < Self::SIZE {
76            return Err(Error::Corrupt("dx entry buffer too small"));
77        }
78        Ok(Self {
79            hash: u32::from_le_bytes(buf[0..4].try_into().unwrap()),
80            block: u32::from_le_bytes(buf[4..8].try_into().unwrap()),
81        })
82    }
83}
84
85/// Parse `DxRootInfo` from a directory's first block. Layout:
86///
87/// ```text
88///   0..12   "."  fake dir entry  (inode, rec_len=12, name_len=1, type, '.')
89///  12..24   ".." fake dir entry  (inode, rec_len=block_size-12, ...)
90///  24..32   dx_root_info  (reserved_zero, hash_version, info_length,
91///                          indirect_levels, unused_flags)
92///  32..     dx_count_limit (4 bytes) followed by dx_entry[] (8 bytes each)
93/// ```
94pub fn parse_root_info(block: &[u8]) -> Result<DxRootInfo> {
95    if block.len() < 32 {
96        return Err(Error::Corrupt("dx root block too small"));
97    }
98    let reserved = u32::from_le_bytes(block[24..28].try_into().unwrap());
99    if reserved != DX_ROOT_RESERVED_ZERO {
100        return Err(Error::Corrupt("dx root reserved must be 0"));
101    }
102    let hash_version_byte = block[28];
103    let hash_version = HashVersion::from_u8(hash_version_byte)
104        .ok_or(Error::Corrupt("unknown htree hash version"))?;
105    Ok(DxRootInfo {
106        reserved_zero: reserved,
107        hash_version,
108        info_length: block[29],
109        indirect_levels: block[30],
110        unused_flags: block[31],
111    })
112}
113
114/// Parse the count_limit + dx_entry array from a dx_root block.
115///
116/// Kernel layout trick: `entries[0]` sits at offset `24 + info_length`. Its
117/// 4-byte *hash* slot is **overloaded** with `(limit u16, count u16)` — but
118/// its 4-byte *block* slot is a real leaf/node pointer covering the low-hash
119/// range. Earlier versions of this module skipped past the limit/count and
120/// treated `entries[1..]` as `entries[0..]`, losing the first pointer and
121/// breaking lookups whenever a name hashed below the real `entries[1].hash`
122/// boundary. We now parse starting at `cl_offset` and force
123/// `entries[0].hash = 0` to match the kernel semantics.
124pub fn parse_root_entries(block: &[u8]) -> Result<(DxCountLimit, Vec<DxEntry>)> {
125    let info = parse_root_info(block)?;
126    let cl_offset = 24 + info.info_length as usize;
127    if cl_offset + 4 > block.len() {
128        return Err(Error::Corrupt("dx root count_limit out of range"));
129    }
130    let cl = DxCountLimit::parse(&block[cl_offset..cl_offset + 4])?;
131    let (cl, mut entries) = parse_entries_from(block, cl_offset, cl)?;
132    if let Some(first) = entries.first_mut() {
133        // The 4 bytes we just parsed as `hash` are really (limit, count); the
134        // kernel reads entry[0] with hash == 0 (slot-0 sentinel).
135        first.hash = 0;
136    }
137    Ok((cl, entries))
138}
139
140/// Parse the count_limit + dx_entry array from an *intermediate* dx_node block.
141/// Layout is simpler than the root: a single 8-byte "fake" entry as a header
142/// (inode=0, rec_len=block_size, name_len=0, type=0), then `entries[0]` whose
143/// hash slot is overloaded with `(limit, count)` — same trick as the root.
144pub fn parse_node_entries(block: &[u8]) -> Result<(DxCountLimit, Vec<DxEntry>)> {
145    if block.len() < 8 + 4 {
146        return Err(Error::Corrupt("dx node block too small"));
147    }
148    let cl = DxCountLimit::parse(&block[8..12])?;
149    let (cl, mut entries) = parse_entries_from(block, 8, cl)?;
150    if let Some(first) = entries.first_mut() {
151        first.hash = 0;
152    }
153    Ok((cl, entries))
154}
155
156fn parse_entries_from(
157    block: &[u8],
158    start: usize,
159    cl: DxCountLimit,
160) -> Result<(DxCountLimit, Vec<DxEntry>)> {
161    if cl.count == 0 {
162        return Err(Error::Corrupt("dx node has zero entries"));
163    }
164    if cl.count > cl.limit {
165        return Err(Error::Corrupt("dx count > limit"));
166    }
167    let need_bytes = cl.count as usize * DxEntry::SIZE;
168    if start + need_bytes > block.len() {
169        return Err(Error::Corrupt("dx entries overflow block"));
170    }
171    let mut out = Vec::with_capacity(cl.count as usize);
172    for i in 0..cl.count as usize {
173        let off = start + i * DxEntry::SIZE;
174        out.push(DxEntry::parse(&block[off..off + DxEntry::SIZE])?);
175    }
176    Ok((cl, out))
177}
178
179/// Find the dx_entry whose hash range covers `target`. Returns the largest
180/// entry with `entry.hash <= target`. The first entry's hash field is the
181/// "limit" sentinel (always 0 in slot 0) — slot 0 covers `[0, slot1.hash)`.
182pub fn find_entry_for_hash(entries: &[DxEntry], target: u32) -> &DxEntry {
183    debug_assert!(!entries.is_empty(), "dx node must have ≥1 entry");
184    // Linear scan is fine: typical dx blocks hold ≤500 entries and we only
185    // descend at most twice. Binary search is a future optimisation.
186    let mut chosen = &entries[0];
187    for e in &entries[1..] {
188        if e.hash <= target {
189            chosen = e;
190        } else {
191            break;
192        }
193    }
194    chosen
195}
196
197/// One step of the descent. Caller supplies the current node block and gets
198/// back the next logical block to read (within the directory file).
199///
200/// Returns:
201/// - `Ok(NextStep::Leaf(block))` if the descent has reached a leaf and the
202///   caller should now linearly scan that block for the name.
203/// - `Ok(NextStep::Inner(block))` if there's another intermediate level.
204pub enum NextStep {
205    /// `block` is the logical block index of a leaf containing dir entries.
206    Leaf(u32),
207    /// `block` is the logical block index of another intermediate dx_node.
208    Inner(u32),
209}
210
211/// Compute the lookup target hash for a name, then walk the root + 0/1
212/// intermediate nodes to produce the leaf block index.
213///
214/// `block_size` is needed only to make the algorithm self-describing; the
215/// caller is responsible for actually reading dir blocks via [`crate::file_io`]
216/// because reading inside the directory file (logical → physical) requires
217/// the inode + extent tree.
218pub fn target_hash(name: &[u8], root_block: &[u8], hash_seed: &[u32; 4]) -> Result<NameHash> {
219    let info = parse_root_info(root_block)?;
220    Ok(name_hash(name, info.hash_version, hash_seed))
221}
222
223/// Resolve the leaf block (logical block index within the directory) that
224/// MIGHT contain `name`. Returns `Some(leaf_block)` on success or `None`
225/// only when the dx tree is empty (defensive — should not happen in a real
226/// indexed directory).
227///
228/// `read_dx_block` is a closure the caller provides to read a logical block
229/// of the directory file (since this module has no view of the inode/extent
230/// tree). Signature: `fn(logical_block: u32) -> Result<Vec<u8>>`.
231///
232/// Hashes with the root's version exactly as recorded, which is the signed
233/// variant. A filesystem whose superblock says unsigned needs
234/// [`lookup_leaf_with`], or the descent picks leaves by the wrong hash for
235/// any name with a byte at or above 0x80.
236pub fn lookup_leaf<R>(
237    name: &[u8],
238    root_block: &[u8],
239    hash_seed: &[u32; 4],
240    read_dx_block: R,
241) -> Result<Option<u32>>
242where
243    R: FnMut(u32) -> Result<Vec<u8>>,
244{
245    lookup_leaf_with(name, root_block, hash_seed, false, read_dx_block)
246}
247
248/// [`lookup_leaf`], with the superblock's `EXT2_FLAGS_UNSIGNED_HASH` applied
249/// to the root's hash version the way the kernel applies it
250/// ([`crate::hash::effective_version`]).
251pub fn lookup_leaf_with<R>(
252    name: &[u8],
253    root_block: &[u8],
254    hash_seed: &[u32; 4],
255    unsigned_hash: bool,
256    mut read_dx_block: R,
257) -> Result<Option<u32>>
258where
259    R: FnMut(u32) -> Result<Vec<u8>>,
260{
261    let info = parse_root_info(root_block)?;
262    let version = crate::hash::effective_version(info.hash_version, unsigned_hash);
263    let hash = name_hash(name, version, hash_seed);
264
265    // Root: parse + pick the entry whose range covers this hash.
266    let (_cl, entries) = parse_root_entries(root_block)?;
267    if entries.is_empty() {
268        return Ok(None);
269    }
270    let mut target = find_entry_for_hash(&entries, hash.major).block;
271
272    // Descend through `indirect_levels` intermediate nodes, if any.
273    for _level in 0..info.indirect_levels {
274        let block = read_dx_block(target)?;
275        let (_cl, entries) = parse_node_entries(&block)?;
276        if entries.is_empty() {
277            return Ok(None);
278        }
279        target = find_entry_for_hash(&entries, hash.major).block;
280    }
281
282    Ok(Some(target))
283}
284
285// ---------------------------------------------------------------------------
286// Tests — synthetic data only (real htree images need a special mkfs config)
287// ---------------------------------------------------------------------------
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    /// Build a minimal dx_root block: "." entry, ".." entry, root_info
294    /// declaring TEA hash with 0 indirect levels, count_limit (count=2,
295    /// limit=200), then two dx_entry slots.
296    fn synth_root_block() -> Vec<u8> {
297        let block_size = 4096;
298        let mut buf = vec![0u8; block_size];
299
300        // "." dir entry (12 bytes): inode=2, rec_len=12, name_len=1, type=2, "."
301        buf[0..4].copy_from_slice(&2u32.to_le_bytes());
302        buf[4..6].copy_from_slice(&12u16.to_le_bytes());
303        buf[6] = 1;
304        buf[7] = 2;
305        buf[8] = b'.';
306
307        // ".." dir entry (12 bytes): inode=2, rec_len=block_size-12, name_len=2, type=2, ".."
308        buf[12..16].copy_from_slice(&2u32.to_le_bytes());
309        buf[16..18].copy_from_slice(&((block_size - 12) as u16).to_le_bytes());
310        buf[18] = 2;
311        buf[19] = 2;
312        buf[20] = b'.';
313        buf[21] = b'.';
314
315        // dx_root_info (8 bytes at offset 24):
316        //   reserved=0, hash_version=2 (TEA), info_length=8, indirect=0, flags=0
317        buf[24..28].copy_from_slice(&0u32.to_le_bytes());
318        buf[28] = 2; // TEA
319        buf[29] = 8; // info_length
320        buf[30] = 0; // indirect_levels
321        buf[31] = 0; // unused_flags
322
323        // entries[0] at offset 32..40: hash slot overloaded with (limit, count),
324        // block = 1 (sentinel slot — covers the low-hash range [0, entries[1].hash)).
325        buf[32..34].copy_from_slice(&200u16.to_le_bytes()); // limit
326        buf[34..36].copy_from_slice(&2u16.to_le_bytes()); // count
327        buf[36..40].copy_from_slice(&1u32.to_le_bytes()); // entries[0].block
328
329        // entries[1] at offset 40..48: hash=0x80000000, block=2 (covers high hashes).
330        buf[40..44].copy_from_slice(&0x8000_0000u32.to_le_bytes());
331        buf[44..48].copy_from_slice(&2u32.to_le_bytes());
332
333        buf
334    }
335
336    #[test]
337    fn parses_root_info_correctly() {
338        let buf = synth_root_block();
339        let info = parse_root_info(&buf).expect("parse");
340        assert_eq!(info.hash_version, HashVersion::Tea);
341        assert_eq!(info.info_length, 8);
342        assert_eq!(info.indirect_levels, 0);
343    }
344
345    #[test]
346    fn parses_root_entries_count_2() {
347        let buf = synth_root_block();
348        let (cl, entries) = parse_root_entries(&buf).expect("parse entries");
349        assert_eq!(cl.count, 2);
350        assert_eq!(cl.limit, 200);
351        assert_eq!(entries.len(), 2);
352        assert_eq!(entries[0].block, 1);
353        assert_eq!(entries[1].block, 2);
354        assert_eq!(entries[1].hash, 0x8000_0000);
355    }
356
357    #[test]
358    fn find_entry_for_hash_picks_correct_slot() {
359        let buf = synth_root_block();
360        let (_cl, entries) = parse_root_entries(&buf).unwrap();
361        // Low hash → slot 0 (block 1)
362        let e = find_entry_for_hash(&entries, 0x1000);
363        assert_eq!(e.block, 1);
364        // High hash → slot 1 (block 2)
365        let e = find_entry_for_hash(&entries, 0x9000_0000);
366        assert_eq!(e.block, 2);
367        // Exact boundary → slot 1
368        let e = find_entry_for_hash(&entries, 0x8000_0000);
369        assert_eq!(e.block, 2);
370    }
371
372    #[test]
373    fn lookup_leaf_zero_indirect_picks_correct_block() {
374        let buf = synth_root_block();
375        let seed = [0u32; 4];
376        // The closure won't be called when indirect_levels == 0.
377        let result = lookup_leaf(b"any-name", &buf, &seed, |_| {
378            unreachable!("should not descend with 0 indirect levels")
379        })
380        .expect("lookup");
381        assert!(result.is_some());
382        // We can't predict which block deterministically because the hash
383        // depends on the name + seed, but it must be 1 or 2.
384        let block = result.unwrap();
385        assert!(block == 1 || block == 2, "expected 1 or 2, got {block}");
386    }
387
388    #[test]
389    fn rejects_bad_hash_version() {
390        let mut buf = synth_root_block();
391        buf[28] = 99; // unknown version
392        let result = parse_root_info(&buf);
393        assert!(matches!(result, Err(Error::Corrupt(_))));
394    }
395
396    #[test]
397    fn rejects_nonzero_reserved() {
398        let mut buf = synth_root_block();
399        buf[24] = 1; // corrupt reserved field
400        let result = parse_root_info(&buf);
401        assert!(matches!(result, Err(Error::Corrupt(_))));
402    }
403}