Skip to main content

fs_ext4/
superblock.rs

1//! ext4 superblock parsing.
2//!
3//! Spec: docs/ext4-spec/superblock.md
4//! Located at byte offset 1024, 1024 bytes long. Magic 0xEF53 at offset 56.
5
6use crate::block_io::BlockDevice;
7use crate::error::{Error, Result};
8
9pub const SUPERBLOCK_OFFSET: u64 = 1024;
10pub const SUPERBLOCK_SIZE: usize = 1024;
11pub const EXT4_MAGIC: u16 = 0xEF53;
12
13/// `s_state` bits (byte offset 0x3A). The kernel sets `VALID_FS` when a
14/// clean unmount completes and clears it on mount; a dirty value on a
15/// not-currently-mounted image therefore indicates an unclean shutdown
16/// and signals the caller that journal replay (or `fsck`) is required
17/// before writes are safe.
18pub const EXT4_VALID_FS: u16 = 0x0001;
19pub const EXT4_ERROR_FS: u16 = 0x0002;
20
21/// Parsed in-memory representation of the ext4 superblock.
22/// Field names follow `struct ext4_super_block` in the kernel.org format
23/// documentation (s_ prefix dropped).
24#[derive(Debug, Clone)]
25pub struct Superblock {
26    pub inodes_count: u32,
27    pub blocks_count: u64, // combined lo + hi
28    pub free_blocks_count: u64,
29    pub free_inodes_count: u32,
30    /// `s_r_blocks_count` (lo at 0x08, hi at 0x154 — 64-bit on
31    /// INCOMPAT_64BIT volumes). Blocks reserved for the superuser
32    /// — explains the gap between `free_blocks` and what `df`
33    /// reports as available to a normal user.
34    pub r_blocks_count: u64,
35    pub first_data_block: u32,
36    pub log_block_size: u32,
37    pub blocks_per_group: u32,
38    pub inodes_per_group: u32,
39    pub magic: u16,
40    /// `s_state` (0x3A). `EXT4_VALID_FS` = cleanly unmounted. Any other
41    /// value means the FS was mounted and not cleanly unmounted (dirty)
42    /// or that the kernel marked the FS as having errors.
43    pub state: u16,
44    /// `s_errors` (0x3C). Kernel error policy: 1=continue, 2=remount-ro,
45    /// 3=panic. Informational from a Swift host's POV but useful in
46    /// diagnostics output.
47    pub errors_behavior: u16,
48    /// `s_minor_rev_level` (0x3E). Bumped by filesystem admin tools for minor format
49    /// tweaks within a major rev_level.
50    pub minor_rev_level: u16,
51    pub rev_level: u32,
52    pub inode_size: u16,
53    /// `s_first_ino` (0x54, dynamic-rev only). First non-reserved
54    /// inode number; defaults to 11 on rev_level=1+ filesystems.
55    pub first_inode: u32,
56    pub feature_compat: u32,
57    pub feature_incompat: u32,
58    pub feature_ro_compat: u32,
59    pub uuid: [u8; 16],
60    pub volume_name: String,
61    /// `s_last_mounted` (0x88, 64 bytes). Last directory the FS was
62    /// mounted at — handy for diagnostics ("when was this disk last
63    /// in another machine?").
64    pub last_mounted: String,
65    pub desc_size: u16, // BGD size: 32 or 64
66    /// `s_reserved_gdt_blocks` — blocks held back after the group
67    /// descriptor table so the filesystem can be grown online.
68    ///
69    /// They sit between the GDT and the block bitmap in every group that
70    /// carries a superblock backup, and they are **not** free space:
71    /// anything that rebuilds a block bitmap has to mark them used or
72    /// the next allocation writes over the filesystem's own room to
73    /// expand.
74    pub reserved_gdt_blocks: u16,
75    /// `s_backup_bgs` — the only two groups carrying backups when
76    /// `SPARSE_SUPER2` is set. Meaningless without that feature.
77    pub backup_bgs: [u32; 2],
78    pub hash_seed: [u32; 4],
79    pub default_hash_version: u8,
80    pub checksum_seed: u32, // s_checksum_seed (used when INCOMPAT_CSUM_SEED)
81    pub journal_inode: u32,
82    /// `s_last_orphan` (0xE8). Head of the orphan-inode list — inodes
83    /// whose link count reached zero while still open. The kernel
84    /// inserts unlink-while-open targets here so that, on the next
85    /// mount, recovery can reclaim them. Each inode's `i_dtime` field
86    /// is overloaded to point at the next orphan in the chain; the
87    /// chain terminates with a zero `dtime`.
88    pub last_orphan: u32,
89    /// `s_mtime` (0x2C). Timestamp the FS was last mounted.
90    pub mtime: u32,
91    /// `s_wtime` (0x30). Timestamp the FS was last written to.
92    pub wtime: u32,
93    /// `s_mnt_count` (0x34). Mounts since last fsck.
94    pub mnt_count: u16,
95    /// `s_max_mnt_count` (0x36). Forced fsck after this many mounts;
96    /// 0 disables.
97    pub max_mnt_count: u16,
98    /// `s_lastcheck` (0x40). Timestamp of the last fsck pass.
99    pub lastcheck: u32,
100    /// `s_checkinterval` (0x44). Seconds between forced fscks; 0
101    /// disables time-based forced fsck.
102    pub checkinterval: u32,
103    /// `s_creator_os` (0x48). 0=Linux, 1=Hurd, 2=Masix, 3=FreeBSD,
104    /// 4=Lites.
105    pub creator_os: u32,
106    /// `s_def_resuid` (0x50). UID with access to reserved blocks.
107    pub def_resuid: u16,
108    /// `s_def_resgid` (0x52). GID with access to reserved blocks.
109    pub def_resgid: u16,
110    pub raw: Vec<u8>, // keep raw bytes for re-checksum on writes (future)
111}
112
113/// The classic `RO_COMPAT_SPARSE_SUPER` layout: groups 0 and 1, and
114/// every power of 3, 5 or 7.
115///
116/// Separate from [`Superblock::group_has_super`] because `mkfs` needs the
117/// rule without a filesystem to ask — it is deciding what to write, not
118/// reading what someone else wrote.
119pub(crate) fn classic_sparse_super(g: u64) -> bool {
120    fn is_power_of(mut g: u64, base: u64) -> bool {
121        while g.is_multiple_of(base) {
122            g /= base;
123        }
124        g == 1
125    }
126    g <= 1 || is_power_of(g, 3) || is_power_of(g, 5) || is_power_of(g, 7)
127}
128
129/// The largest block ext4 defines, as its base-2 log over 1024.
130///
131/// 6, which is a 64 KiB block.
132pub const MAX_LOG_BLOCK_SIZE: u32 = 6;
133
134/// The first inode a filesystem may hand out, before `s_first_ino`.
135///
136/// 11. Inodes 1 through 10 are the filesystem's own -- 2 is the root
137/// directory and 8 the journal -- and the kernel refuses a superblock
138/// whose `s_first_ino` is below it.
139pub const GOOD_OLD_FIRST_INODE: u32 = 11;
140
141impl Superblock {
142    /// Read and parse the superblock from a block device.
143    pub fn read<D: BlockDevice + ?Sized>(dev: &D) -> Result<Self> {
144        let mut buf = vec![0u8; SUPERBLOCK_SIZE];
145        dev.read_at(SUPERBLOCK_OFFSET, &mut buf)?;
146        Self::parse(buf)
147    }
148
149    pub fn parse(raw: Vec<u8>) -> Result<Self> {
150        if raw.len() < SUPERBLOCK_SIZE {
151            return Err(Error::Corrupt("superblock buffer too small"));
152        }
153
154        let magic = u16::from_le_bytes([raw[0x38], raw[0x39]]);
155        if magic != EXT4_MAGIC {
156            return Err(Error::BadMagic {
157                found: magic,
158                expected: EXT4_MAGIC,
159            });
160        }
161
162        let inodes_count = u32::from_le_bytes(raw[0x00..0x04].try_into().unwrap());
163        let blocks_count_lo = u32::from_le_bytes(raw[0x04..0x08].try_into().unwrap());
164        let r_blocks_count_lo = u32::from_le_bytes(raw[0x08..0x0C].try_into().unwrap());
165        let free_blocks_count_lo = u32::from_le_bytes(raw[0x0C..0x10].try_into().unwrap());
166        let free_inodes_count = u32::from_le_bytes(raw[0x10..0x14].try_into().unwrap());
167        let first_data_block = u32::from_le_bytes(raw[0x14..0x18].try_into().unwrap());
168        let log_block_size = u32::from_le_bytes(raw[0x18..0x1C].try_into().unwrap());
169        let blocks_per_group = u32::from_le_bytes(raw[0x20..0x24].try_into().unwrap());
170        let inodes_per_group = u32::from_le_bytes(raw[0x28..0x2C].try_into().unwrap());
171        let mtime = u32::from_le_bytes(raw[0x2C..0x30].try_into().unwrap());
172        let wtime = u32::from_le_bytes(raw[0x30..0x34].try_into().unwrap());
173        let mnt_count = u16::from_le_bytes(raw[0x34..0x36].try_into().unwrap());
174        let max_mnt_count = u16::from_le_bytes(raw[0x36..0x38].try_into().unwrap());
175        let state = u16::from_le_bytes(raw[0x3A..0x3C].try_into().unwrap());
176        let errors_behavior = u16::from_le_bytes(raw[0x3C..0x3E].try_into().unwrap());
177        let minor_rev_level = u16::from_le_bytes(raw[0x3E..0x40].try_into().unwrap());
178        let lastcheck = u32::from_le_bytes(raw[0x40..0x44].try_into().unwrap());
179        let checkinterval = u32::from_le_bytes(raw[0x44..0x48].try_into().unwrap());
180        let creator_os = u32::from_le_bytes(raw[0x48..0x4C].try_into().unwrap());
181        let rev_level = u32::from_le_bytes(raw[0x4C..0x50].try_into().unwrap());
182        let def_resuid = u16::from_le_bytes(raw[0x50..0x52].try_into().unwrap());
183        let def_resgid = u16::from_le_bytes(raw[0x52..0x54].try_into().unwrap());
184
185        // Dynamic-rev fields (rev_level >= 1). Pre-rev1 filesystems
186        // pin sensible defaults — `inode_size = 128` (the historical
187        // ext2 size), `first_inode = 11` (the spec-defined start of
188        // user-visible inodes; lower numbers are reserved).
189        // A `s_first_ino` below 11 would hand the reserved inodes -- 2
190        // is the root directory, 8 the journal -- to the allocator, so
191        // the kernel refuses one and this takes the floor rather than
192        // the field. Nothing legitimate writes a lower value; a
193        // filesystem that does is claiming its own root is available.
194        let first_inode = if rev_level >= 1 {
195            u32::from_le_bytes(raw[0x54..0x58].try_into().unwrap()).max(GOOD_OLD_FIRST_INODE)
196        } else {
197            GOOD_OLD_FIRST_INODE
198        };
199        let inode_size = if rev_level >= 1 {
200            u16::from_le_bytes(raw[0x58..0x5A].try_into().unwrap())
201        } else {
202            128
203        };
204        let feature_compat = if rev_level >= 1 {
205            u32::from_le_bytes(raw[0x5C..0x60].try_into().unwrap())
206        } else {
207            0
208        };
209        let feature_incompat = if rev_level >= 1 {
210            u32::from_le_bytes(raw[0x60..0x64].try_into().unwrap())
211        } else {
212            0
213        };
214        let feature_ro_compat = if rev_level >= 1 {
215            u32::from_le_bytes(raw[0x64..0x68].try_into().unwrap())
216        } else {
217            0
218        };
219
220        // s_reserved_gdt_blocks at 0xCE, and s_backup_bgs[2] at 0x24C, between
221        // s_overhead_clusters and s_encrypt_algos. Both are zero on a
222        // revision-0 filesystem, which has neither.
223        let reserved_gdt_blocks = if rev_level >= 1 {
224            u16::from_le_bytes(raw[0xCE..0xD0].try_into().unwrap())
225        } else {
226            0
227        };
228        let backup_bgs = if rev_level >= 1 && raw.len() >= 0x254 {
229            [
230                u32::from_le_bytes(raw[0x24C..0x250].try_into().unwrap()),
231                u32::from_le_bytes(raw[0x250..0x254].try_into().unwrap()),
232            ]
233        } else {
234            [0, 0]
235        };
236
237        let mut uuid = [0u8; 16];
238        uuid.copy_from_slice(&raw[0x68..0x78]);
239
240        let volume_name_bytes = &raw[0x78..0x88];
241        let nul = volume_name_bytes.iter().position(|&b| b == 0).unwrap_or(16);
242        let volume_name = String::from_utf8_lossy(&volume_name_bytes[..nul]).into_owned();
243
244        // s_last_mounted at 0x88, 64 bytes. The kernel writes the path
245        // here on every successful mount; a freshly mkfs'd filesystem
246        // leaves it zero-padded.
247        let last_mounted_bytes = &raw[0x88..0xC8];
248        let nul = last_mounted_bytes
249            .iter()
250            .position(|&b| b == 0)
251            .unwrap_or(64);
252        let last_mounted = String::from_utf8_lossy(&last_mounted_bytes[..nul]).into_owned();
253
254        let desc_size = u16::from_le_bytes(raw[0xFE..0x100].try_into().unwrap());
255        // If desc_size is 0, default to 32 (legacy); spec says 32 or 64
256        let desc_size = if desc_size == 0 { 32 } else { desc_size };
257
258        let mut hash_seed = [0u32; 4];
259        for (i, slot) in hash_seed.iter_mut().enumerate() {
260            let off = 0xEC + i * 4;
261            *slot = u32::from_le_bytes(raw[off..off + 4].try_into().unwrap());
262        }
263        let default_hash_version = raw[0xFC];
264
265        // 64-bit fields (only valid when INCOMPAT_64BIT). Pre-64bit
266        // filesystems leave the high halves zero so combining is safe
267        // unconditionally.
268        let blocks_count_hi = u32::from_le_bytes(raw[0x150..0x154].try_into().unwrap());
269        let r_blocks_count_hi = u32::from_le_bytes(raw[0x154..0x158].try_into().unwrap());
270        let free_blocks_count_hi = u32::from_le_bytes(raw[0x158..0x15C].try_into().unwrap());
271
272        let blocks_count = ((blocks_count_hi as u64) << 32) | (blocks_count_lo as u64);
273        let r_blocks_count = ((r_blocks_count_hi as u64) << 32) | (r_blocks_count_lo as u64);
274        let free_blocks_count =
275            ((free_blocks_count_hi as u64) << 32) | (free_blocks_count_lo as u64);
276
277        let checksum_seed = u32::from_le_bytes(raw[0x270..0x274].try_into().unwrap());
278        let journal_inode = u32::from_le_bytes(raw[0xE0..0xE4].try_into().unwrap());
279        let last_orphan = u32::from_le_bytes(raw[0xE8..0xEC].try_into().unwrap());
280
281        // Reject impossible geometry early so downstream arithmetic never
282        // divides by zero. All three are required for the filesystem to
283        // name even a single block or inode.
284        if blocks_per_group == 0 {
285            return Err(Error::Corrupt("superblock: blocks_per_group == 0"));
286        }
287        if inodes_per_group == 0 {
288            return Err(Error::Corrupt("superblock: inodes_per_group == 0"));
289        }
290        if inode_size == 0 {
291            return Err(Error::Corrupt("superblock: inode_size == 0"));
292        }
293        // ext4 tops out at a 64 KiB block, which is `log_block_size = 6`.
294        // The guard used to admit 20, a 1 GiB block, on the argument that
295        // anything larger was "certainly a corrupt field" -- which is
296        // true of 20 as well. Every `vec![0u8; block_size]` in this
297        // crate is sized by this field, and `mount_inner` wraps the
298        // device in a 256-entry cache, so a 1 GiB block is 256 GiB of
299        // resident memory from a sparse image; three `read_block` calls
300        // allocated and zero-filled 3 GiB in five seconds. It is also
301        // what makes `ppb * ppb * ppb` overflow in the indirect-block
302        // map.
303        if log_block_size > MAX_LOG_BLOCK_SIZE {
304            return Err(Error::Corrupt(
305                "superblock: log_block_size exceeds the largest ext4 block",
306            ));
307        }
308        check_group_fits_bitmap(
309            raw.as_slice(),
310            log_block_size,
311            blocks_per_group,
312            feature_ro_compat & crate::features::RoCompat::BIGALLOC.bits() != 0,
313        )?;
314        // 32 bytes without the 64BIT feature, 64 or more with it, and a
315        // power of two either way -- the kernel's own rule. The parser
316        // reads fixed offsets up to 0x20, and up to 0x3C when the field
317        // says 64, so a smaller value indexes past the buffer: 8 gave
318        // "range end index 12 out of range for slice of length 8" during
319        // mount.
320        let sixty_four_bit = feature_incompat & crate::features::Incompat::BIT64.bits() != 0;
321        // WITHOUT 64BIT THE FIELD IS NOT READ AT ALL (#140). The format
322        // documentation defines `s_desc_size` only "if the 64bit incompat
323        // feature flag is set"; without it a descriptor is 32 bytes whatever
324        // the field says, and the kernel mounts such a volume. Under 64BIT
325        // it is a power of two from 64 to 1024. A floor alone let a
326        // non-64BIT volume with a padded field of 64 have its descriptors
327        // read at 64-byte stride with the 64-bit halves filled from the
328        // next descriptor -- and refusing it instead would refuse a volume
329        // Linux mounts.
330        let desc_size = if sixty_four_bit {
331            if !(64..=1024).contains(&desc_size) || !desc_size.is_power_of_two() {
332                return Err(Error::Corrupt(
333                    "superblock: desc_size is not a group descriptor size",
334                ));
335            }
336            desc_size
337        } else {
338            32
339        };
340        if blocks_count == 0 {
341            return Err(Error::Corrupt("superblock: blocks_count == 0"));
342        }
343        // THE FIRST GROUP STARTS INSIDE THE FILESYSTEM (#181). Every block
344        // the allocator returns is `group * blocks_per_group +
345        // first_data_block + bit`, so a first data block at or past the
346        // end put every allocation outside the filesystem -- and on a
347        // device larger than it, as an FSKit mount routinely is, inside
348        // somebody else's bytes. The kernel refuses both of these at mount
349        // ("bad geometry").
350        if u64::from(first_data_block) >= blocks_count {
351            return Err(Error::Corrupt(
352                "superblock: first_data_block is at or past the end of the filesystem",
353            ));
354        }
355        // On a 1 KiB block size block 0 is the boot block, so the first
356        // group starts at block 1 -- unless clusters are larger than a
357        // block, which bigalloc allows.
358        if first_data_block == 0
359            && log_block_size == 0
360            && feature_ro_compat & crate::features::RoCompat::BIGALLOC.bits() == 0
361        {
362            return Err(Error::Corrupt(
363                "superblock: a 1 KiB-block filesystem's first data block is 0, not 1",
364            ));
365        }
366
367        Ok(Self {
368            inodes_count,
369            blocks_count,
370            free_blocks_count,
371            free_inodes_count,
372            r_blocks_count,
373            first_data_block,
374            log_block_size,
375            blocks_per_group,
376            inodes_per_group,
377            magic,
378            state,
379            errors_behavior,
380            minor_rev_level,
381            rev_level,
382            inode_size,
383            first_inode,
384            feature_compat,
385            feature_incompat,
386            feature_ro_compat,
387            uuid,
388            volume_name,
389            last_mounted,
390            desc_size,
391            reserved_gdt_blocks,
392            backup_bgs,
393            hash_seed,
394            default_hash_version,
395            checksum_seed,
396            journal_inode,
397            last_orphan,
398            mtime,
399            wtime,
400            mnt_count,
401            max_mnt_count,
402            lastcheck,
403            checkinterval,
404            creator_os,
405            def_resuid,
406            def_resgid,
407            raw,
408        })
409    }
410
411    /// Whether the filesystem was cleanly unmounted. `false` here means
412    /// the FS was not cleanly unmounted and a journal replay (or fsck)
413    /// is required before writes are safe. Read-only consumers can
414    /// still mount a dirty FS; callers that intend to write should
415    /// surface this to the user and either run fsck or refuse to
416    /// mount read-write.
417    pub fn is_clean(&self) -> bool {
418        self.state & EXT4_VALID_FS != 0
419    }
420
421    /// Whether block group `g` carries a superblock and group-descriptor
422    /// backup.
423    ///
424    /// Three layouts, and the filesystem's own flags decide which:
425    ///
426    /// - **`SPARSE_SUPER2`**: group 0, and the two groups named by
427    ///   `s_backup_bgs`. Nothing else.
428    /// - **`SPARSE_SUPER` clear**: every group. This is the old ext2
429    ///   layout, and the one that costs most to get wrong — assuming the
430    ///   sparse rule leaves real backups looking like free space.
431    /// - **otherwise**: the classic sparse rule — groups 0 and 1, and
432    ///   every power of 3, 5 or 7.
433    ///
434    /// Answering unconditionally with the classic rule is wrong in both
435    /// of the first two cases, and wrong in the direction that matters:
436    /// on a filesystem without `SPARSE_SUPER` it reports "no backup here"
437    /// for groups that have one, so a rebuilt bitmap offers the backup
438    /// superblock and its descriptor table as free blocks.
439    pub fn group_has_super(&self, g: u64) -> bool {
440        use crate::features::{Compat, RoCompat};
441
442        if g == 0 {
443            return true;
444        }
445        if self.feature_compat & Compat::SPARSE_SUPER2.bits() != 0 {
446            return self.backup_bgs.iter().any(|&b| u64::from(b) == g);
447        }
448        if self.feature_ro_compat & RoCompat::SPARSE_SUPER.bits() == 0 {
449            return true;
450        }
451        classic_sparse_super(g)
452    }
453
454    /// Group descriptors per descriptor-table block.
455    pub fn descs_per_block(&self) -> u64 {
456        u64::from(self.block_size()) / u64::from(self.desc_size.max(1))
457    }
458
459    /// `s_first_meta_bg` (0x104): the first meta group whose descriptors
460    /// live in the meta group rather than the contiguous table. Zero
461    /// without `META_BG`.
462    pub fn first_meta_bg(&self) -> u64 {
463        if self.feature_incompat & crate::features::Incompat::META_BG.bits() == 0 {
464            return 0;
465        }
466        self.raw
467            .get(0x104..0x108)
468            .map_or(0, |f| u64::from(u32::from_le_bytes(f.try_into().unwrap())))
469    }
470
471    // -----------------------------------------------------------------
472    // Where descriptors live, and what heads a group
473    // -----------------------------------------------------------------
474    //
475    // From kernel.org's ext4 documentation (blockgroup.html "Layout" and
476    // "Meta Block Groups", group_descr.html):
477    //
478    // - The superblock is always at byte 1024. With 1 KiB blocks that is
479    //   block 1 (block 0 is left for a boot sector); with larger blocks it
480    //   is inside block 0. The descriptor table starts in the next block.
481    // - Without META_BG the table is one run of blocks, and each group that
482    //   carries a superblock copy (see `group_has_super`) carries a copy of
483    //   the whole run right after it, followed by the reserved GDT blocks
484    //   kept for growing the table.
485    // - With META_BG the groups are split into meta groups of as many groups
486    //   as one block holds descriptors for. A meta group's single
487    //   descriptor block sits at the head of its first group (after that
488    //   group's superblock copy, if it has one), with copies at the head of
489    //   its second and last groups.
490    //
491    // Where that starts is `s_first_meta_bg`, which super.html lists as
492    // "First metablock block group, if the meta_bg feature is enabled". The
493    // documentation does not say whether it counts meta groups or block
494    // groups, nor what becomes of the groups before it. `dumpe2fs` does:
495    // on a volume the kernel converted to META_BG by growing it online (1
496    // KiB blocks, 32-byte descriptors, 32 to a block), it reports "First
497    // meta block group: 1", the descriptors of groups 32 to 63 at the heads
498    // of groups 32, 33 and 63, and group 0's table after the superblock
499    // one block long. So it is a meta group number: meta groups from
500    // `s_first_meta_bg` on have their own blocks, and those before it keep
501    // the run after the superblock, which is then `s_first_meta_bg` blocks
502    // long, one per meta group.
503    //
504    // Checked against `dumpe2fs` on mke2fs-made volumes with and without
505    // META_BG, SPARSE_SUPER and SPARSE_SUPER2, at 1 KiB and 4 KiB, with
506    // many groups, and on two volumes the kernel grew online, with 32- and
507    // 64-byte descriptors (tests/group_layout_oracle.rs). mke2fs left
508    // `s_first_meta_bg` at 0 on each of its META_BG volumes there, so only
509    // the kernel-grown ones tell the two readings apart. On a META_BG volume whose last meta group is short,
510    // dumpe2fs shows no copy in the volume's last group: the copy belongs to
511    // the meta group's last *possible* group, which does not exist.
512
513    /// The block holding the primary superblock (the one at byte 1024).
514    fn primary_superblock_block(&self) -> u64 {
515        SUPERBLOCK_OFFSET / u64::from(self.block_size())
516    }
517
518    /// The first block of group `group`.
519    fn first_block_of_group(&self, group: u64) -> u64 {
520        u64::from(self.first_data_block) + group * u64::from(self.blocks_per_group)
521    }
522
523    /// The meta group whose own descriptor block describes `group`, or
524    /// `None` when `group` is described by the run after the superblock.
525    fn meta_group_holding(&self, group: u64) -> Option<u64> {
526        if self.feature_incompat & crate::features::Incompat::META_BG.bits() == 0 {
527            return None;
528        }
529        let meta_group = group / self.descs_per_block();
530        (meta_group >= self.first_meta_bg()).then_some(meta_group)
531    }
532
533    /// Length in blocks of the descriptor run that follows a superblock.
534    fn shared_descriptor_run(&self) -> u64 {
535        if self.feature_incompat & crate::features::Incompat::META_BG.bits() != 0 {
536            self.first_meta_bg()
537        } else {
538            self.block_group_count().div_ceil(self.descs_per_block())
539        }
540    }
541
542    /// Where group `group`'s descriptor lives: the block, and the byte
543    /// offset within it (#73). The primary copy; backups are elsewhere.
544    pub fn descriptor_location(&self, group: u64) -> (u64, usize) {
545        let per_block = self.descs_per_block();
546        let offset = ((group % per_block) * u64::from(self.desc_size)) as usize;
547        let block = match self.meta_group_holding(group) {
548            // The run begins the block after the superblock's own block.
549            // That is `first_data_block + 1` on every volume but a 1 KiB
550            // bigalloc one, whose groups start at block 0 while the
551            // superblock is still in block 1 (#75).
552            None => self.primary_superblock_block() + 1 + group / per_block,
553            Some(meta_group) => {
554                let head_group = meta_group * per_block;
555                if head_group == 0 {
556                    self.primary_superblock_block() + 1
557                } else {
558                    self.first_block_of_group(head_group)
559                        + u64::from(self.group_has_super(head_group))
560                }
561            }
562        };
563        (block, offset)
564    }
565
566    /// How many blocks at the head of `group` are superblock copy,
567    /// descriptor blocks and reserved GDT blocks: what precedes any bitmap
568    /// or inode table the group holds.
569    pub fn group_head_metadata_blocks(&self, group: u64) -> u64 {
570        let superblock = u64::from(self.group_has_super(group));
571        match self.meta_group_holding(group) {
572            Some(meta_group) => {
573                let per_block = self.descs_per_block();
574                let place = group - meta_group * per_block;
575                let holds_descriptor_copy = place == 0 || place == 1 || place == per_block - 1;
576                superblock + u64::from(holds_descriptor_copy)
577            }
578            None if superblock == 1 => {
579                1 + self.shared_descriptor_run() + u64::from(self.reserved_gdt_blocks)
580            }
581            None => 0,
582        }
583    }
584
585    /// Whether directory names are hashed as unsigned bytes: `s_flags`
586    /// (0x160) carries `EXT2_FLAGS_UNSIGNED_HASH` (0x2). See
587    /// [`crate::hash::effective_version`].
588    pub fn unsigned_hash(&self) -> bool {
589        self.raw
590            .get(0x160..0x164)
591            .is_some_and(|f| u32::from_le_bytes(f.try_into().unwrap()) & 0x2 != 0)
592    }
593
594    /// Block size in bytes: 1024 << log_block_size.
595    pub fn block_size(&self) -> u32 {
596        1024u32 << self.log_block_size
597    }
598
599    /// `blocks_count * block_size`, or `None` when the product overflows
600    /// a `u64` -- which no real filesystem does, so a caller refuses it
601    /// rather than saturating to a bound that admits anything.
602    pub fn filesystem_bytes(&self) -> Option<u64> {
603        self.blocks_count.checked_mul(u64::from(self.block_size()))
604    }
605
606    /// THE FILESYSTEM FITS ITS DEVICE (#321), the kernel's "block count
607    /// exceeds size of device". Every bound that reads `blocks_count`
608    /// otherwise trusts a size nothing checked: a superblock claiming
609    /// 2^32 blocks on a few megabytes let `read_all` ask for terabytes.
610    /// A device larger than the filesystem is normal (a partition, an
611    /// FSKit volume); a smaller one is a truncated or forged image.
612    pub fn check_fits_device(&self, device_bytes: u64) -> Result<()> {
613        match self.filesystem_bytes() {
614            Some(bytes) if bytes <= device_bytes => Ok(()),
615            _ => Err(Error::Corrupt(
616                "superblock: blocks_count makes the filesystem larger than the device",
617            )),
618        }
619    }
620
621    /// Number of block groups.
622    ///
623    /// Counted from `s_first_data_block` (format documentation: "at least 1
624    /// for 1k-block filesystems"): on a 1 KiB-block filesystem block 0 is the
625    /// boot block and the
626    /// groups start at block 1. Dividing the whole of `s_blocks_count`
627    /// gave one group too many whenever it was one past a multiple of
628    /// `s_blocks_per_group`, and the phantom group's descriptor -- zeros --
629    /// failed its checksum and refused a volume Linux mounts (#86). The
630    /// rest of the crate (`alloc::blocks_in_group`, the block-to-group
631    /// mapping) already subtracted it.
632    pub fn block_group_count(&self) -> u64 {
633        group_count(
634            self.blocks_count,
635            self.first_data_block,
636            self.blocks_per_group,
637        )
638    }
639
640    /// Whether the 64BIT incompat feature is enabled.
641    pub fn is_64bit(&self) -> bool {
642        self.feature_incompat & crate::features::Incompat::BIT64.bits() != 0
643    }
644}
645
646#[cfg(test)]
647mod backup_layout_tests {
648    use super::*;
649    use crate::features::{Compat, RoCompat};
650
651    /// A superblock with only the fields the backup-layout rule reads.
652    fn sb_with(compat: u32, ro_compat: u32, backup_bgs: [u32; 2], reserved_gdt: u16) -> Superblock {
653        let mut raw = vec![0u8; SUPERBLOCK_SIZE];
654        raw[0x38..0x3A].copy_from_slice(&EXT4_MAGIC.to_le_bytes());
655        raw[0x00..0x04].copy_from_slice(&8192u32.to_le_bytes()); // inodes_count
656        raw[0x04..0x08].copy_from_slice(&65536u32.to_le_bytes()); // blocks_count
657        raw[0x14..0x18].copy_from_slice(&1u32.to_le_bytes()); // first_data_block
658        raw[0x20..0x24].copy_from_slice(&8192u32.to_le_bytes()); // blocks_per_group
659        raw[0x28..0x2C].copy_from_slice(&2048u32.to_le_bytes()); // inodes_per_group
660        raw[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); // rev_level
661        raw[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); // inode_size
662        raw[0x5C..0x60].copy_from_slice(&compat.to_le_bytes());
663        raw[0x64..0x68].copy_from_slice(&ro_compat.to_le_bytes());
664        raw[0xCE..0xD0].copy_from_slice(&reserved_gdt.to_le_bytes());
665        raw[0xFE..0x100].copy_from_slice(&64u16.to_le_bytes()); // desc_size
666        raw[0x24C..0x250].copy_from_slice(&backup_bgs[0].to_le_bytes());
667        raw[0x250..0x254].copy_from_slice(&backup_bgs[1].to_le_bytes());
668        Superblock::parse(raw).expect("superblock")
669    }
670
671    /// The layout nearly every filesystem has: groups 0 and 1, then the
672    /// powers of 3, 5 and 7.
673    #[test]
674    fn the_classic_sparse_layout() {
675        let sb = sb_with(0, RoCompat::SPARSE_SUPER.bits(), [0, 0], 0);
676        for g in [0, 1, 3, 5, 7, 9, 25, 27, 49, 81, 125] {
677            assert!(sb.group_has_super(g), "group {g} should carry a backup");
678        }
679        for g in [2, 4, 6, 8, 10, 11, 26, 50, 100] {
680            assert!(!sb.group_has_super(g), "group {g} should not");
681        }
682    }
683
684    /// Without `SPARSE_SUPER`, every group carries one.
685    ///
686    /// This is the case that costs data rather than capacity: answering
687    /// with the sparse rule reports "no backup here" for groups that have
688    /// one, so rebuilding a bitmap offers a live backup superblock and its
689    /// descriptor table as free space.
690    #[test]
691    fn without_sparse_super_every_group_carries_a_backup() {
692        let sb = sb_with(0, 0, [0, 0], 0);
693        for g in 0..40 {
694            assert!(sb.group_has_super(g), "group {g} should carry a backup");
695        }
696    }
697
698    /// `SPARSE_SUPER2` names exactly two groups, and no rule applies
699    /// beyond them.
700    #[test]
701    fn sparse_super2_names_its_own_groups() {
702        let sb = sb_with(
703            Compat::SPARSE_SUPER2.bits(),
704            RoCompat::SPARSE_SUPER.bits(),
705            [4, 17],
706            0,
707        );
708        assert!(sb.group_has_super(0), "group 0 always carries one");
709        assert!(sb.group_has_super(4));
710        assert!(sb.group_has_super(17));
711        // Powers of 3, 5 and 7 are not special here, and group 1 is not
712        // either — which is what distinguishes this from the classic rule
713        // rather than merely narrowing it.
714        for g in [1, 3, 5, 7, 9, 25, 49] {
715            assert!(
716                !sb.group_has_super(g),
717                "group {g} is not named by s_backup_bgs and must not be treated as \
718                 carrying a backup"
719            );
720        }
721    }
722
723    /// The field that says how much room the filesystem keeps to grow.
724    #[test]
725    fn reserved_gdt_blocks_is_read() {
726        assert_eq!(
727            sb_with(0, RoCompat::SPARSE_SUPER.bits(), [0, 0], 1024).reserved_gdt_blocks,
728            1024
729        );
730        assert_eq!(
731            sb_with(0, RoCompat::SPARSE_SUPER.bits(), [0, 0], 0).reserved_gdt_blocks,
732            0
733        );
734    }
735}
736
737/// Descriptor placement and group heads on synthetic superblocks, for
738/// the layouts mke2fs does not produce on its own (a nonzero
739/// `s_first_meta_bg`, a 1 KiB volume whose groups start at block 0).
740/// tests/group_layout_oracle.rs checks the layouts it does produce
741/// against dumpe2fs.
742#[cfg(test)]
743mod descriptor_layout_tests {
744    use super::*;
745    use crate::features::{Incompat, RoCompat};
746
747    /// 1 KiB blocks, 1024 blocks per group, 64-byte descriptors (16 to a
748    /// block), 100 groups, classic sparse backups.
749    fn sb(first_data_block: u32, meta_bg_from: Option<u32>, reserved_gdt: u16) -> Superblock {
750        let mut raw = vec![0u8; SUPERBLOCK_SIZE];
751        raw[0x38..0x3A].copy_from_slice(&EXT4_MAGIC.to_le_bytes());
752        raw[0x00..0x04].copy_from_slice(&25600u32.to_le_bytes()); // inodes_count
753        raw[0x04..0x08].copy_from_slice(&(102_400 + first_data_block).to_le_bytes());
754        raw[0x14..0x18].copy_from_slice(&first_data_block.to_le_bytes());
755        raw[0x20..0x24].copy_from_slice(&1024u32.to_le_bytes()); // blocks_per_group
756        raw[0x24..0x28].copy_from_slice(&1024u32.to_le_bytes()); // clusters_per_group
757        raw[0x28..0x2C].copy_from_slice(&256u32.to_le_bytes()); // inodes_per_group
758        raw[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); // rev_level
759        raw[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); // inode_size
760        raw[0x64..0x68].copy_from_slice(&RoCompat::SPARSE_SUPER.bits().to_le_bytes());
761        let mut incompat = Incompat::BIT64.bits();
762        if let Some(first) = meta_bg_from {
763            incompat |= Incompat::META_BG.bits();
764            raw[0x104..0x108].copy_from_slice(&first.to_le_bytes());
765        }
766        raw[0x60..0x64].copy_from_slice(&incompat.to_le_bytes());
767        raw[0xCE..0xD0].copy_from_slice(&reserved_gdt.to_le_bytes());
768        raw[0xFE..0x100].copy_from_slice(&64u16.to_le_bytes()); // desc_size
769        Superblock::parse(raw).expect("superblock")
770    }
771
772    #[test]
773    fn without_meta_bg_the_table_follows_the_superblock() {
774        let sb = sb(1, None, 50);
775        assert_eq!(sb.descriptor_location(0), (2, 0));
776        assert_eq!(sb.descriptor_location(17), (3, 64));
777        assert_eq!(sb.descriptor_location(99), (8, 3 * 64));
778        // 100 groups need 7 descriptor blocks.
779        assert_eq!(sb.group_head_metadata_blocks(0), 1 + 7 + 50);
780        assert_eq!(sb.group_head_metadata_blocks(1), 1 + 7 + 50);
781        assert_eq!(sb.group_head_metadata_blocks(2), 0);
782        assert_eq!(sb.group_head_metadata_blocks(49), 1 + 7 + 50);
783    }
784
785    /// Meta groups 0 and 1 (groups 0..32) keep the run after the
786    /// superblock, two blocks long; from group 32 each meta group keeps
787    /// its own block.
788    #[test]
789    fn meta_groups_before_first_meta_bg_keep_the_shared_run() {
790        let sb = sb(1, Some(2), 0);
791        assert_eq!(sb.descriptor_location(5), (2, 5 * 64));
792        assert_eq!(sb.descriptor_location(31), (3, 15 * 64));
793        // Group 32 starts at block 1 + 32 * 1024 and has no superblock.
794        assert_eq!(sb.descriptor_location(32), (1 + 32 * 1024, 0));
795        assert_eq!(sb.descriptor_location(47), (1 + 32 * 1024, 15 * 64));
796        // Group 48's meta group starts at group 48; 49 = 7^2 has a backup
797        // but 48 does not.
798        assert_eq!(sb.descriptor_location(50), (1 + 48 * 1024, 2 * 64));
799        assert_eq!(sb.group_head_metadata_blocks(0), 1 + 2);
800        assert_eq!(sb.group_head_metadata_blocks(1), 1 + 2);
801        assert_eq!(sb.group_head_metadata_blocks(3), 1 + 2);
802        assert_eq!(sb.group_head_metadata_blocks(32), 1);
803        assert_eq!(sb.group_head_metadata_blocks(33), 1);
804        assert_eq!(sb.group_head_metadata_blocks(34), 0);
805        assert_eq!(sb.group_head_metadata_blocks(47), 1);
806        assert_eq!(sb.group_head_metadata_blocks(49), 1 + 1, "backup and copy");
807        assert_eq!(
808            sb.group_head_metadata_blocks(27),
809            1 + 2,
810            "3^3, before first_meta_bg"
811        );
812        assert_eq!(
813            sb.group_head_metadata_blocks(81),
814            1 + 1,
815            "3^4, second of its meta group"
816        );
817    }
818}
819
820#[cfg(test)]
821mod geometry_tests {
822    use super::*;
823
824    fn raw(blocks_count: u32, first_data_block: u32, log_block_size: u32) -> Vec<u8> {
825        let mut raw = vec![0u8; SUPERBLOCK_SIZE];
826        raw[0x38..0x3A].copy_from_slice(&EXT4_MAGIC.to_le_bytes());
827        raw[0x00..0x04].copy_from_slice(&8192u32.to_le_bytes()); // inodes_count
828        raw[0x04..0x08].copy_from_slice(&blocks_count.to_le_bytes());
829        raw[0x14..0x18].copy_from_slice(&first_data_block.to_le_bytes());
830        raw[0x18..0x1C].copy_from_slice(&log_block_size.to_le_bytes());
831        raw[0x20..0x24].copy_from_slice(&8192u32.to_le_bytes()); // blocks_per_group
832        raw[0x28..0x2C].copy_from_slice(&2048u32.to_le_bytes()); // inodes_per_group
833        raw[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); // rev_level
834        raw[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); // inode_size
835        raw[0xFE..0x100].copy_from_slice(&64u16.to_le_bytes()); // desc_size
836        raw
837    }
838
839    fn refusal(raw: Vec<u8>) -> String {
840        match Superblock::parse(raw) {
841            Err(Error::Corrupt(m)) => m.to_string(),
842            other => panic!("expected Corrupt, got {:?}", other.map(|_| ())),
843        }
844    }
845
846    /// A first data block at or past the end is refused, as the kernel's
847    /// "bad geometry" check does (#181).
848    #[test]
849    fn a_first_data_block_past_the_end_is_refused() {
850        for (blocks, first) in [(16384u32, 16384u32), (16384, 20000), (100, u32::MAX)] {
851            assert!(
852                refusal(raw(blocks, first, 0)).contains("first_data_block"),
853                "blocks {blocks}, first {first}"
854            );
855        }
856    }
857
858    /// And a 1 KiB-block filesystem that starts its groups at block 0 --
859    /// unless it is bigalloc, whose clusters are larger than a block and
860    /// may start at 0.
861    #[test]
862    fn a_one_kib_filesystem_starting_at_block_zero_is_refused() {
863        assert!(refusal(raw(16384, 0, 0)).contains("first data block is 0"));
864
865        let mut bigalloc = raw(16384, 0, 0);
866        bigalloc[0x64..0x68]
867            .copy_from_slice(&crate::features::RoCompat::BIGALLOC.bits().to_le_bytes());
868        bigalloc[0x24..0x28].copy_from_slice(&8192u32.to_le_bytes()); // clusters_per_group
869        Superblock::parse(bigalloc)
870            .expect("a 1 KiB bigalloc filesystem may start its groups at block 0");
871    }
872
873    /// A group has one bitmap block, so it cannot have more blocks than
874    /// that block has bits -- the kernel's "blocks per group too big"
875    /// (#321). Without this bound the allocator's scan ran past the
876    /// bitmap into the next group's blocks.
877    #[test]
878    fn more_blocks_per_group_than_a_bitmap_has_bits_is_refused() {
879        for (log, bpg) in [(0u32, 8193u32), (2, 32769), (2, u32::MAX)] {
880            let mut r = raw(65536, u32::from(log == 0), log);
881            r[0x20..0x24].copy_from_slice(&bpg.to_le_bytes());
882            assert!(
883                refusal(r).contains("blocks_per_group"),
884                "log {log}, blocks_per_group {bpg}"
885            );
886        }
887        for (log, bpg) in [(0u32, 8192u32), (2, 32768)] {
888            let mut r = raw(65536, u32::from(log == 0), log);
889            r[0x20..0x24].copy_from_slice(&bpg.to_le_bytes());
890            Superblock::parse(r)
891                .unwrap_or_else(|e| panic!("log {log}, blocks_per_group {bpg}: {e:?}"));
892        }
893    }
894
895    /// Under bigalloc the bitmap counts clusters, so the bound is on
896    /// `s_clusters_per_group`, and `s_blocks_per_group` must be that
897    /// many clusters in blocks -- the kernel's two bigalloc checks.
898    #[test]
899    fn a_bigalloc_group_is_bounded_in_clusters() {
900        let bigalloc = |log_cluster: u32, cpg: u32, bpg: u32| {
901            let mut r = raw(1 << 20, 0, 2);
902            r[0x64..0x68]
903                .copy_from_slice(&crate::features::RoCompat::BIGALLOC.bits().to_le_bytes());
904            r[0x1C..0x20].copy_from_slice(&log_cluster.to_le_bytes());
905            r[0x20..0x24].copy_from_slice(&bpg.to_le_bytes());
906            r[0x24..0x28].copy_from_slice(&cpg.to_le_bytes());
907            r
908        };
909        // 64 KiB clusters over 4 KiB blocks: 32768 clusters is 16 * 32768
910        // blocks, far more than a 4 KiB bitmap has bits, and legitimate.
911        Superblock::parse(bigalloc(6, 32768, 32768 * 16)).expect("mke2fs -O bigalloc -C 65536");
912        assert!(refusal(bigalloc(6, 32769, 32769 * 16)).contains("clusters_per_group"));
913        assert!(refusal(bigalloc(6, 32768, 32768)).contains("clusters_per_group"));
914        assert!(refusal(bigalloc(6, 0, 32768)).contains("clusters_per_group"));
915        assert!(refusal(bigalloc(1, 32768, 32768)).contains("cluster"));
916        assert!(refusal(bigalloc(40, 1, 1)).contains("cluster"));
917    }
918
919    /// The geometries mke2fs writes still parse: 1 KiB from block 1, and
920    /// larger blocks from block 0.
921    #[test]
922    fn the_geometries_mke2fs_writes_still_parse() {
923        for (blocks, first, log) in [
924            (16385u32, 1u32, 0u32),
925            (16384, 1, 0),
926            (65536, 0, 2),
927            (1, 0, 2),
928        ] {
929            Superblock::parse(raw(blocks, first, log))
930                .unwrap_or_else(|e| panic!("blocks {blocks}, first {first}, log {log}: {e:?}"));
931        }
932    }
933}
934
935/// The largest `s_log_cluster_size` the kernel mounts: 20, a cluster of
936/// 2^(10 + 20) bytes, 1 GiB (the field is a log2 above 1 KiB, as the format
937/// documentation defines it).
938const MAX_LOG_CLUSTER_SIZE: u32 = 20;
939
940/// A GROUP FITS ITS BITMAP (#321). Each group has one bitmap block, so it
941/// cannot track more units than that block has bits. Without this bound
942/// the allocator's scan ran past the bitmap -- `bit_is_set` reads a bit
943/// past the end as free -- and handed out the next group's blocks, and a
944/// single group of `u32::MAX` blocks passed the descriptor-table check.
945///
946/// The unit is a block, or under BIGALLOC a cluster: there the bitmap
947/// counts clusters, `s_clusters_per_group` is what is bounded, and
948/// `s_blocks_per_group` must be exactly that many clusters in blocks.
949/// The kernel refuses to mount a volume that breaks any of these.
950fn check_group_fits_bitmap(
951    raw: &[u8],
952    log_block_size: u32,
953    blocks_per_group: u32,
954    bigalloc: bool,
955) -> Result<()> {
956    let bitmap_bits = 8u64 << (10 + log_block_size);
957    if !bigalloc {
958        if u64::from(blocks_per_group) > bitmap_bits {
959            return Err(Error::Corrupt(
960                "superblock: blocks_per_group exceeds the bits in one bitmap block",
961            ));
962        }
963        return Ok(());
964    }
965    let log_cluster_size = u32::from_le_bytes(raw[0x1C..0x20].try_into().unwrap());
966    if log_cluster_size < log_block_size || log_cluster_size > MAX_LOG_CLUSTER_SIZE {
967        return Err(Error::Corrupt(
968            "superblock: bigalloc cluster is smaller than a block or larger than ext4 allows",
969        ));
970    }
971    let clusters_per_group = u32::from_le_bytes(raw[0x24..0x28].try_into().unwrap());
972    if clusters_per_group == 0 || u64::from(clusters_per_group) > bitmap_bits {
973        return Err(Error::Corrupt(
974            "superblock: clusters_per_group is zero or exceeds the bits in one bitmap block",
975        ));
976    }
977    let cluster_ratio_log = log_cluster_size - log_block_size;
978    if u64::from(clusters_per_group) << cluster_ratio_log != u64::from(blocks_per_group) {
979        return Err(Error::Corrupt(
980            "superblock: blocks_per_group is not clusters_per_group clusters",
981        ));
982    }
983    Ok(())
984}
985
986/// `ceil((blocks_count - first_data_block) / blocks_per_group)`, the
987/// kernel's group count; see [`Superblock::block_group_count`].
988pub fn group_count(blocks_count: u64, first_data_block: u32, blocks_per_group: u32) -> u64 {
989    blocks_count
990        .saturating_sub(u64::from(first_data_block))
991        .div_ceil(u64::from(blocks_per_group).max(1))
992}
993
994#[cfg(test)]
995mod group_count_tests {
996    use super::group_count;
997
998    /// Against the kernel's arithmetic, including the 1 KiB geometries
999    /// one block past a multiple of the group size, where dividing the
1000    /// whole block count gave one group too many (#86).
1001    #[test]
1002    fn the_group_count_starts_at_the_first_data_block() {
1003        for (blocks, first, bpg, groups) in [
1004            (16385u64, 1u32, 8192u32, 2u64),
1005            (16384, 1, 8192, 2),
1006            (24577, 1, 8192, 3),
1007            (8193, 1, 8192, 1),
1008            (16385, 0, 8192, 3),
1009            (32768, 0, 32768, 1),
1010            (32769, 0, 32768, 2),
1011        ] {
1012            assert_eq!(
1013                group_count(blocks, first, bpg),
1014                groups,
1015                "blocks={blocks} first_data_block={first} blocks_per_group={bpg}"
1016            );
1017        }
1018    }
1019}
1020
1021#[cfg(test)]
1022mod desc_size_tests {
1023    use super::*;
1024
1025    fn raw(desc_size: u16, sixty_four_bit: bool) -> Vec<u8> {
1026        let mut raw = vec![0u8; SUPERBLOCK_SIZE];
1027        raw[0x38..0x3A].copy_from_slice(&EXT4_MAGIC.to_le_bytes());
1028        raw[0x00..0x04].copy_from_slice(&8192u32.to_le_bytes()); // inodes_count
1029        raw[0x04..0x08].copy_from_slice(&65536u32.to_le_bytes()); // blocks_count
1030        raw[0x18..0x1C].copy_from_slice(&2u32.to_le_bytes()); // 4 KiB blocks
1031        raw[0x20..0x24].copy_from_slice(&32768u32.to_le_bytes()); // blocks_per_group
1032        raw[0x28..0x2C].copy_from_slice(&4096u32.to_le_bytes()); // inodes_per_group
1033        raw[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); // rev_level
1034        raw[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); // inode_size
1035        if sixty_four_bit {
1036            let incompat = crate::features::Incompat::BIT64.bits();
1037            raw[0x60..0x64].copy_from_slice(&incompat.to_le_bytes());
1038        }
1039        raw[0xFE..0x100].copy_from_slice(&desc_size.to_le_bytes());
1040        raw
1041    }
1042
1043    /// Without 64BIT the descriptor is 32 bytes whatever the field says,
1044    /// as the kernel reads it (#140).
1045    #[test]
1046    fn without_64bit_the_descriptor_is_32_bytes_whatever_the_field_says() {
1047        for field in [0u16, 32, 64, 128, 1024, 33] {
1048            let sb = Superblock::parse(raw(field, false))
1049                .unwrap_or_else(|e| panic!("field {field}: {e:?}"));
1050            assert_eq!(sb.desc_size, 32, "field {field}");
1051        }
1052    }
1053
1054    /// With 64BIT the field is the size, a power of two from 64 to 1024.
1055    #[test]
1056    fn with_64bit_the_field_is_range_checked() {
1057        for field in [64u16, 128, 1024] {
1058            assert_eq!(
1059                Superblock::parse(raw(field, true)).unwrap().desc_size,
1060                field
1061            );
1062        }
1063        for field in [0u16, 32, 96, 2048] {
1064            assert!(
1065                matches!(Superblock::parse(raw(field, true)), Err(Error::Corrupt(_))),
1066                "field {field} must be refused"
1067            );
1068        }
1069    }
1070}