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}