Skip to main content

fs_ext4/
htree_mut.rs

1//! Htree rebalance — planning layer (E9).
2//!
3//! Produces typed [`HtreeMutation`] values describing how to rebalance an
4//! indexed directory after `dir::add_entry_to_block` fails with `OutOfBounds`
5//! on a full leaf block. Does NOT write to disk; E10 composes into file-level
6//! writes, E11 journals.
7//!
8//! Scope of the initial landing:
9//! - **Leaf split** — the common case. A leaf block that can't fit one more
10//!   entry is split in two at the median hash. Caller allocates a new logical
11//!   block (via E5) and calls `plan_leaf_split` to get the two leaves' bytes
12//!   + the hash→block pair that must be inserted in the parent.
13//! - **DX entry insert into root** — once the caller has the new leaf's
14//!   hash bound + block number, `plan_insert_dx_entry_root` (or `_node`)
15//!   emits the updated parent-block bytes. If the parent overflows, we
16//!   return `NEEDS_PARENT_SPLIT` so the caller can split the parent too
17//!   (or, for depth-1 root → depth-2 promotion, fail clearly).
18//!
19//! `Filesystem::buffer_split_htree_leaf_and_add_entry` composes the two for a
20//! create into a full leaf (#195); where the parent is full it drops the
21//! index instead.
22//!
23//! Deferred to future iterations:
24//! - Intermediate-node split (parent full while root still has room).
25//! - Depth-0 → depth-1 promotion (inline root out of room; no intermediate
26//!   level exists yet).
27//! - Depth-1 → depth-2 promotion (large-dir feature).
28//! - Leaf merge on delete.
29
30use crate::error::{Error, Result};
31use crate::hash::{name_hash, HashVersion, NameHash};
32use crate::htree::{DxCountLimit, DxEntry};
33
34/// Primitive change to the directory tree. The caller (E10 or a test) turns
35/// these into real I/O under a JBD2 transaction (E11).
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum HtreeMutation {
38    /// Overwrite an existing directory leaf/root/intermediate block.
39    /// `logical_block` is the dir-file-relative block index.
40    WriteDirBlock { logical_block: u32, bytes: Vec<u8> },
41    /// Allocate a NEW logical block at the tail of the dir file and write
42    /// these bytes there. The caller also needs to extend the inode size +
43    /// extent tree via E7 to make the new logical block mapped.
44    AppendNewDirBlock { bytes: Vec<u8> },
45}
46
47/// Result of a leaf-split planning call. The caller still needs to:
48///   1. Allocate a new logical block for `new_leaf` (extent + inode size).
49///   2. Apply both `WriteDirBlock` mutations.
50///   3. Call `plan_insert_dx_entry_root` / `_node` on the parent with
51///      `split_out_hash` and the new leaf's logical block number.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct LeafSplit {
54    /// Updated bytes of the original (left) leaf block. Contains entries with
55    /// hash < `split_out_hash`.
56    pub left_bytes: Vec<u8>,
57    /// Bytes of the new (right) leaf block. Contains entries with hash >=
58    /// `split_out_hash`. Caller writes to the freshly-allocated block.
59    pub right_bytes: Vec<u8>,
60    /// Hash bound for the new right-leaf — this is the dx_entry.hash the
61    /// caller must install in the parent when routing the new leaf into the
62    /// tree.
63    pub split_out_hash: u32,
64}
65
66/// Read the packed entries from a linear dir leaf into a vector of
67/// (hash, raw entry bytes including trailing pad). `reserved_tail` is the
68/// number of bytes at the end that are reserved for the metadata-csum tail
69/// entry (typically 12) — those bytes are preserved verbatim in the output
70/// blocks. `has_file_type` follows the ext4 FILETYPE feature (almost always
71/// true in modern fs).
72fn read_leaf_entries(
73    leaf: &[u8],
74    has_file_type: bool,
75    hash_version: HashVersion,
76    hash_seed: &[u32; 4],
77    reserved_tail: usize,
78) -> Result<Vec<(NameHash, Vec<u8>)>> {
79    let usable = leaf
80        .len()
81        .checked_sub(reserved_tail)
82        .ok_or(Error::OutOfBounds)?;
83    let mut out = Vec::new();
84    let mut off = 0usize;
85    while off + 8 <= usable {
86        let cur_inode = u32::from_le_bytes(leaf[off..off + 4].try_into().unwrap());
87        let rec_len = u16::from_le_bytes(leaf[off + 4..off + 6].try_into().unwrap()) as usize;
88        if rec_len < 8 || !rec_len.is_multiple_of(4) || off + rec_len > usable {
89            return Err(Error::CorruptDirEntry(
90                "bad rec_len during htree leaf split",
91            ));
92        }
93
94        if cur_inode != 0 {
95            let name_len_lo = leaf[off + 6];
96            let type_or_hi = leaf[off + 7];
97            let name_len = if has_file_type {
98                name_len_lo as usize
99            } else {
100                ((type_or_hi as usize) << 8) | name_len_lo as usize
101            };
102            if 8 + name_len > rec_len {
103                return Err(Error::CorruptDirEntry(
104                    "name overflows rec_len in htree split",
105                ));
106            }
107            let name = &leaf[off + 8..off + 8 + name_len];
108            let h = name_hash(name, hash_version, hash_seed);
109            // Copy just the minimum needed: 8-byte header + name + pad to 4.
110            let padded = 8 + ((name_len + 3) & !3);
111            let mut entry_bytes = vec![0u8; padded];
112            entry_bytes[..8].copy_from_slice(&leaf[off..off + 8]);
113            entry_bytes[8..8 + name_len].copy_from_slice(name);
114            // The rec_len inside entry_bytes is still the old one — the caller
115            // fixes it up when emitting into the new block.
116            out.push((h, entry_bytes));
117        }
118        off += rec_len;
119    }
120    Ok(out)
121}
122
123/// Write a packed list of entries into a fresh leaf block, setting correct
124/// `rec_len`s (last entry absorbs the remaining space up to the tail).
125fn write_packed_leaf(
126    entries: &[&[u8]],
127    block_size: usize,
128    reserved_tail: usize,
129) -> Result<Vec<u8>> {
130    let usable = block_size
131        .checked_sub(reserved_tail)
132        .ok_or(Error::OutOfBounds)?;
133    let mut out = vec![0u8; block_size];
134    let mut off = 0usize;
135    for (i, raw) in entries.iter().enumerate() {
136        let is_last = i + 1 == entries.len();
137        let padded = raw.len();
138        if off + padded > usable {
139            return Err(Error::OutOfBounds);
140        }
141        // rec_len: either exact padded size, or (for the last entry) all the
142        // remaining space up to the tail. Matches kernel layout.
143        let rec_len = if is_last { usable - off } else { padded };
144        out[off..off + raw.len()].copy_from_slice(raw);
145        // Patch rec_len in the copied header.
146        out[off + 4..off + 6].copy_from_slice(&(rec_len as u16).to_le_bytes());
147        off += rec_len;
148    }
149    // Tail region preserved as zeros — caller re-applies metadata-csum tail
150    // separately after JBD2 commit.
151    Ok(out)
152}
153
154/// Plan a leaf split. Reads `leaf_bytes`, rehashes names, sorts by hash,
155/// cuts at the median, emits two balanced leaves + the hash bound for the
156/// parent dx_entry.
157///
158/// Caller contract: this only rebuilds the two leaf BLOCKS. Updating the
159/// parent's dx_entry array is a separate call (`plan_insert_dx_entry_*`).
160pub fn plan_leaf_split(
161    leaf_bytes: &[u8],
162    hash_version: HashVersion,
163    hash_seed: &[u32; 4],
164    has_file_type: bool,
165    block_size: usize,
166    reserved_tail: usize,
167) -> Result<LeafSplit> {
168    let mut entries = read_leaf_entries(
169        leaf_bytes,
170        has_file_type,
171        hash_version,
172        hash_seed,
173        reserved_tail,
174    )?;
175    if entries.len() < 2 {
176        return Err(Error::Corrupt(
177            "htree leaf split needs >=2 entries; caller's overflow may be spurious",
178        ));
179    }
180    // Sort stably by major hash.
181    entries.sort_by_key(|(h, _)| h.major);
182
183    // Split nearest the midpoint, but never between two names with the same
184    // hash: a lookup for that hash descends to the right leaf only, and the
185    // kernel marks such a split with the continuation bit so readers also
186    // try the left. Cutting at a hash change needs no bit (#195).
187    let half = entries.len() / 2;
188    let mid = (0..entries.len())
189        .flat_map(|d| [half + d, half.wrapping_sub(d)])
190        .find(|&m| m > 0 && m < entries.len() && entries[m - 1].0.major != entries[m].0.major)
191        .ok_or(Error::Corrupt(
192            "htree leaf split: every name in the leaf has the same hash",
193        ))?;
194    let split_out_hash = entries[mid].0.major;
195    let (left, right) = entries.split_at(mid);
196    let left_bytes = write_packed_leaf(
197        &left.iter().map(|(_, b)| b.as_slice()).collect::<Vec<_>>(),
198        block_size,
199        reserved_tail,
200    )?;
201    let right_bytes = write_packed_leaf(
202        &right.iter().map(|(_, b)| b.as_slice()).collect::<Vec<_>>(),
203        block_size,
204        reserved_tail,
205    )?;
206    Ok(LeafSplit {
207        left_bytes,
208        right_bytes,
209        split_out_hash,
210    })
211}
212
213/// Insert a new `(hash, block)` routing entry into a dx_root's entry array.
214/// Returns the updated root block bytes. Errors with `NEEDS_PARENT_SPLIT`
215/// when the root is saturated (count == limit).
216///
217/// `cl_offset` is the offset at which the `DxCountLimit` lives in the root
218/// (24 + info_length; info_length is always 8 in practice, so 32).
219pub fn plan_insert_dx_entry_root(
220    root_bytes: &[u8],
221    cl_offset: usize,
222    new_hash: u32,
223    new_block: u32,
224) -> Result<Vec<u8>> {
225    plan_insert_dx_entry_generic(root_bytes, cl_offset, new_hash, new_block)
226}
227
228/// Insert a new `(hash, block)` routing entry into an intermediate dx_node.
229/// Intermediate nodes have the `DxCountLimit` at offset 8 (after the single
230/// fake dir entry header).
231pub fn plan_insert_dx_entry_node(
232    node_bytes: &[u8],
233    new_hash: u32,
234    new_block: u32,
235) -> Result<Vec<u8>> {
236    plan_insert_dx_entry_generic(node_bytes, 8, new_hash, new_block)
237}
238
239fn plan_insert_dx_entry_generic(
240    block: &[u8],
241    cl_offset: usize,
242    new_hash: u32,
243    new_block: u32,
244) -> Result<Vec<u8>> {
245    if cl_offset + DxEntry::SIZE > block.len() {
246        return Err(Error::Corrupt("dx cl_offset out of range"));
247    }
248    let cl = DxCountLimit::parse(&block[cl_offset..cl_offset + 4])?;
249    if cl.count >= cl.limit {
250        return Err(Error::CorruptExtentTree("NEEDS_PARENT_SPLIT: dx node full"));
251    }
252    if cl.count == 0 {
253        return Err(Error::Corrupt("dx node has zero entries"));
254    }
255
256    // THE ARRAY STARTS AT THE COUNT/LIMIT PAIR. Entry 0's hash slot holds
257    // (limit, count) and its block follows, as `htree::parse_root_entries`
258    // reads it and the kernel writes it. This planned the array from four
259    // bytes later, one hash/block pair out of step, which only its own
260    // synthetic tests agreed with (#195).
261    let entry = |i: usize| -> Result<DxEntry> {
262        let off = cl_offset + i * DxEntry::SIZE;
263        if off + DxEntry::SIZE > block.len() {
264            return Err(Error::Corrupt("dx entries overrun block"));
265        }
266        DxEntry::parse(&block[off..off + DxEntry::SIZE])
267    };
268    let mut entries = (0..cl.count as usize)
269        .map(entry)
270        .collect::<Result<Vec<_>>>()?;
271    entries[0].hash = 0;
272
273    // Reject duplicate hash (kernel allows it by convention but here we want
274    // strict uniqueness for tests — real callers get unique hashes from a
275    // fresh leaf split).
276    if entries[1..].iter().any(|e| e.hash == new_hash) {
277        return Err(Error::CorruptExtentTree("duplicate dx hash insert"));
278    }
279
280    // Insert at sorted position after the slot-0 sentinel.
281    let pos = entries
282        .iter()
283        .skip(1)
284        .position(|e| e.hash > new_hash)
285        .map(|p| p + 1)
286        .unwrap_or(entries.len());
287    entries.insert(
288        pos,
289        DxEntry {
290            hash: new_hash,
291            block: new_block,
292        },
293    );
294
295    let mut out = block.to_vec();
296    if cl_offset + entries.len() * DxEntry::SIZE > out.len() {
297        return Err(Error::Corrupt("dx serialized entries exceed block"));
298    }
299    for (i, e) in entries.iter().enumerate() {
300        let off = cl_offset + i * DxEntry::SIZE;
301        if i > 0 {
302            out[off..off + 4].copy_from_slice(&e.hash.to_le_bytes());
303        }
304        out[off + 4..off + 8].copy_from_slice(&e.block.to_le_bytes());
305    }
306    out[cl_offset + 2..cl_offset + 4].copy_from_slice(&(cl.count + 1).to_le_bytes());
307    Ok(out)
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use crate::dir::{add_entry_to_block, DirEntryType};
314
315    /// Compose a fresh leaf populated with `count` small entries named
316    /// "file_<i>" with inode `100 + i`.
317    fn make_populated_leaf(count: usize, block_size: usize, reserved_tail: usize) -> Vec<u8> {
318        let mut buf = vec![0u8; block_size];
319        // Seed the block with one oversized initial entry so add_entry_to_block
320        // has something to split against.
321        let usable = block_size - reserved_tail;
322        buf[0..4].copy_from_slice(&1u32.to_le_bytes());
323        buf[4..6].copy_from_slice(&(usable as u16).to_le_bytes());
324        buf[6] = 1;
325        buf[7] = DirEntryType::RegFile as u8;
326        buf[8] = b'a';
327        for i in 0..count {
328            let name = format!("file_{}", i);
329            add_entry_to_block(
330                &mut buf,
331                100 + i as u32,
332                name.as_bytes(),
333                DirEntryType::RegFile,
334                true,
335                reserved_tail,
336            )
337            .unwrap();
338        }
339        buf
340    }
341
342    #[test]
343    fn leaf_split_balances_entries() {
344        let leaf = make_populated_leaf(40, 4096, 0);
345        let split = plan_leaf_split(&leaf, HashVersion::HalfMd4, &[0; 4], true, 4096, 0).unwrap();
346        // Both halves should be valid leaf blocks of the full size.
347        assert_eq!(split.left_bytes.len(), 4096);
348        assert_eq!(split.right_bytes.len(), 4096);
349        // The split_out_hash sits at the boundary — every right-side entry
350        // should rehash >= it, every left-side entry < it.
351        let left_entries =
352            read_leaf_entries(&split.left_bytes, true, HashVersion::HalfMd4, &[0; 4], 0).unwrap();
353        let right_entries =
354            read_leaf_entries(&split.right_bytes, true, HashVersion::HalfMd4, &[0; 4], 0).unwrap();
355        for (h, _) in &left_entries {
356            assert!(
357                h.major < split.split_out_hash,
358                "left entry hash {} >= split_out {}",
359                h.major,
360                split.split_out_hash
361            );
362        }
363        for (h, _) in &right_entries {
364            assert!(
365                h.major >= split.split_out_hash,
366                "right entry hash {} < split_out {}",
367                h.major,
368                split.split_out_hash
369            );
370        }
371        // Both halves populated (not all-in-one).
372        assert!(!left_entries.is_empty());
373        assert!(!right_entries.is_empty());
374    }
375
376    #[test]
377    fn leaf_split_rejects_trivial() {
378        // Only one entry — split has no meaning.
379        let leaf = make_populated_leaf(0, 4096, 0); // just the initial "a" seed
380        let err = plan_leaf_split(&leaf, HashVersion::HalfMd4, &[0; 4], true, 4096, 0).unwrap_err();
381        match err {
382            Error::Corrupt(msg) => assert!(msg.contains(">=2 entries")),
383            _ => panic!("wrong error kind"),
384        }
385    }
386
387    /// Build a synthetic dx root with `count` entries.
388    /// A leaf holding `names`, in order.
389    fn leaf_of(names: &[&[u8]]) -> Vec<u8> {
390        let mut buf = vec![0u8; 4096];
391        buf[4..6].copy_from_slice(&4096u16.to_le_bytes());
392        for (i, name) in names.iter().enumerate() {
393            add_entry_to_block(
394                &mut buf,
395                100 + i as u32,
396                name,
397                DirEntryType::RegFile,
398                true,
399                0,
400            )
401            .unwrap();
402        }
403        buf
404    }
405
406    /// The cut never falls between two names with one hash: a lookup for
407    /// that hash reads the right leaf only (#195).
408    #[test]
409    fn leaf_split_does_not_divide_a_hash() {
410        let seed = [0u32; 4];
411        // Four copies of one name (one hash) and one other: the midpoint is
412        // inside the run, so the cut moves to its edge.
413        let leaf = leaf_of(&[b"same", b"same", b"same", b"same", b"other"]);
414        let split = plan_leaf_split(&leaf, HashVersion::HalfMd4, &seed, true, 4096, 0).unwrap();
415        let same = name_hash(b"same", HashVersion::HalfMd4, &seed).major;
416        let left = crate::dir::parse_block(&split.left_bytes, true).unwrap();
417        let right = crate::dir::parse_block(&split.right_bytes, true).unwrap();
418        let sides =
419            |entries: &[crate::dir::DirEntry]| entries.iter().filter(|e| e.name == b"same").count();
420        assert!(
421            sides(&left) == 0 || sides(&right) == 0,
422            "the four names with hash {same:#x} were divided between the leaves"
423        );
424
425        let leaf = leaf_of(&[b"same", b"same", b"same"]);
426        assert!(plan_leaf_split(&leaf, HashVersion::HalfMd4, &seed, true, 4096, 0).is_err());
427    }
428
429    fn make_dx_root(count: u16, limit: u16) -> Vec<u8> {
430        let mut buf = vec![0u8; 4096];
431        // Fake "." at 0..12
432        buf[0..4].copy_from_slice(&2u32.to_le_bytes());
433        buf[4..6].copy_from_slice(&12u16.to_le_bytes());
434        buf[6] = 1;
435        buf[7] = DirEntryType::Directory as u8;
436        buf[8] = b'.';
437        // Fake ".." at 12..24
438        buf[12..16].copy_from_slice(&2u32.to_le_bytes());
439        buf[16..18].copy_from_slice(&(4096u16 - 12).to_le_bytes());
440        buf[18] = 2;
441        buf[19] = DirEntryType::Directory as u8;
442        buf[20] = b'.';
443        buf[21] = b'.';
444        // dx_root_info at 24..32: reserved(4)=0, hash_version=1, info_length=8, levels=0, flags=0
445        buf[28] = HashVersion::HalfMd4 as u8;
446        buf[29] = 8;
447        // dx_entry[0] at 32..40: (limit, count) in its hash slot, block = 1.
448        buf[32..34].copy_from_slice(&limit.to_le_bytes());
449        buf[34..36].copy_from_slice(&count.to_le_bytes());
450        buf[36..40].copy_from_slice(&1u32.to_le_bytes());
451        // dx_entry[1..count] at 32 + 8i: synthetic hashes.
452        for i in 1..count as usize {
453            let off = 32 + i * DxEntry::SIZE;
454            let hash: u32 = (i as u32) * 1000;
455            buf[off..off + 4].copy_from_slice(&hash.to_le_bytes());
456            buf[off + 4..off + 8].copy_from_slice(&((i as u32) + 1).to_le_bytes());
457        }
458        buf
459    }
460
461    /// Read back through the crate's reader, which lays the array out as
462    /// the kernel does.
463    fn entries(root: &[u8]) -> Vec<(u32, u32)> {
464        let (_, entries) = crate::htree::parse_root_entries(root).unwrap();
465        entries.iter().map(|e| (e.hash, e.block)).collect()
466    }
467
468    #[test]
469    fn dx_entry_insert_root_preserves_sort() {
470        // count=3 entries: hash=0, 1000, 2000 → insert 1500 → should land at slot 2.
471        let root = make_dx_root(3, 500);
472        let updated = plan_insert_dx_entry_root(&root, 32, 1500, 42).unwrap();
473        assert_eq!(
474            entries(&updated),
475            [(0, 1), (1000, 2), (1500, 42), (2000, 3)]
476        );
477        let limit = u16::from_le_bytes(updated[32..34].try_into().unwrap());
478        assert_eq!(limit, 500, "the limit in entry 0's hash slot is kept");
479    }
480
481    #[test]
482    fn dx_entry_insert_at_end() {
483        let root = make_dx_root(3, 500);
484        let updated = plan_insert_dx_entry_root(&root, 32, 9999, 77).unwrap();
485        assert_eq!(
486            entries(&updated),
487            [(0, 1), (1000, 2), (2000, 3), (9999, 77)]
488        );
489    }
490
491    #[test]
492    fn dx_entry_insert_rejects_full_node() {
493        let root = make_dx_root(5, 5); // count==limit
494        let err = plan_insert_dx_entry_root(&root, 32, 1234, 10).unwrap_err();
495        match err {
496            Error::CorruptExtentTree(msg) => assert!(msg.contains("NEEDS_PARENT_SPLIT")),
497            _ => panic!("wrong error kind"),
498        }
499    }
500
501    #[test]
502    fn dx_entry_insert_rejects_duplicate_hash() {
503        let root = make_dx_root(3, 500);
504        let err = plan_insert_dx_entry_root(&root, 32, 1000, 42).unwrap_err();
505        match err {
506            Error::CorruptExtentTree(msg) => assert!(msg.contains("duplicate")),
507            _ => panic!("wrong error kind"),
508        }
509    }
510}