Skip to main content

fs_ext4/
indirect_mut.rs

1//! Mutation primitives for the legacy direct/indirect block-pointer scheme
2//! used by ext2 / ext3 (and ext4 inodes that opt out of extents).
3//!
4//! Two pure-data planners — neither touches the device or the global inode
5//! state. The caller is responsible for: invoking the allocator, marking the
6//! allocated blocks in the on-disk block bitmap, persisting the indirect
7//! tree's block writes, updating BGD/SB counters, and finally publishing the
8//! new inode.
9//!
10//! Companions to `extent_mut.rs`. Same architectural split: planning is pure
11//! and unit-testable; the IO/concurrency story lives in `fs.rs`.
12//!
13//! Spec source: kernel.org/doc/html/latest/filesystems/ext4/blockmap.html
14//! (the same scheme ext4 inherited from ext2/ext3) and Carrier,
15//! *File System Forensic Analysis*, ch. 14.
16
17use crate::block_io::BlockDevice;
18use crate::error::{Error, Result};
19
20/// Number of direct block pointers in `i_block` (slots 0..=11).
21pub const DIRECT_COUNT: u32 = 12;
22
23#[inline]
24fn ppb(block_size: u32) -> u32 {
25    block_size / 4
26}
27
28#[inline]
29fn write_u32_le(buf: &mut [u8], idx: usize, val: u32) {
30    let off = idx * 4;
31    buf[off..off + 4].copy_from_slice(&val.to_le_bytes());
32}
33
34#[inline]
35fn read_u32_le(buf: &[u8], idx: usize) -> u32 {
36    let off = idx * 4;
37    u32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
38}
39
40/// Plan output: the new `i_block` region, plus the indirect-block writes the
41/// caller MUST persist before publishing the inode, plus the list of physical
42/// blocks the planner allocated for the indirect tree (which the caller must
43/// mark in the block bitmap and credit against BGD/SB counters).
44///
45/// The data blocks themselves are NOT in `block_writes` — `plan_contiguous`
46/// does not own data; it only tracks the indirect-tree metadata blocks. The
47/// caller already knows the data block range and writes the payload there
48/// directly.
49#[derive(Debug, Clone)]
50pub struct IndirectPlan {
51    pub i_block: [u8; 60],
52    pub block_writes: Vec<(u64, Vec<u8>)>,
53    pub indirect_blocks_allocated: Vec<u64>,
54}
55
56/// Count the indirect-tree metadata blocks needed to address `data_blocks`
57/// data blocks via the legacy direct/indirect scheme at `block_size`.
58///
59/// This lets a caller pre-allocate one contiguous run sized for both the
60/// data payload AND the indirect tree, eliminating a second round-trip
61/// through the block allocator.
62///
63/// Counts (for `ppb = block_size / 4`):
64/// - 0 if `data_blocks <= 12`
65/// - 1 if `data_blocks <= 12 + ppb` (one single-indirect block)
66/// - `1 + 1 + ceil((data_blocks - 12 - ppb) / ppb)` for the double tier
67///   (single-indirect + double-outer + double-inners)
68/// - additional L1 + ceil(remaining / ppb²) L2 + ceil(remaining / ppb) L3
69///   for the triple tier
70pub fn count_indirect_blocks(data_blocks: u32, block_size: u32) -> u64 {
71    let ppb = (block_size / 4) as u64;
72    if ppb == 0 {
73        return 0;
74    }
75    let mut remaining = data_blocks as u64;
76    let mut count: u64 = 0;
77
78    // Tier 1 (direct): no indirect blocks.
79    if remaining <= DIRECT_COUNT as u64 {
80        return 0;
81    }
82    remaining -= DIRECT_COUNT as u64;
83
84    // Tier 2 (single indirect): 1 indirect block holds up to ppb pointers.
85    count += 1;
86    if remaining <= ppb {
87        return count;
88    }
89    remaining -= ppb;
90
91    // Tier 3 (double indirect): 1 outer + ceil(remaining_in_tier / ppb) inners.
92    count += 1; // outer
93    let in_double = remaining.min(ppb * ppb);
94    count += in_double.div_ceil(ppb);
95    if remaining <= ppb * ppb {
96        return count;
97    }
98    remaining -= ppb * ppb;
99
100    // Tier 4 (triple indirect): 1 L1 + L2 layer + L3 layer.
101    count += 1; // L1
102    let in_triple = remaining.min(ppb * ppb * ppb);
103    let l2_count = in_triple.div_ceil(ppb * ppb);
104    let l3_count = in_triple.div_ceil(ppb);
105    count += l2_count + l3_count;
106    count
107}
108
109/// Plan an indirect-tree layout that maps logical blocks `0..data_block_count`
110/// to physical blocks `first_data_phys..first_data_phys+data_block_count`.
111///
112/// `alloc_one` is invoked once per indirect-tree block needed:
113/// - 0 calls for `count <= 12`
114/// - 1 call for `count <= 12 + ppb`
115/// - `1 + ceil((count - 12 - ppb) / ppb)` calls in the double-indirect tier
116/// - additional calls in the triple-indirect tier
117///
118/// All pointers are 32-bit (no `INCOMPAT_64BIT` for ext2/3 in any production
119/// deployment), so this errors if any input physical block exceeds `u32::MAX`
120/// or if `data_block_count` exceeds the triple-indirect address span.
121pub fn plan_contiguous<F>(
122    data_block_count: u32,
123    first_data_phys: u64,
124    block_size: u32,
125    mut alloc_one: F,
126) -> Result<IndirectPlan>
127where
128    F: FnMut() -> Result<u64>,
129{
130    if !block_size.is_power_of_two() || block_size < 1024 {
131        return Err(Error::InvalidArgument(
132            "indirect_mut: block_size out of range",
133        ));
134    }
135    let ppb = ppb(block_size);
136    if ppb == 0 {
137        return Err(Error::InvalidArgument(
138            "indirect_mut: block_size too small for any pointers",
139        ));
140    }
141
142    let mut plan = IndirectPlan {
143        i_block: [0u8; 60],
144        block_writes: Vec::new(),
145        indirect_blocks_allocated: Vec::new(),
146    };
147
148    // Bounds: all data block addresses must fit in u32.
149    let last_data_phys = first_data_phys
150        .checked_add(data_block_count.saturating_sub(1) as u64)
151        .ok_or(Error::InvalidArgument(
152            "indirect_mut: physical address overflow",
153        ))?;
154    if last_data_phys > u32::MAX as u64 {
155        return Err(Error::InvalidArgument(
156            "indirect_mut: data physical block exceeds u32 (ext2/3 cap)",
157        ));
158    }
159
160    let mut remaining = data_block_count;
161    let mut logical_offset: u32 = 0;
162
163    // Helper: write a contiguous run of data pointers into `buf` starting at
164    // `idx` for `n` pointers. Returns the number of pointers written.
165    let emit_data_ptrs = |buf: &mut [u8], idx: usize, n: u32, base: u32| {
166        for i in 0..n {
167            write_u32_le(buf, idx + i as usize, base + i);
168        }
169    };
170
171    // ---------- Tier 1: direct (i_block[0..12]) ----------
172    {
173        let direct_count = remaining.min(DIRECT_COUNT);
174        let base = (first_data_phys + logical_offset as u64) as u32;
175        emit_data_ptrs(&mut plan.i_block, 0, direct_count, base);
176        remaining -= direct_count;
177        logical_offset += direct_count;
178    }
179    if remaining == 0 {
180        return Ok(plan);
181    }
182
183    // ---------- Tier 2: single indirect (i_block[12]) ----------
184    {
185        let single_blk = alloc_one()?;
186        check_u32(single_blk, "single-indirect block")?;
187        plan.indirect_blocks_allocated.push(single_blk);
188        write_u32_le(&mut plan.i_block, 12, single_blk as u32);
189
190        let n = remaining.min(ppb);
191        let mut buf = vec![0u8; block_size as usize];
192        let base = (first_data_phys + logical_offset as u64) as u32;
193        emit_data_ptrs(&mut buf, 0, n, base);
194        plan.block_writes.push((single_blk, buf));
195
196        remaining -= n;
197        logical_offset += n;
198    }
199    if remaining == 0 {
200        return Ok(plan);
201    }
202
203    // ---------- Tier 3: double indirect (i_block[13]) ----------
204    {
205        let outer_blk = alloc_one()?;
206        check_u32(outer_blk, "double-indirect outer block")?;
207        plan.indirect_blocks_allocated.push(outer_blk);
208        write_u32_le(&mut plan.i_block, 13, outer_blk as u32);
209
210        // Cap this tier's contribution at ppb*ppb data pointers.
211        let tier_capacity = ppb as u64 * ppb as u64;
212        let tier_count = (remaining as u64).min(tier_capacity) as u32;
213        let inner_blocks_needed = tier_count.div_ceil(ppb);
214
215        let mut outer_buf = vec![0u8; block_size as usize];
216        for outer_idx in 0..inner_blocks_needed {
217            let inner_blk = alloc_one()?;
218            check_u32(inner_blk, "double-indirect inner block")?;
219            plan.indirect_blocks_allocated.push(inner_blk);
220            write_u32_le(&mut outer_buf, outer_idx as usize, inner_blk as u32);
221
222            let inner_count = remaining.min(ppb);
223            let mut inner_buf = vec![0u8; block_size as usize];
224            let base = (first_data_phys + logical_offset as u64) as u32;
225            emit_data_ptrs(&mut inner_buf, 0, inner_count, base);
226            plan.block_writes.push((inner_blk, inner_buf));
227
228            remaining -= inner_count;
229            logical_offset += inner_count;
230        }
231        plan.block_writes.push((outer_blk, outer_buf));
232    }
233    if remaining == 0 {
234        return Ok(plan);
235    }
236
237    // ---------- Tier 4: triple indirect (i_block[14]) ----------
238    {
239        let l1_blk = alloc_one()?;
240        check_u32(l1_blk, "triple-indirect L1 block")?;
241        plan.indirect_blocks_allocated.push(l1_blk);
242        write_u32_le(&mut plan.i_block, 14, l1_blk as u32);
243
244        let tier_capacity = ppb as u64 * ppb as u64 * ppb as u64;
245        let tier_count = (remaining as u64).min(tier_capacity) as u32;
246        let l2_blocks_needed = tier_count.div_ceil(ppb * ppb);
247
248        let mut l1_buf = vec![0u8; block_size as usize];
249        for l1_idx in 0..l2_blocks_needed {
250            let l2_blk = alloc_one()?;
251            check_u32(l2_blk, "triple-indirect L2 block")?;
252            plan.indirect_blocks_allocated.push(l2_blk);
253            write_u32_le(&mut l1_buf, l1_idx as usize, l2_blk as u32);
254
255            // L2 block holds up to ppb pointers; each points at an L3 block
256            // holding up to ppb data pointers.
257            let l3_capacity_under_this_l2 = ppb as u64 * ppb as u64;
258            let l3_count = (remaining as u64).min(l3_capacity_under_this_l2) as u32;
259            let l3_blocks_needed = l3_count.div_ceil(ppb);
260
261            let mut l2_buf = vec![0u8; block_size as usize];
262            for l2_idx in 0..l3_blocks_needed {
263                let l3_blk = alloc_one()?;
264                check_u32(l3_blk, "triple-indirect L3 block")?;
265                plan.indirect_blocks_allocated.push(l3_blk);
266                write_u32_le(&mut l2_buf, l2_idx as usize, l3_blk as u32);
267
268                let inner_count = remaining.min(ppb);
269                let mut l3_buf = vec![0u8; block_size as usize];
270                let base = (first_data_phys + logical_offset as u64) as u32;
271                emit_data_ptrs(&mut l3_buf, 0, inner_count, base);
272                plan.block_writes.push((l3_blk, l3_buf));
273
274                remaining -= inner_count;
275                logical_offset += inner_count;
276            }
277            plan.block_writes.push((l2_blk, l2_buf));
278        }
279        plan.block_writes.push((l1_blk, l1_buf));
280    }
281
282    if remaining > 0 {
283        return Err(Error::InvalidArgument(
284            "indirect_mut: data block count exceeds triple-indirect address space",
285        ));
286    }
287    Ok(plan)
288}
289
290fn check_u32(v: u64, what: &'static str) -> Result<()> {
291    if v > u32::MAX as u64 {
292        // Static string only — Error::Corrupt takes &'static str. Use a
293        // generic message; the `what` is for future logging once we have it.
294        let _ = what;
295        return Err(Error::Corrupt(
296            "indirect_mut: indirect block address exceeds u32 (ext2/3 cap)",
297        ));
298    }
299    Ok(())
300}
301
302/// Coalesced run of contiguous data blocks slated for freeing.
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub struct FreeRun {
305    pub start: u64,
306    pub len: u32,
307}
308
309/// Lists a truncate-to-zero produces:
310/// - `data_runs`: data blocks, coalesced into contiguous runs (caller frees
311///   via the block bitmap; `start..start+len` clears one bitmap range).
312/// - `indirect_blocks`: every indirect-tree block reachable from the inode
313///   (single block freed each — they're rarely contiguous with each other
314///   or with the data, so leaving them as singletons is correct).
315#[derive(Debug, Clone, Default)]
316pub struct FreedBlocks {
317    pub data_runs: Vec<FreeRun>,
318    pub indirect_blocks: Vec<u64>,
319}
320
321/// Walk the indirect tree rooted in `i_block` and gather every physical
322/// block reachable from it (data + indirect). Used by truncate-to-zero
323/// before zeroing the inode's block-pointer region.
324///
325/// Sparse holes (zero pointers) are skipped — they reference no on-disk
326/// blocks, so there is nothing to free.
327///
328/// `expected_data_blocks` lets the caller cap the walk at the file's
329/// declared size (so trailing tier capacity beyond `inode.size` doesn't
330/// dredge up garbage pointers from beyond-EOF). Pass `u32::MAX` to walk
331/// the full address space.
332pub fn collect_for_free(
333    i_block: &[u8; 60],
334    block_size: u32,
335    expected_data_blocks: u32,
336    dev: &dyn BlockDevice,
337) -> Result<FreedBlocks> {
338    if !block_size.is_power_of_two() || block_size < 1024 {
339        return Err(Error::InvalidArgument(
340            "indirect_mut: block_size out of range",
341        ));
342    }
343    let ppb = ppb(block_size);
344
345    let mut data_blocks: Vec<u64> = Vec::new();
346    let mut indirect_blocks: Vec<u64> = Vec::new();
347    let mut remaining = expected_data_blocks as u64;
348
349    // Tier 1: direct. The first 12 logical blocks live inline.
350    let direct_count = (remaining.min(DIRECT_COUNT as u64)) as usize;
351    for i in 0..direct_count {
352        let p = read_u32_le(i_block, i) as u64;
353        if p != 0 {
354            data_blocks.push(p);
355        }
356    }
357    remaining = remaining.saturating_sub(direct_count as u64);
358
359    // Tier 2: single indirect.
360    let single_blk = read_u32_le(i_block, 12) as u64;
361    if remaining > 0 && single_blk != 0 {
362        indirect_blocks.push(single_blk);
363        let buf = read_block(dev, single_blk, block_size)?;
364        let take = remaining.min(ppb as u64) as usize;
365        for i in 0..take {
366            let p = read_u32_le(&buf, i) as u64;
367            if p != 0 {
368                data_blocks.push(p);
369            }
370        }
371        remaining = remaining.saturating_sub(take as u64);
372    } else {
373        remaining = remaining.saturating_sub(ppb as u64);
374    }
375
376    // Tier 3: double indirect.
377    let double_blk = read_u32_le(i_block, 13) as u64;
378    if remaining > 0 && double_blk != 0 {
379        indirect_blocks.push(double_blk);
380        let outer = read_block(dev, double_blk, block_size)?;
381        let outer_take = remaining.min(ppb as u64 * ppb as u64).div_ceil(ppb as u64) as usize;
382        for outer_idx in 0..outer_take {
383            let inner_blk = read_u32_le(&outer, outer_idx) as u64;
384            if inner_blk == 0 {
385                remaining = remaining.saturating_sub(ppb as u64);
386                continue;
387            }
388            indirect_blocks.push(inner_blk);
389            let inner = read_block(dev, inner_blk, block_size)?;
390            let take = remaining.min(ppb as u64) as usize;
391            for i in 0..take {
392                let p = read_u32_le(&inner, i) as u64;
393                if p != 0 {
394                    data_blocks.push(p);
395                }
396            }
397            remaining = remaining.saturating_sub(take as u64);
398        }
399    } else {
400        remaining = remaining.saturating_sub(ppb as u64 * ppb as u64);
401    }
402
403    // Tier 4: triple indirect.
404    let triple_blk = read_u32_le(i_block, 14) as u64;
405    if remaining > 0 && triple_blk != 0 {
406        indirect_blocks.push(triple_blk);
407        let l1 = read_block(dev, triple_blk, block_size)?;
408        let l2_count = remaining
409            .min(ppb as u64 * ppb as u64 * ppb as u64)
410            .div_ceil(ppb as u64 * ppb as u64) as usize;
411        for l1_idx in 0..l2_count {
412            let l2_blk = read_u32_le(&l1, l1_idx) as u64;
413            if l2_blk == 0 {
414                remaining = remaining.saturating_sub(ppb as u64 * ppb as u64);
415                continue;
416            }
417            indirect_blocks.push(l2_blk);
418            let l2 = read_block(dev, l2_blk, block_size)?;
419            let l3_count = remaining.min(ppb as u64 * ppb as u64).div_ceil(ppb as u64) as usize;
420            for l2_idx in 0..l3_count {
421                let l3_blk = read_u32_le(&l2, l2_idx) as u64;
422                if l3_blk == 0 {
423                    remaining = remaining.saturating_sub(ppb as u64);
424                    continue;
425                }
426                indirect_blocks.push(l3_blk);
427                let l3 = read_block(dev, l3_blk, block_size)?;
428                let take = remaining.min(ppb as u64) as usize;
429                for i in 0..take {
430                    let p = read_u32_le(&l3, i) as u64;
431                    if p != 0 {
432                        data_blocks.push(p);
433                    }
434                }
435                remaining = remaining.saturating_sub(take as u64);
436            }
437        }
438    }
439
440    // Coalesce data blocks into runs. We sort + merge contiguous addresses.
441    data_blocks.sort_unstable();
442    data_blocks.dedup();
443    let data_runs = coalesce_runs(&data_blocks);
444
445    // Indirect blocks stay as singletons — they're scattered metadata.
446    indirect_blocks.sort_unstable();
447    indirect_blocks.dedup();
448
449    Ok(FreedBlocks {
450        data_runs,
451        indirect_blocks,
452    })
453}
454
455fn read_block(dev: &dyn BlockDevice, block_no: u64, block_size: u32) -> Result<Vec<u8>> {
456    let mut buf = vec![0u8; block_size as usize];
457    let off = block_no
458        .checked_mul(block_size as u64)
459        .ok_or(Error::Corrupt("indirect_mut: block byte offset overflow"))?;
460    dev.read_at(off, &mut buf)?;
461    Ok(buf)
462}
463
464fn coalesce_runs(sorted: &[u64]) -> Vec<FreeRun> {
465    let mut out: Vec<FreeRun> = Vec::new();
466    for &b in sorted {
467        if let Some(last) = out.last_mut() {
468            if last.start + last.len as u64 == b {
469                last.len += 1;
470                continue;
471            }
472        }
473        out.push(FreeRun { start: b, len: 1 });
474    }
475    out
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::block_io::BlockDevice;
482    use std::sync::Mutex;
483
484    /// In-memory block device for tests. Mirrors the helper in `indirect.rs`
485    /// — kept local rather than shared because both modules want to stay
486    /// usable as standalone units.
487    struct MemDev {
488        data: Mutex<Vec<u8>>,
489    }
490
491    impl MemDev {
492        fn new(size: usize) -> Self {
493            Self {
494                data: Mutex::new(vec![0u8; size]),
495            }
496        }
497        fn write_block(&self, block_no: u64, block_size: u32, contents: &[u8]) {
498            let mut d = self.data.lock().unwrap();
499            let off = (block_no as usize) * (block_size as usize);
500            d[off..off + contents.len()].copy_from_slice(contents);
501        }
502    }
503
504    impl BlockDevice for MemDev {
505        fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
506            let d = self.data.lock().unwrap();
507            let start = offset as usize;
508            buf.copy_from_slice(&d[start..start + buf.len()]);
509            Ok(())
510        }
511        fn size_bytes(&self) -> u64 {
512            self.data.lock().unwrap().len() as u64
513        }
514    }
515
516    /// Bump-allocator: returns sequential block numbers starting at `next`.
517    fn make_alloc(start: u64) -> impl FnMut() -> Result<u64> {
518        let mut next = start;
519        move || {
520            let v = next;
521            next += 1;
522            Ok(v)
523        }
524    }
525
526    #[test]
527    fn plan_contiguous_direct_only() {
528        let bs = 1024u32;
529        let plan = plan_contiguous(5, 100, bs, make_alloc(1000)).unwrap();
530        assert!(plan.indirect_blocks_allocated.is_empty());
531        assert!(plan.block_writes.is_empty());
532        for i in 0..5 {
533            assert_eq!(read_u32_le(&plan.i_block, i), 100 + i as u32);
534        }
535        // i_block[5..15] all zero (no remaining direct/indirect/double/triple).
536        for i in 5..15 {
537            assert_eq!(read_u32_le(&plan.i_block, i), 0);
538        }
539    }
540
541    #[test]
542    fn plan_contiguous_fills_all_direct() {
543        let bs = 1024u32;
544        let plan = plan_contiguous(12, 200, bs, make_alloc(500)).unwrap();
545        assert!(plan.indirect_blocks_allocated.is_empty());
546        assert!(plan.block_writes.is_empty());
547        for i in 0..12 {
548            assert_eq!(read_u32_le(&plan.i_block, i), 200 + i as u32);
549        }
550        // i_block[12..15] zero — no indirect tier needed.
551        for i in 12..15 {
552            assert_eq!(read_u32_le(&plan.i_block, i), 0);
553        }
554    }
555
556    #[test]
557    fn plan_contiguous_into_single_indirect() {
558        let bs = 1024u32; // ppb = 256
559                          // 12 direct + 5 single-indirect = 17 blocks total.
560        let plan = plan_contiguous(17, 300, bs, make_alloc(900)).unwrap();
561        assert_eq!(plan.indirect_blocks_allocated, vec![900]);
562        assert_eq!(plan.block_writes.len(), 1);
563        // i_block[12] points at the indirect block.
564        assert_eq!(read_u32_le(&plan.i_block, 12), 900);
565        // Inner buffer holds 5 data pointers at indices 0..=4.
566        let (blk_no, ref buf) = plan.block_writes[0];
567        assert_eq!(blk_no, 900);
568        for i in 0..5 {
569            assert_eq!(read_u32_le(buf, i), 300 + 12 + i as u32);
570        }
571        // Index 5 onward is zero (no further data).
572        assert_eq!(read_u32_le(buf, 5), 0);
573    }
574
575    #[test]
576    fn plan_contiguous_fills_single_indirect_exactly() {
577        let bs = 1024u32; // ppb = 256
578                          // 12 direct + 256 single = 268 blocks.
579        let plan = plan_contiguous(268, 1000, bs, make_alloc(50_000)).unwrap();
580        assert_eq!(plan.indirect_blocks_allocated, vec![50_000]);
581        assert_eq!(plan.block_writes.len(), 1);
582        let (_, ref buf) = plan.block_writes[0];
583        // All 256 single-indirect slots populated.
584        for i in 0..256 {
585            assert_eq!(read_u32_le(buf, i), 1000 + 12 + i as u32);
586        }
587        // i_block[13] zero — double tier not yet entered.
588        assert_eq!(read_u32_le(&plan.i_block, 13), 0);
589    }
590
591    #[test]
592    fn plan_contiguous_into_double_indirect() {
593        let bs = 1024u32; // ppb = 256
594                          // 12 direct + 256 single + 3 double = 271 blocks.
595                          // Double tier needs 1 outer + ceil(3/256)=1 inner = 2 indirect blocks.
596        let plan = plan_contiguous(271, 2000, bs, make_alloc(100_000)).unwrap();
597        // Allocations in order: single(100000), outer(100001), inner(100002).
598        assert_eq!(
599            plan.indirect_blocks_allocated,
600            vec![100_000, 100_001, 100_002]
601        );
602        assert_eq!(read_u32_le(&plan.i_block, 12), 100_000);
603        assert_eq!(read_u32_le(&plan.i_block, 13), 100_001);
604        assert_eq!(read_u32_le(&plan.i_block, 14), 0);
605
606        // Find each block's buffer.
607        let writes: std::collections::HashMap<u64, &Vec<u8>> =
608            plan.block_writes.iter().map(|(b, v)| (*b, v)).collect();
609        // Single-indirect block: 256 data pointers.
610        let single_buf = writes[&100_000];
611        for i in 0..256 {
612            assert_eq!(read_u32_le(single_buf, i), 2000 + 12 + i as u32);
613        }
614        // Double outer: ptrs[0] = 100002 (the inner block).
615        let outer_buf = writes[&100_001];
616        assert_eq!(read_u32_le(outer_buf, 0), 100_002);
617        assert_eq!(read_u32_le(outer_buf, 1), 0);
618        // Double inner: 3 data pointers (logical 268, 269, 270).
619        let inner_buf = writes[&100_002];
620        for i in 0..3 {
621            assert_eq!(read_u32_le(inner_buf, i), 2000 + 268 + i as u32);
622        }
623        assert_eq!(read_u32_le(inner_buf, 3), 0);
624    }
625
626    #[test]
627    fn plan_contiguous_into_triple_indirect() {
628        let bs = 1024u32; // ppb = 256
629                          // Fill direct + all of single + all of double, then 1 block into triple.
630                          // Single: 256, Double: 256*256 = 65536. Total before triple: 12 + 256 +
631                          // 65536 = 65804. One past = 65805.
632        let count = 65_805u32;
633        let plan = plan_contiguous(count, 500_000, bs, make_alloc(1_000_000)).unwrap();
634        // i_block[14] should be set; triple tier should have allocated:
635        //   1 single (100000)
636        //   1 double-outer (100001) + 256 double-inners (100002..100258)
637        //   1 triple-l1 + 1 triple-l2 + 1 triple-l3 = 3 more blocks
638        // Total: 1 + 1 + 256 + 3 = 261 indirect-tree blocks.
639        assert_eq!(plan.indirect_blocks_allocated.len(), 1 + 1 + 256 + 3);
640        assert_ne!(read_u32_le(&plan.i_block, 14), 0);
641    }
642
643    #[test]
644    fn plan_contiguous_rejects_overflow_beyond_triple() {
645        let bs = 1024u32; // ppb = 256 → triple cap = 12 + 256 + 65536 + 16777216 ≈ 16.8M
646        let too_big = 12 + 256 + 65_536 + (256u32 * 256 * 256) + 1;
647        // Trip the "exceeds triple-indirect" guard. We never get past the bounds
648        // check at top because data_block_count > u32::MAX would saturate;
649        // here we go just past triple capacity.
650        let result = plan_contiguous(too_big, 1, bs, make_alloc(1));
651        assert!(matches!(result, Err(Error::InvalidArgument(_))));
652    }
653
654    #[test]
655    fn count_indirect_blocks_matches_plan_contiguous() {
656        // Property: for any data_block_count, count_indirect_blocks() must
657        // equal plan_contiguous().indirect_blocks_allocated.len() — the two
658        // counts drive the same physical reality (single bitmap allocation).
659        let bs = 1024u32;
660        for n in [
661            1u32,
662            11,
663            12,
664            13,
665            12 + 256,
666            12 + 257,
667            12 + 256 + 1,
668            271,
669            1000,
670            12 + 256 + 65_536,
671            65_805,
672        ] {
673            let predicted = count_indirect_blocks(n, bs);
674            let plan = plan_contiguous(n, 1, bs, make_alloc(1)).unwrap();
675            assert_eq!(
676                predicted as usize,
677                plan.indirect_blocks_allocated.len(),
678                "mismatch for n={n}: predicted={predicted}, actual={}",
679                plan.indirect_blocks_allocated.len()
680            );
681        }
682    }
683
684    #[test]
685    fn collect_for_free_walks_tree_and_coalesces_runs() {
686        let bs = 1024u32; // ppb = 256
687        let dev = MemDev::new(1024 * 4096);
688
689        // Build an i_block with:
690        // - direct[0..5] = blocks 100..105 (contiguous run)
691        // - direct[7] = block 999 (isolated)
692        // - single-indirect at block 50, holding [0]=200, [1]=201, [2]=202
693        let mut indirect = vec![0u8; bs as usize];
694        write_u32_le(&mut indirect, 0, 200);
695        write_u32_le(&mut indirect, 1, 201);
696        write_u32_le(&mut indirect, 2, 202);
697        dev.write_block(50, bs, &indirect);
698
699        let mut i_block = [0u8; 60];
700        for i in 0..5 {
701            write_u32_le(&mut i_block, i, 100 + i as u32);
702        }
703        write_u32_le(&mut i_block, 7, 999);
704        write_u32_le(&mut i_block, 12, 50);
705
706        // 12 direct + 3 single = 15 logical blocks of file content.
707        let freed = collect_for_free(&i_block, bs, 15, &dev).unwrap();
708
709        // Indirect tree: just block 50.
710        assert_eq!(freed.indirect_blocks, vec![50]);
711
712        // Data runs: {100..105 contiguous run} + {200..203 contiguous run}
713        // + {999 singleton}. Sort order: 100, 200, 999 by start.
714        assert_eq!(
715            freed.data_runs,
716            vec![
717                FreeRun { start: 100, len: 5 },
718                FreeRun { start: 200, len: 3 },
719                FreeRun { start: 999, len: 1 },
720            ]
721        );
722    }
723
724    #[test]
725    fn collect_for_free_skips_sparse_holes() {
726        let bs = 1024u32;
727        let dev = MemDev::new(1024 * 64);
728        // i_block[0]=100, [1]=0 (hole), [2]=102, no indirect tiers used.
729        let mut i_block = [0u8; 60];
730        write_u32_le(&mut i_block, 0, 100);
731        write_u32_le(&mut i_block, 2, 102);
732
733        let freed = collect_for_free(&i_block, bs, 12, &dev).unwrap();
734        assert!(freed.indirect_blocks.is_empty());
735        // Two singletons (100 and 102 — not contiguous).
736        assert_eq!(
737            freed.data_runs,
738            vec![
739                FreeRun { start: 100, len: 1 },
740                FreeRun { start: 102, len: 1 },
741            ]
742        );
743    }
744
745    #[test]
746    fn collect_for_free_zero_size_yields_nothing() {
747        let bs = 1024u32;
748        let dev = MemDev::new(1024 * 64);
749        let mut i_block = [0u8; 60];
750        // Set some pointers; they should be ignored when expected_data_blocks=0.
751        for i in 0..12 {
752            write_u32_le(&mut i_block, i, 100 + i as u32);
753        }
754        let freed = collect_for_free(&i_block, bs, 0, &dev).unwrap();
755        assert!(freed.indirect_blocks.is_empty());
756        assert!(freed.data_runs.is_empty());
757    }
758
759    #[test]
760    fn round_trip_plan_then_collect() {
761        // Ensure plan_contiguous output, when persisted into a MemDev, is
762        // walked correctly by collect_for_free — same set of physical blocks
763        // come out (data + indirect).
764        let bs = 1024u32; // ppb = 256
765        let count = 271u32; // 12 direct + 256 single + 3 double
766        let plan = plan_contiguous(count, 5_000, bs, make_alloc(99_000)).unwrap();
767
768        let dev = MemDev::new(1024 * 200_000);
769        for (blk, buf) in &plan.block_writes {
770            dev.write_block(*blk, bs, buf);
771        }
772
773        let freed = collect_for_free(&plan.i_block, bs, count, &dev).unwrap();
774
775        // Indirect blocks should match exactly (up to sort order).
776        let mut got_indirect = freed.indirect_blocks.clone();
777        let mut want_indirect = plan.indirect_blocks_allocated.clone();
778        got_indirect.sort_unstable();
779        want_indirect.sort_unstable();
780        assert_eq!(got_indirect, want_indirect);
781
782        // Data should coalesce into one big contiguous run of `count` blocks.
783        assert_eq!(
784            freed.data_runs,
785            vec![FreeRun {
786                start: 5_000,
787                len: count
788            }]
789        );
790    }
791}