Skip to main content

fs_ext4/
extent.rs

1//! Extent tree traversal.
2//!
3//! Spec: kernel.org/doc/html/latest/filesystems/ext4/blockmap.html#extent-tree
4//!
5//! When `EXT4_EXTENTS_FL` is set on an inode, the 60-byte `i_block` field holds
6//! an `ext4_extent_header` followed by entries. Entries are one of:
7//!
8//! - **Leaf** (`eh_depth == 0`): `ext4_extent` records mapping logical blocks
9//!   to physical blocks.
10//! - **Internal** (`eh_depth > 0`): `ext4_extent_idx` records pointing at child
11//!   blocks containing further `ext4_extent_header` + entries.
12//!
13//! Each on-disk node is 12 bytes:
14//! ```text
15//!  ext4_extent_header (12 bytes):
16//!    0x00 u16 eh_magic      (0xF30A)
17//!    0x02 u16 eh_entries    (number of valid entries)
18//!    0x04 u16 eh_max        (max entries this node can hold)
19//!    0x06 u16 eh_depth      (0 = leaf, else internal)
20//!    0x08 u32 eh_generation
21//!
22//!  ext4_extent (leaf, 12 bytes):
23//!    0x00 u32 ee_block      (first logical block)
24//!    0x04 u16 ee_len        (number of blocks; >32768 means uninitialized)
25//!    0x06 u16 ee_start_hi   (upper 16 bits of physical block)
26//!    0x08 u32 ee_start_lo   (lower 32 bits of physical block)
27//!
28//!  ext4_extent_idx (internal, 12 bytes):
29//!    0x00 u32 ei_block      (first logical block in subtree)
30//!    0x04 u32 ei_leaf_lo    (lower 32 bits of child physical block)
31//!    0x08 u16 ei_leaf_hi    (upper 16 bits of child physical block)
32//!    0x0A u16 ei_unused
33//! ```
34//!
35//! Uninitialized extents: when `ee_len > 32768`, the extent is a pre-allocated
36//! hole — reads return zeros. The actual length is `ee_len - 32768`. Writes
37//! convert it to initialised by clearing the high bit.
38//!
39//! Checksums: when `METADATA_CSUM` or `GDT_CSUM` is enabled, each extent block
40//! (i.e. internal-node data, not the inline i_block) has a trailing
41//! `ext4_extent_tail` (4-byte CRC32c), verified as each block is read.
42
43use crate::block_io::BlockDevice;
44use crate::checksum::Checksummer;
45use crate::error::{Error, Result};
46
47/// Context for verifying extent-block tail checksums during a tree walk.
48///
49/// Pass `Some(&ctx)` to `lookup_verified` / `map_logical_verified` /
50/// `collect_all_verified` when reading a real on-disk extent tree under
51/// a `metadata_csum` filesystem. Each off-inode block's
52/// `ext4_extent_tail.et_checksum` is verified against
53/// `crc32c(seed, ino_le, gen_le, block[..len-4])`.
54///
55/// Pass `None` to skip verification (tests, callers without a `Filesystem`).
56#[derive(Clone, Copy)]
57pub struct ExtentVerifyCtx<'a> {
58    /// Inode number that owns this extent tree.
59    pub ino: u32,
60    /// Inode `generation` field (salts the per-inode CRC).
61    pub generation: u32,
62    /// Mount-wide checksummer (skipped when `csum.enabled == false`).
63    pub csum: &'a Checksummer,
64}
65
66impl<'a> ExtentVerifyCtx<'a> {
67    /// True if every off-inode node read should be CRC-verified.
68    fn active(&self) -> bool {
69        self.csum.enabled
70    }
71}
72
73/// Magic number at the start of every extent header.
74pub const EXT4_EXT_MAGIC: u16 = 0xF30A;
75
76/// High bit flag on `ee_len` marking an uninitialised (pre-allocated) extent.
77/// Actual length = `ee_len & 0x7FFF` if `ee_len > EXT_INIT_MAX_LEN`.
78pub const EXT_INIT_MAX_LEN: u16 = 32768;
79
80/// Bytes per on-disk extent node (header or entry).
81pub const EXT4_EXT_NODE_SIZE: usize = 12;
82
83/// Maximum extent tree depth we will descend. ext4 spec puts the hard
84/// limit at 5; anything above that is a pathological / malformed image
85/// and we bail out rather than chase pointers forever.
86pub const EXT4_EXT_MAX_DEPTH: u16 = 5;
87
88/// Header at the start of every extent tree node.
89#[derive(Debug, Clone, Copy)]
90pub struct ExtentHeader {
91    pub magic: u16,
92    pub entries: u16,
93    pub max: u16,
94    pub depth: u16,
95    pub generation: u32,
96}
97
98impl ExtentHeader {
99    pub fn parse(raw: &[u8]) -> Result<Self> {
100        if raw.len() < EXT4_EXT_NODE_SIZE {
101            return Err(Error::CorruptExtentTree("header buffer too small"));
102        }
103        let magic = u16::from_le_bytes(raw[0x00..0x02].try_into().unwrap());
104        let entries = u16::from_le_bytes(raw[0x02..0x04].try_into().unwrap());
105        let max = u16::from_le_bytes(raw[0x04..0x06].try_into().unwrap());
106        let depth = u16::from_le_bytes(raw[0x06..0x08].try_into().unwrap());
107        let generation = u32::from_le_bytes(raw[0x08..0x0C].try_into().unwrap());
108
109        if magic != EXT4_EXT_MAGIC {
110            return Err(Error::CorruptExtentTree("bad extent header magic"));
111        }
112        if entries > max {
113            return Err(Error::CorruptExtentTree("entries > max"));
114        }
115        Ok(Self {
116            magic,
117            entries,
118            max,
119            depth,
120            generation,
121        })
122    }
123
124    pub fn is_leaf(&self) -> bool {
125        self.depth == 0
126    }
127}
128
129/// Leaf extent: maps `[ee_block, ee_block+ee_len)` logical to physical.
130#[derive(Debug, Clone, Copy)]
131pub struct Extent {
132    /// First logical block number covered by this extent.
133    pub logical_block: u32,
134    /// Number of contiguous blocks (after masking the uninit flag).
135    pub length: u16,
136    /// Physical block where the data lives (assembled from hi/lo).
137    pub physical_block: u64,
138    /// True if this extent is a pre-allocated hole (reads return zeros).
139    pub uninitialized: bool,
140}
141
142impl Extent {
143    pub fn parse(raw: &[u8]) -> Result<Self> {
144        if raw.len() < EXT4_EXT_NODE_SIZE {
145            return Err(Error::CorruptExtentTree("leaf extent buffer too small"));
146        }
147        let logical_block = u32::from_le_bytes(raw[0x00..0x04].try_into().unwrap());
148        let ee_len = u16::from_le_bytes(raw[0x04..0x06].try_into().unwrap());
149        let start_hi = u16::from_le_bytes(raw[0x06..0x08].try_into().unwrap());
150        let start_lo = u32::from_le_bytes(raw[0x08..0x0C].try_into().unwrap());
151
152        let (length, uninitialized) = if ee_len > EXT_INIT_MAX_LEN {
153            (ee_len - EXT_INIT_MAX_LEN, true)
154        } else {
155            (ee_len, false)
156        };
157        let physical_block = ((start_hi as u64) << 32) | start_lo as u64;
158
159        Ok(Self {
160            logical_block,
161            length,
162            physical_block,
163            uninitialized,
164        })
165    }
166
167    /// Does this extent cover the given logical block?
168    pub fn contains(&self, logical: u64) -> bool {
169        let start = self.logical_block as u64;
170        let end = start + self.length as u64;
171        logical >= start && logical < end
172    }
173
174    /// Physical block corresponding to `logical` (caller must check contains()).
175    pub fn map(&self, logical: u64) -> u64 {
176        debug_assert!(self.contains(logical));
177        self.physical_block + (logical - self.logical_block as u64)
178    }
179}
180
181/// Internal (index) node pointing at a child extent block.
182#[derive(Debug, Clone, Copy)]
183pub struct ExtentIdx {
184    /// First logical block in the subtree rooted at this index.
185    pub logical_block: u32,
186    /// Physical block holding the child header + entries.
187    pub leaf_block: u64,
188}
189
190impl ExtentIdx {
191    pub fn parse(raw: &[u8]) -> Result<Self> {
192        if raw.len() < EXT4_EXT_NODE_SIZE {
193            return Err(Error::CorruptExtentTree("index entry buffer too small"));
194        }
195        let logical_block = u32::from_le_bytes(raw[0x00..0x04].try_into().unwrap());
196        let leaf_lo = u32::from_le_bytes(raw[0x04..0x08].try_into().unwrap());
197        let leaf_hi = u16::from_le_bytes(raw[0x08..0x0A].try_into().unwrap());
198        // 0x0A..0x0C reserved.
199        let leaf_block = ((leaf_hi as u64) << 32) | leaf_lo as u64;
200        Ok(Self {
201            logical_block,
202            leaf_block,
203        })
204    }
205}
206
207/// Traverse the extent tree rooted in `inode.block` and find the extent covering
208/// the given logical block. Reads internal-node blocks via `dev` as needed.
209///
210/// Returns `Ok(None)` if the logical block falls in a sparse hole (no extent
211/// covers it). Returns `Ok(Some(extent))` if a mapping was found.
212pub fn lookup(
213    root: &[u8],
214    dev: &dyn BlockDevice,
215    block_size: u32,
216    logical_block: u64,
217) -> Result<Option<Extent>> {
218    lookup_verified(root, dev, block_size, logical_block, None)
219}
220
221/// Same as `lookup`, but verifies each off-inode internal-node block's
222/// `ext4_extent_tail.et_checksum` when `ctx` is `Some` and enabled.
223///
224/// A mismatch returns `Error::BadChecksum { what: "extent block" }`.
225pub fn lookup_verified(
226    root: &[u8],
227    dev: &dyn BlockDevice,
228    block_size: u32,
229    logical_block: u64,
230    ctx: Option<&ExtentVerifyCtx>,
231) -> Result<Option<Extent>> {
232    // Parse root header from inode.block (the 60-byte i_block area).
233    let mut header = ExtentHeader::parse(root)?;
234    if header.depth > EXT4_EXT_MAX_DEPTH {
235        return Err(Error::CorruptExtentTree(
236            "extent tree depth exceeds spec maximum",
237        ));
238    }
239    // Holds data for non-root nodes once we descend into internal indices.
240    // `cursor` borrows from `root` initially, then from `node` after the first
241    // descent — we keep `node` alive for the duration of the lookup.
242    let mut node: Vec<u8>;
243    let mut cursor: &[u8] = root;
244    // Guard against malformed images where an internal node's child
245    // references cycle back (self or each other). We never need to
246    // recurse more than the root's claimed depth.
247    let mut descents_remaining = header.depth as usize;
248
249    loop {
250        if header.is_leaf() {
251            // Binary-search-ish linear walk (entries are sorted by logical_block).
252            // ext4 extent counts per node are small (≤340 for 4 KiB blocks, and
253            // ≤4 for the inline root) so linear scan is fine.
254            let mut found: Option<Extent> = None;
255            for i in 0..header.entries {
256                let off = EXT4_EXT_NODE_SIZE * (1 + i as usize);
257                if off + EXT4_EXT_NODE_SIZE > cursor.len() {
258                    return Err(Error::CorruptExtentTree("leaf entry out of range"));
259                }
260                let ext = Extent::parse(&cursor[off..off + EXT4_EXT_NODE_SIZE])?;
261                if ext.contains(logical_block) {
262                    found = Some(ext);
263                    break;
264                }
265                // Past our target — no mapping (sparse hole).
266                if (ext.logical_block as u64) > logical_block {
267                    break;
268                }
269            }
270            return Ok(found);
271        }
272
273        // Internal: find the largest index entry whose ei_block <= logical_block.
274        //
275        // OR THE FIRST ONE, when the block is below all of them. That is a
276        // hole in front of the file's first data, and the leaf below the
277        // first index says so by holding no extent that contains it. Refusing
278        // here instead called a sound filesystem corrupt, and made every
279        // sparse file whose data starts past block 0 unreadable at its
280        // leading hole (#260). A block no extent covers is a hole, and
281        // e2fsck accepts such a file (tests/leading_hole_deep_tree.rs), so a
282        // lookup below every index key descends through the first entry and
283        // finds no extent there.
284        let mut chosen_idx: Option<ExtentIdx> = None;
285        let mut first_idx: Option<ExtentIdx> = None;
286        for i in 0..header.entries {
287            let off = EXT4_EXT_NODE_SIZE * (1 + i as usize);
288            if off + EXT4_EXT_NODE_SIZE > cursor.len() {
289                return Err(Error::CorruptExtentTree("index entry out of range"));
290            }
291            let idx = ExtentIdx::parse(&cursor[off..off + EXT4_EXT_NODE_SIZE])?;
292            if first_idx.is_none() {
293                first_idx = Some(idx);
294            }
295            if (idx.logical_block as u64) <= logical_block {
296                chosen_idx = Some(idx);
297            } else {
298                break;
299            }
300        }
301        // An index node with no entries at all is corrupt: there is nothing
302        // below it to descend into.
303        let idx = chosen_idx
304            .or(first_idx)
305            .ok_or(Error::CorruptExtentTree("index node holds no entries"))?;
306
307        // Read the child block, parse its header, continue loop.
308        let mut buf = vec![0u8; block_size as usize];
309        let child_offset =
310            idx.leaf_block
311                .checked_mul(block_size as u64)
312                .ok_or(Error::CorruptExtentTree(
313                    "extent index: child block offset overflow",
314                ))?;
315        dev.read_at(child_offset, &mut buf)?;
316        if let Some(c) = ctx {
317            if c.active() && !c.csum.verify_extent_tail(c.ino, c.generation, &buf) {
318                return Err(Error::BadChecksum {
319                    what: "extent block",
320                });
321            }
322        }
323        node = buf;
324        cursor = &node[..];
325        header = ExtentHeader::parse(cursor)?;
326        if descents_remaining == 0 {
327            return Err(Error::CorruptExtentTree(
328                "extent tree descended past claimed depth",
329            ));
330        }
331        descents_remaining -= 1;
332    }
333}
334
335/// Walk the entire extent tree and collect every leaf extent. Useful for
336/// building a file-block list or for testing.
337pub fn collect_all(root: &[u8], dev: &dyn BlockDevice, block_size: u32) -> Result<Vec<Extent>> {
338    let header = ExtentHeader::parse(root)?;
339    if header.depth > EXT4_EXT_MAX_DEPTH {
340        return Err(Error::CorruptExtentTree(
341            "extent tree depth exceeds spec maximum",
342        ));
343    }
344    let mut out = Vec::new();
345    walk(
346        root,
347        &header,
348        dev,
349        block_size,
350        &mut out,
351        &mut Vec::new(),
352        header.depth,
353    )?;
354    Ok(out)
355}
356
357/// Every leaf extent, and every block holding a node of the tree below the
358/// inode's inline root: the index and leaf blocks a file owns besides its
359/// data. Freeing a file has to free both.
360pub fn collect_all_with_nodes(
361    root: &[u8],
362    dev: &dyn BlockDevice,
363    block_size: u32,
364) -> Result<(Vec<Extent>, Vec<u64>)> {
365    let header = ExtentHeader::parse(root)?;
366    if header.depth > EXT4_EXT_MAX_DEPTH {
367        return Err(Error::CorruptExtentTree(
368            "extent tree depth exceeds spec maximum",
369        ));
370    }
371    let mut out = Vec::new();
372    let mut nodes = Vec::new();
373    walk(
374        root,
375        &header,
376        dev,
377        block_size,
378        &mut out,
379        &mut nodes,
380        header.depth,
381    )?;
382    Ok((out, nodes))
383}
384
385fn walk(
386    node: &[u8],
387    header: &ExtentHeader,
388    dev: &dyn BlockDevice,
389    block_size: u32,
390    out: &mut Vec<Extent>,
391    nodes: &mut Vec<u64>,
392    descents_remaining: u16,
393) -> Result<()> {
394    if header.is_leaf() {
395        for i in 0..header.entries {
396            let off = EXT4_EXT_NODE_SIZE * (1 + i as usize);
397            if off + EXT4_EXT_NODE_SIZE > node.len() {
398                return Err(Error::CorruptExtentTree("leaf entry out of range"));
399            }
400            out.push(Extent::parse(&node[off..off + EXT4_EXT_NODE_SIZE])?);
401        }
402        return Ok(());
403    }
404
405    if descents_remaining == 0 {
406        return Err(Error::CorruptExtentTree(
407            "extent tree descended past claimed depth",
408        ));
409    }
410
411    for i in 0..header.entries {
412        let off = EXT4_EXT_NODE_SIZE * (1 + i as usize);
413        if off + EXT4_EXT_NODE_SIZE > node.len() {
414            return Err(Error::CorruptExtentTree("index entry out of range"));
415        }
416        let idx = ExtentIdx::parse(&node[off..off + EXT4_EXT_NODE_SIZE])?;
417        let mut buf = vec![0u8; block_size as usize];
418        let child_offset =
419            idx.leaf_block
420                .checked_mul(block_size as u64)
421                .ok_or(Error::CorruptExtentTree(
422                    "extent walk: child block offset overflow",
423                ))?;
424        dev.read_at(child_offset, &mut buf)?;
425        nodes.push(idx.leaf_block);
426        let child_header = ExtentHeader::parse(&buf)?;
427        walk(
428            &buf,
429            &child_header,
430            dev,
431            block_size,
432            out,
433            nodes,
434            descents_remaining - 1,
435        )?;
436    }
437    Ok(())
438}
439
440/// Map a logical block to its physical block. Returns `None` for sparse holes
441/// and for uninitialised extents (callers wanting to read zeros should handle
442/// that case explicitly — see `map_for_read`).
443pub fn map_logical(
444    root: &[u8],
445    dev: &dyn BlockDevice,
446    block_size: u32,
447    logical_block: u64,
448) -> Result<Option<u64>> {
449    map_logical_verified(root, dev, block_size, logical_block, None)
450}
451
452/// Same as `map_logical`, with optional extent-tail CRC verification on
453/// every off-inode node read.
454pub fn map_logical_verified(
455    root: &[u8],
456    dev: &dyn BlockDevice,
457    block_size: u32,
458    logical_block: u64,
459    ctx: Option<&ExtentVerifyCtx>,
460) -> Result<Option<u64>> {
461    match lookup_verified(root, dev, block_size, logical_block, ctx)? {
462        Some(ext) if !ext.uninitialized => Ok(Some(ext.map(logical_block))),
463        Some(_) => Ok(None), // uninitialised → read as zeros
464        None => Ok(None),    // sparse hole → read as zeros
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    /// Build a minimal root header + leaf extent buffer inline (fits in 60 bytes).
473    fn make_root(ext_count: u16) -> Vec<u8> {
474        let mut buf = vec![0u8; 60];
475        // header
476        buf[0..2].copy_from_slice(&EXT4_EXT_MAGIC.to_le_bytes());
477        buf[2..4].copy_from_slice(&ext_count.to_le_bytes());
478        buf[4..6].copy_from_slice(&4u16.to_le_bytes()); // max
479        buf[6..8].copy_from_slice(&0u16.to_le_bytes()); // depth = 0 (leaf)
480        buf[8..12].copy_from_slice(&0u32.to_le_bytes());
481        buf
482    }
483
484    fn set_leaf(buf: &mut [u8], i: usize, log: u32, len: u16, phys: u64) {
485        let off = 12 * (1 + i);
486        buf[off..off + 4].copy_from_slice(&log.to_le_bytes());
487        buf[off + 4..off + 6].copy_from_slice(&len.to_le_bytes());
488        buf[off + 6..off + 8].copy_from_slice(&((phys >> 32) as u16).to_le_bytes());
489        buf[off + 8..off + 12].copy_from_slice(&((phys & 0xFFFF_FFFF) as u32).to_le_bytes());
490    }
491
492    #[test]
493    fn leaf_lookup_finds_mapping() {
494        let mut buf = make_root(2);
495        set_leaf(&mut buf, 0, 0, 4, 100); // logical 0..4 -> physical 100..104
496        set_leaf(&mut buf, 1, 4, 2, 200); // logical 4..6 -> physical 200..202
497
498        // Stub dev; never read.
499        struct Dummy;
500        impl BlockDevice for Dummy {
501            fn read_at(&self, _o: u64, _b: &mut [u8]) -> Result<()> {
502                unreachable!()
503            }
504            fn size_bytes(&self) -> u64 {
505                0
506            }
507        }
508
509        let r = lookup(&buf, &Dummy, 4096, 0).unwrap().unwrap();
510        assert_eq!(r.map(0), 100);
511
512        let r = lookup(&buf, &Dummy, 4096, 3).unwrap().unwrap();
513        assert_eq!(r.map(3), 103);
514
515        let r = lookup(&buf, &Dummy, 4096, 5).unwrap().unwrap();
516        assert_eq!(r.map(5), 201);
517
518        // Beyond all extents = hole.
519        assert!(lookup(&buf, &Dummy, 4096, 10).unwrap().is_none());
520    }
521
522    #[test]
523    fn uninitialized_extent_maps_to_none() {
524        let mut buf = make_root(1);
525        // length stored as EXT_INIT_MAX_LEN + 4 means "4 blocks uninitialised"
526        set_leaf(&mut buf, 0, 0, EXT_INIT_MAX_LEN + 4, 500);
527
528        struct Dummy;
529        impl BlockDevice for Dummy {
530            fn read_at(&self, _o: u64, _b: &mut [u8]) -> Result<()> {
531                unreachable!()
532            }
533            fn size_bytes(&self) -> u64 {
534                0
535            }
536        }
537
538        // The lookup succeeds but map_logical returns None (read as zeros).
539        assert!(map_logical(&buf, &Dummy, 4096, 2).unwrap().is_none());
540
541        let ext = lookup(&buf, &Dummy, 4096, 2).unwrap().unwrap();
542        assert!(ext.uninitialized);
543        assert_eq!(ext.length, 4);
544    }
545
546    #[test]
547    fn rejects_bad_magic() {
548        let mut buf = make_root(0);
549        buf[0] = 0x00;
550        buf[1] = 0x00;
551        let err = ExtentHeader::parse(&buf).unwrap_err();
552        assert!(matches!(err, Error::CorruptExtentTree(_)));
553    }
554
555    #[test]
556    fn rejects_entries_gt_max() {
557        let mut buf = make_root(10); // entries=10, max=4
558        buf[4..6].copy_from_slice(&4u16.to_le_bytes());
559        let err = ExtentHeader::parse(&buf).unwrap_err();
560        assert!(matches!(err, Error::CorruptExtentTree(_)));
561    }
562
563    #[test]
564    fn rejects_impossible_depth() {
565        // depth=99 is well past the spec-allowed max of 5.
566        let mut buf = make_root(0);
567        buf[6..8].copy_from_slice(&99u16.to_le_bytes());
568
569        struct Dummy;
570        impl BlockDevice for Dummy {
571            fn read_at(&self, _o: u64, _b: &mut [u8]) -> Result<()> {
572                unreachable!()
573            }
574            fn size_bytes(&self) -> u64 {
575                0
576            }
577        }
578
579        // lookup/collect_all must refuse rather than try to descend 99 levels.
580        assert!(matches!(
581            lookup(&buf, &Dummy, 4096, 0),
582            Err(Error::CorruptExtentTree(_))
583        ));
584        assert!(matches!(
585            collect_all(&buf, &Dummy, 4096),
586            Err(Error::CorruptExtentTree(_))
587        ));
588    }
589
590    fn make_extent(logical_block: u32, length: u16, physical_block: u64) -> Extent {
591        Extent {
592            logical_block,
593            length,
594            physical_block,
595            uninitialized: false,
596        }
597    }
598
599    #[test]
600    fn extent_contains_within_range() {
601        let e = make_extent(10, 5, 100); // covers logical 10..15
602        assert!(e.contains(10));
603        assert!(e.contains(12));
604        assert!(e.contains(14));
605    }
606
607    #[test]
608    fn extent_contains_rejects_before_start() {
609        let e = make_extent(10, 5, 100);
610        assert!(!e.contains(9));
611        assert!(!e.contains(0));
612    }
613
614    #[test]
615    fn extent_contains_rejects_at_end() {
616        let e = make_extent(10, 5, 100); // end = 15, exclusive
617        assert!(!e.contains(15));
618        assert!(!e.contains(100));
619    }
620
621    #[test]
622    fn extent_contains_single_block_extent() {
623        let e = make_extent(7, 1, 50);
624        assert!(e.contains(7));
625        assert!(!e.contains(6));
626        assert!(!e.contains(8));
627    }
628
629    #[test]
630    fn extent_map_start_of_extent() {
631        let e = make_extent(10, 5, 100);
632        assert_eq!(e.map(10), 100);
633    }
634
635    #[test]
636    fn extent_map_interior_block() {
637        let e = make_extent(10, 5, 100);
638        assert_eq!(e.map(13), 103);
639    }
640
641    #[test]
642    fn extent_map_last_block() {
643        let e = make_extent(10, 5, 100);
644        assert_eq!(e.map(14), 104);
645    }
646
647    #[test]
648    fn extent_parse_rejects_short_buffer() {
649        let short = vec![0u8; 4];
650        assert!(Extent::parse(&short).is_err());
651    }
652
653    #[test]
654    fn extent_parse_uninitialized_flag() {
655        let mut raw = vec![0u8; 12];
656        // ee_len > EXT_INIT_MAX_LEN (32768) marks as uninitialized
657        let uninit_len: u16 = EXT_INIT_MAX_LEN + 1;
658        raw[4..6].copy_from_slice(&uninit_len.to_le_bytes());
659        let e = Extent::parse(&raw).unwrap();
660        assert!(e.uninitialized);
661        assert_eq!(e.length, 1);
662    }
663
664    #[test]
665    fn extent_parse_initialized() {
666        let mut raw = vec![0u8; 12];
667        raw[0..4].copy_from_slice(&5u32.to_le_bytes()); // logical_block
668        raw[4..6].copy_from_slice(&3u16.to_le_bytes()); // ee_len (init)
669        raw[8..12].copy_from_slice(&200u32.to_le_bytes()); // phys lo
670        let e = Extent::parse(&raw).unwrap();
671        assert_eq!(e.logical_block, 5);
672        assert_eq!(e.length, 3);
673        assert_eq!(e.physical_block, 200);
674        assert!(!e.uninitialized);
675    }
676
677    /// Tests that read a fixture from `test-disks/` (`chore fixtures`).
678    mod needs_host {
679        use super::*;
680        use crate::block_io::FileDevice;
681        use crate::fs::Filesystem;
682        use crate::inode::Inode;
683        use std::sync::Arc;
684
685        /// Real ext4-basic.img test: the root directory's inode has exactly one
686        /// leaf extent pointing at a single directory block. We verify the extent
687        /// layout and that reading the mapped physical block returns data that
688        /// parses as directory entries.
689        #[test]
690        fn ext4_basic_root_inode_has_extent() {
691            let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
692            let file = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
693            let dev: Arc<dyn BlockDevice> = Arc::new(file);
694            let fs = Filesystem::mount(dev.clone()).expect("mount");
695            let raw = fs.read_inode_raw(2).expect("root inode"); // inode 2 = root dir
696            let inode = Inode::parse(&raw).expect("parse inode");
697
698            assert!(inode.is_dir(), "root inode is not a directory");
699            assert!(inode.has_extents(), "root inode does not use extents");
700
701            let header = ExtentHeader::parse(&inode.block).expect("parse extent header");
702            assert_eq!(header.magic, EXT4_EXT_MAGIC);
703            assert_eq!(header.depth, 0, "expected a leaf root for tiny test image");
704            assert!(header.entries >= 1);
705
706            let all = collect_all(&inode.block, dev.as_ref(), fs.sb.block_size())
707                .expect("collect extents");
708            assert!(!all.is_empty());
709            // First extent should start at logical block 0 for a small dir.
710            assert_eq!(all[0].logical_block, 0);
711            assert!(!all[0].uninitialized);
712
713            // Read the first physical block and verify it looks like a dir block
714            // (should have '.' as the first entry).
715            let first_phys = all[0].map(0);
716            let mut blk = vec![0u8; fs.sb.block_size() as usize];
717            dev.read_at(first_phys * fs.sb.block_size() as u64, &mut blk)
718                .unwrap();
719
720            let entries = crate::dir::parse_block(&blk, true).expect("parse dir block");
721            assert!(!entries.is_empty());
722            assert_eq!(entries[0].name, b".");
723        }
724    }
725}