Skip to main content

microsandbox_image/ext4/
mod.rs

1mod format;
2
3use std::io::{self, BufWriter, SeekFrom, Write};
4use std::path::Path;
5
6use crate::crc32c;
7use crate::filetree::{DirectoryNode, FileTree, TreeNode};
8use format::{
9    EXT4_BG_INODE_ZEROED, EXT4_BLOCK_SIZE, EXT4_BLOCKS_PER_GROUP, EXT4_DESC_SIZE, EXT4_EH_MAGIC,
10    EXT4_EXTENTS_FL, EXT4_FEATURE_COMPAT_DIR_INDEX, EXT4_FEATURE_COMPAT_EXT_ATTR,
11    EXT4_FEATURE_COMPAT_HAS_JOURNAL, EXT4_FEATURE_INCOMPAT_64BIT, EXT4_FEATURE_INCOMPAT_EXTENTS,
12    EXT4_FEATURE_INCOMPAT_FILETYPE, EXT4_FEATURE_RO_COMPAT_DIR_NLINK,
13    EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE, EXT4_FEATURE_RO_COMPAT_HUGE_FILE,
14    EXT4_FEATURE_RO_COMPAT_LARGE_FILE, EXT4_FEATURE_RO_COMPAT_METADATA_CSUM,
15    EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER, EXT4_FIRST_INO, EXT4_INODE_SIZE, EXT4_INODES_PER_GROUP,
16    EXT4_JOURNAL_INO, EXT4_LOG_BLOCK_SIZE, EXT4_MIN_EXTRA_ISIZE, EXT4_ROOT_INO, EXT4_SUPER_MAGIC,
17    JBD2_MAGIC, JBD2_SUPERBLOCK_V2, S_IFCHR, S_IFDIR, S_IFLNK, S_IFREG,
18};
19
20//--------------------------------------------------------------------------------------------------
21// Constants
22//--------------------------------------------------------------------------------------------------
23
24/// Default image size: 4 GiB.
25const DEFAULT_SIZE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
26
27/// Default journal size in blocks (64 MiB at 4 KiB/block = 16384 blocks).
28const DEFAULT_JOURNAL_BLOCKS: u32 = 16384;
29
30/// Maximum number of block groups we format (keeps things simple).
31const MAX_GROUPS: u32 = 32;
32
33/// This minimal filesystem does not reserve space for online resize metadata.
34const RESERVED_GDT_BLOCKS: u32 = 0;
35
36/// ext4 directory entry file type: directory.
37const EXT4_FT_DIR: u8 = 2;
38
39/// ext4 directory entry file type: regular file.
40#[allow(dead_code)]
41const EXT4_FT_REG_FILE: u8 = 1;
42
43/// ext4 directory entry file type: character device.
44const EXT4_FT_CHRDEV: u8 = 3;
45
46/// ext4 directory entry file type: symbolic link.
47const EXT4_FT_SYMLINK: u8 = 7;
48
49/// jbd2 superblocks are always 1024 bytes, even on 4 KiB block filesystems.
50const JBD2_SUPERBLOCK_SIZE: usize = 1024;
51
52//--------------------------------------------------------------------------------------------------
53// Types
54//--------------------------------------------------------------------------------------------------
55
56/// Options for creating an ext4 filesystem image.
57pub struct Ext4FormatOptions {
58    /// Total image size in bytes. Must be large enough to hold metadata and
59    /// journal. Defaults to 4 GiB.
60    pub size_bytes: u64,
61
62    /// Number of 4 KiB blocks to allocate for the journal.
63    /// Defaults to 16384 (64 MiB).
64    pub journal_blocks: u32,
65}
66
67/// Errors that can occur during ext4 formatting.
68#[derive(Debug)]
69pub enum Ext4Error {
70    /// An I/O error occurred while writing the image.
71    Io(io::Error),
72
73    /// The requested image size is too small to hold the minimum metadata and
74    /// journal.
75    TooSmall,
76
77    /// The requested tree cannot be serialized by this minimal formatter.
78    Layout(String),
79}
80
81/// Internal layout computed from `Ext4FormatOptions`.
82struct Layout {
83    num_blocks: u64,
84    num_groups: u32,
85    uuid: [u8; 16],
86    gdt_blocks: u32,
87    /// First block of the inode table in group 0.
88    inode_table_block: u32,
89    /// Number of blocks occupied by the inode table in group 0.
90    inode_table_blocks: u32,
91    /// First data block after inode table (root dir block).
92    first_data_block: u32,
93    /// First block of the journal region.
94    journal_start_block: u32,
95    /// Total journal blocks.
96    journal_blocks: u32,
97    /// CRC32C checksum seed derived from the UUID.
98    csum_seed: u32,
99    /// Feature compat flags.
100    feature_compat: u32,
101    /// Feature incompat flags.
102    feature_incompat: u32,
103    /// Feature ro-compat flags.
104    feature_ro_compat: u32,
105}
106
107struct FsStats {
108    group_free_blocks: Vec<u32>,
109    group_free_inodes: Vec<u32>,
110    group_used_dirs: Vec<u32>,
111    total_free_blocks: u64,
112    total_free_inodes: u64,
113    total_used_blocks: u64,
114}
115
116enum NodeKind {
117    Directory { children: u16, data: Vec<u8> },
118    RegularFile { data: Vec<u8> },
119    Symlink { target: Vec<u8>, inline: bool },
120    CharDevice { major: u32, minor: u32 },
121}
122
123struct NodePlan {
124    inode: u32,
125    path: String,
126    permissions: u16,
127    uid: u16,
128    gid: u16,
129    kind: NodeKind,
130    block_start: Option<u32>,
131    block_count: u32,
132}
133
134struct DraftDirectory {
135    children: u16,
136    data: Vec<u8>,
137}
138
139struct DirEntrySpec {
140    inode: u32,
141    file_type: u8,
142    name: Vec<u8>,
143}
144
145struct DataAllocator {
146    regions: Vec<(u32, u32)>,
147}
148
149//--------------------------------------------------------------------------------------------------
150// Methods
151//--------------------------------------------------------------------------------------------------
152
153impl Default for Ext4FormatOptions {
154    fn default() -> Self {
155        Self {
156            size_bytes: DEFAULT_SIZE_BYTES,
157            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
158        }
159    }
160}
161
162impl std::fmt::Display for Ext4Error {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match self {
165            Ext4Error::Io(e) => write!(f, "ext4 I/O error: {e}"),
166            Ext4Error::TooSmall => write!(f, "image size is too small for ext4 formatting"),
167            Ext4Error::Layout(e) => write!(f, "ext4 layout error: {e}"),
168        }
169    }
170}
171
172impl std::error::Error for Ext4Error {
173    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
174        match self {
175            Ext4Error::Io(e) => Some(e),
176            Ext4Error::TooSmall | Ext4Error::Layout(_) => None,
177        }
178    }
179}
180
181impl From<io::Error> for Ext4Error {
182    fn from(e: io::Error) -> Self {
183        Ext4Error::Io(e)
184    }
185}
186
187impl Layout {
188    #[cfg(test)]
189    fn compute(opts: &Ext4FormatOptions) -> Result<Self, Ext4Error> {
190        Self::compute_with_root_blocks(opts, 1)
191    }
192
193    fn compute_with_root_blocks(
194        opts: &Ext4FormatOptions,
195        root_dir_blocks: u32,
196    ) -> Result<Self, Ext4Error> {
197        let block_size = EXT4_BLOCK_SIZE as u64;
198        let num_blocks = opts.size_bytes / block_size;
199        let num_groups_raw = num_blocks.div_ceil(EXT4_BLOCKS_PER_GROUP as u64);
200        let num_groups = num_groups_raw.min(MAX_GROUPS as u64) as u32;
201
202        // We need at least: superblock(1) + GDT(1) + reserved_gdt(256) +
203        // bitmaps(2) + inode_table + root_dir(1) + journal
204        let inode_table_blocks =
205            (EXT4_INODES_PER_GROUP as u64 * EXT4_INODE_SIZE as u64 / block_size) as u32;
206        let gdt_blocks = (num_groups as u64 * EXT4_DESC_SIZE as u64).div_ceil(block_size) as u32;
207
208        // Group 0 layout:
209        //   block 0: superblock (bytes 0-4095, sb at offset 1024)
210        //   next block(s): GDT
211        //   next reserved blocks: reserved GDT
212        //   next block: block bitmap
213        //   next block: inode bitmap
214        //   next N blocks: inode table
215        //   next block: root dir data block
216        //   next M blocks: journal
217
218        let overhead_blocks = 1 + gdt_blocks + RESERVED_GDT_BLOCKS; // sb + gdt + reserved_gdt
219        let block_bitmap_block = overhead_blocks;
220        let inode_bitmap_block = block_bitmap_block + 1;
221        let inode_table_block = inode_bitmap_block + 1;
222        let first_data_block = inode_table_block + inode_table_blocks;
223        let journal_start_block = first_data_block + root_dir_blocks;
224
225        let min_blocks = journal_start_block as u64 + opts.journal_blocks as u64 + 1; // +1 slack
226        if num_blocks < min_blocks {
227            return Err(Ext4Error::TooSmall);
228        }
229
230        // Generate a random UUID
231        let uuid = Self::generate_uuid();
232
233        let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, &uuid);
234
235        let feature_compat = EXT4_FEATURE_COMPAT_HAS_JOURNAL
236            | EXT4_FEATURE_COMPAT_EXT_ATTR
237            | EXT4_FEATURE_COMPAT_DIR_INDEX;
238
239        let feature_incompat = EXT4_FEATURE_INCOMPAT_FILETYPE
240            | EXT4_FEATURE_INCOMPAT_EXTENTS
241            | EXT4_FEATURE_INCOMPAT_64BIT;
242
243        let feature_ro_compat = EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER
244            | EXT4_FEATURE_RO_COMPAT_LARGE_FILE
245            | EXT4_FEATURE_RO_COMPAT_HUGE_FILE
246            | EXT4_FEATURE_RO_COMPAT_DIR_NLINK
247            | EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE
248            | EXT4_FEATURE_RO_COMPAT_METADATA_CSUM;
249
250        Ok(Layout {
251            num_blocks,
252            num_groups,
253            uuid,
254            gdt_blocks,
255            inode_table_block,
256            inode_table_blocks,
257            first_data_block,
258            journal_start_block,
259            journal_blocks: opts.journal_blocks,
260            csum_seed,
261            feature_compat,
262            feature_incompat,
263            feature_ro_compat,
264        })
265    }
266
267    fn generate_uuid() -> [u8; 16] {
268        // Simple random UUID using /dev/urandom or fallback to timestamp-based
269        let mut uuid = [0u8; 16];
270        if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
271            use std::io::Read;
272            let _ = f.read_exact(&mut uuid);
273        } else {
274            // Fallback: use system time as entropy source
275            let now = std::time::SystemTime::now()
276                .duration_since(std::time::UNIX_EPOCH)
277                .unwrap_or_default();
278            let nanos = now.as_nanos();
279            uuid[..8].copy_from_slice(&(nanos as u64).to_le_bytes());
280            uuid[8..16].copy_from_slice(&((nanos >> 64) as u64).to_le_bytes());
281        }
282        // Set UUID version 4 and variant bits
283        uuid[6] = (uuid[6] & 0x0F) | 0x40;
284        uuid[7] = (uuid[7] & 0x3F) | 0x80;
285        uuid
286    }
287
288    fn group_start_block(&self, group: u32) -> u32 {
289        group * EXT4_BLOCKS_PER_GROUP
290    }
291
292    fn blocks_in_group(&self, group: u32) -> u32 {
293        let group_start = self.group_start_block(group) as u64;
294        std::cmp::min(
295            EXT4_BLOCKS_PER_GROUP as u64,
296            self.num_blocks.saturating_sub(group_start),
297        ) as u32
298    }
299
300    fn group_has_backup_super(&self, group: u32) -> bool {
301        group == 0 || sparse_super_group(group)
302    }
303
304    fn group_leading_overhead_blocks(&self, group: u32) -> u32 {
305        if self.group_has_backup_super(group) {
306            1 + self.gdt_blocks + RESERVED_GDT_BLOCKS
307        } else {
308            0
309        }
310    }
311
312    fn group_block_bitmap_block(&self, group: u32) -> u32 {
313        self.group_start_block(group) + self.group_leading_overhead_blocks(group)
314    }
315
316    fn group_inode_bitmap_block(&self, group: u32) -> u32 {
317        self.group_block_bitmap_block(group) + 1
318    }
319
320    fn group_inode_table_block(&self, group: u32) -> u32 {
321        self.group_inode_bitmap_block(group) + 1
322    }
323
324    fn group_data_start_block(&self, group: u32) -> u32 {
325        let mut start = self.group_start_block(group) + self.group_metadata_blocks(group);
326        if group == 0 {
327            start = self.journal_start_block + self.journal_blocks;
328        }
329        start
330    }
331
332    fn group_metadata_blocks(&self, group: u32) -> u32 {
333        self.group_leading_overhead_blocks(group) + 2 + self.inode_table_blocks
334    }
335
336    fn group_used_blocks(&self, group: u32) -> u32 {
337        let mut used = self.group_metadata_blocks(group);
338        if group == 0 {
339            used += 1 + self.journal_blocks; // root dir + journal
340        }
341        used.min(self.blocks_in_group(group))
342    }
343
344    fn group_free_blocks(&self, group: u32) -> u32 {
345        self.blocks_in_group(group)
346            .saturating_sub(self.group_used_blocks(group))
347    }
348
349    fn group_free_inodes(&self, group: u32) -> u32 {
350        if group == 0 {
351            EXT4_INODES_PER_GROUP - (EXT4_FIRST_INO - 1)
352        } else {
353            EXT4_INODES_PER_GROUP
354        }
355    }
356
357    #[cfg(test)]
358    fn group_used_dirs(&self, group: u32) -> u32 {
359        if group == 0 { 1 } else { 0 }
360    }
361
362    fn total_free_blocks(&self) -> u64 {
363        (0..self.num_groups)
364            .map(|group| self.group_free_blocks(group) as u64)
365            .sum()
366    }
367
368    fn total_free_inodes(&self) -> u64 {
369        (0..self.num_groups)
370            .map(|group| self.group_free_inodes(group) as u64)
371            .sum()
372    }
373
374    fn total_used_blocks(&self) -> u64 {
375        (0..self.num_groups)
376            .map(|group| self.group_used_blocks(group) as u64)
377            .sum()
378    }
379}
380
381//--------------------------------------------------------------------------------------------------
382// Functions
383//--------------------------------------------------------------------------------------------------
384
385/// Create and format a sparse ext4 filesystem image at `path`.
386///
387/// The image is suitable for use as an overlayfs upper layer. It is created as
388/// a sparse file so the initial on-disk footprint is minimal despite the large
389/// logical size.
390pub fn format_ext4(path: &Path, options: &Ext4FormatOptions) -> Result<(), Ext4Error> {
391    let tree = FileTree::new();
392    format_ext4_with_tree(path, options, tree)
393}
394
395pub fn format_ext4_with_tree(
396    path: &Path,
397    options: &Ext4FormatOptions,
398    tree: FileTree,
399) -> Result<(), Ext4Error> {
400    let mut next_inode = EXT4_FIRST_INO;
401    let mut plans = Vec::new();
402    let root_mode = tree.root.metadata.mode;
403    let root_draft = draft_directory(
404        "/",
405        tree.root,
406        EXT4_ROOT_INO,
407        EXT4_ROOT_INO,
408        &mut next_inode,
409        &mut plans,
410    )?;
411    let root_dir_blocks = blocks_for_len(root_draft.data.len());
412    let layout = Layout::compute_with_root_blocks(options, root_dir_blocks.max(1))?;
413    let mut allocator = DataAllocator::new(&layout);
414
415    for plan in &mut plans {
416        allocate_node_data(&mut allocator, plan)?;
417    }
418
419    let mut all_plans = Vec::with_capacity(plans.len() + 1);
420    all_plans.push(NodePlan {
421        inode: EXT4_ROOT_INO,
422        path: "/".to_string(),
423        permissions: normalize_dir_permissions(root_mode),
424        uid: 0,
425        gid: 0,
426        kind: NodeKind::Directory {
427            children: root_draft.children,
428            data: root_draft.data,
429        },
430        block_start: Some(layout.first_data_block),
431        block_count: root_dir_blocks.max(1),
432    });
433    all_plans.extend(plans);
434    all_plans.sort_by_key(|plan| plan.inode);
435
436    let block_bitmaps = build_block_bitmaps_for_plan(&layout, &all_plans);
437    let inode_bitmaps = build_inode_bitmaps_for_plan(&layout, &all_plans);
438    let stats = compute_fs_stats(&layout, &block_bitmaps, &all_plans);
439
440    let raw_file = std::fs::File::create(path)?;
441    raw_file.set_len(options.size_bytes)?;
442    let mut file = BufWriter::new(raw_file);
443
444    write_bitmaps(&mut file, &layout, &block_bitmaps, &inode_bitmaps)?;
445    write_tree_data(&mut file, &layout, &all_plans)?;
446    write_inode_table_with_plan(&mut file, &layout, &all_plans)?;
447    write_journal(&mut file, &layout)?;
448
449    let sb_bytes = build_superblock_with_stats(&layout, &stats)?;
450    write_superblock_at(&mut file, 0, &sb_bytes)?;
451
452    let gdt_bytes = build_gdt_with_stats(&layout, &stats, &block_bitmaps, &inode_bitmaps)?;
453    write_gdt_at(&mut file, 0, &gdt_bytes)?;
454
455    for g in 1..layout.num_groups {
456        if sparse_super_group(g) {
457            let group_start_block = g as u64 * EXT4_BLOCKS_PER_GROUP as u64;
458            write_superblock_at(&mut file, group_start_block, &sb_bytes)?;
459            write_gdt_at(&mut file, group_start_block, &gdt_bytes)?;
460        }
461    }
462
463    file.flush()?;
464    // No sync_all() — the image is read from page cache by the VM on the
465    // same host. Fsync would add 1-10ms for no benefit.
466
467    Ok(())
468}
469
470fn draft_directory(
471    path: &str,
472    dir: DirectoryNode,
473    inode: u32,
474    parent_inode: u32,
475    next_inode: &mut u32,
476    plans: &mut Vec<NodePlan>,
477) -> Result<DraftDirectory, Ext4Error> {
478    if !dir.xattrs.is_empty() {
479        return Err(Ext4Error::Layout(format!(
480            "ext4 patch baking does not yet support xattrs on '{path}'"
481        )));
482    }
483
484    let mut children = Vec::new();
485    let mut child_dir_count = 0u16;
486
487    for (name, node) in dir.entries {
488        let name_bytes = name.as_os_str().as_encoded_bytes().to_vec();
489        let child_path = child_path(path, &name_bytes);
490        let child_inode = *next_inode;
491        if child_inode >= EXT4_INODES_PER_GROUP {
492            return Err(Ext4Error::Layout(
493                "too many upper-layer inodes for group 0 inode table".to_string(),
494            ));
495        }
496        *next_inode += 1;
497
498        match node {
499            TreeNode::Directory(child_dir) => {
500                child_dir_count = child_dir_count.saturating_add(1);
501                let dir_mode = child_dir.metadata.mode;
502                let child_draft = draft_directory(
503                    &child_path,
504                    child_dir,
505                    child_inode,
506                    inode,
507                    next_inode,
508                    plans,
509                )?;
510                let block_count = blocks_for_len(child_draft.data.len());
511                plans.push(NodePlan {
512                    inode: child_inode,
513                    path: child_path.clone(),
514                    permissions: normalize_dir_permissions(dir_mode),
515                    uid: 0,
516                    gid: 0,
517                    kind: NodeKind::Directory {
518                        children: child_draft.children,
519                        data: child_draft.data,
520                    },
521                    block_start: None,
522                    block_count,
523                });
524                children.push(DirEntrySpec {
525                    inode: child_inode,
526                    file_type: EXT4_FT_DIR,
527                    name: name_bytes,
528                });
529            }
530            TreeNode::RegularFile(file) => {
531                if !file.xattrs.is_empty() {
532                    return Err(Ext4Error::Layout(format!(
533                        "ext4 patch baking does not yet support xattrs on '{child_path}'"
534                    )));
535                }
536                plans.push(NodePlan {
537                    inode: child_inode,
538                    path: child_path.clone(),
539                    permissions: normalize_file_permissions(file.metadata.mode),
540                    uid: 0,
541                    gid: 0,
542                    block_count: blocks_for_len(file.data.len()),
543                    kind: NodeKind::RegularFile {
544                        data: file.data.read_all().map_err(Ext4Error::Io)?,
545                    },
546                    block_start: None,
547                });
548                children.push(DirEntrySpec {
549                    inode: child_inode,
550                    file_type: EXT4_FT_REG_FILE,
551                    name: name_bytes,
552                });
553            }
554            TreeNode::Symlink(symlink) => {
555                let target_len = symlink.target.len();
556                let inline = target_len <= 59;
557                let block_count = if inline {
558                    0
559                } else {
560                    blocks_for_len(target_len)
561                };
562                plans.push(NodePlan {
563                    inode: child_inode,
564                    path: child_path.clone(),
565                    permissions: 0o777,
566                    uid: 0,
567                    gid: 0,
568                    kind: NodeKind::Symlink {
569                        target: symlink.target,
570                        inline,
571                    },
572                    block_start: None,
573                    block_count,
574                });
575                children.push(DirEntrySpec {
576                    inode: child_inode,
577                    file_type: EXT4_FT_SYMLINK,
578                    name: name_bytes,
579                });
580            }
581            TreeNode::CharDevice(device) => {
582                plans.push(NodePlan {
583                    inode: child_inode,
584                    path: child_path.clone(),
585                    permissions: 0,
586                    uid: 0,
587                    gid: 0,
588                    kind: NodeKind::CharDevice {
589                        major: device.major,
590                        minor: device.minor,
591                    },
592                    block_start: None,
593                    block_count: 0,
594                });
595                children.push(DirEntrySpec {
596                    inode: child_inode,
597                    file_type: EXT4_FT_CHRDEV,
598                    name: name_bytes,
599                });
600            }
601            _ => {
602                return Err(Ext4Error::Layout(format!(
603                    "unsupported upper-layer node at '{child_path}'"
604                )));
605            }
606        }
607    }
608
609    let data = build_directory_data(inode, parent_inode, &children, path)?;
610    Ok(DraftDirectory {
611        children: child_dir_count,
612        data,
613    })
614}
615
616fn child_path(parent: &str, name: &[u8]) -> String {
617    let name = String::from_utf8_lossy(name);
618    if parent == "/" {
619        format!("/{name}")
620    } else {
621        format!("{parent}/{name}")
622    }
623}
624
625fn normalize_file_permissions(mode: u16) -> u16 {
626    let perms = mode & 0o7777;
627    if perms == 0 { 0o644 } else { perms }
628}
629
630fn normalize_dir_permissions(mode: u16) -> u16 {
631    let perms = mode & 0o7777;
632    if perms == 0 { 0o755 } else { perms }
633}
634
635fn blocks_for_len(len: usize) -> u32 {
636    if len == 0 {
637        0
638    } else {
639        (len as u64).div_ceil(EXT4_BLOCK_SIZE as u64) as u32
640    }
641}
642
643fn build_directory_data(
644    dir_inode: u32,
645    parent_inode: u32,
646    children: &[DirEntrySpec],
647    path: &str,
648) -> Result<Vec<u8>, Ext4Error> {
649    let mut entries = Vec::with_capacity(children.len() + 2);
650    entries.push(DirEntrySpec {
651        inode: dir_inode,
652        file_type: EXT4_FT_DIR,
653        name: b".".to_vec(),
654    });
655    entries.push(DirEntrySpec {
656        inode: parent_inode,
657        file_type: EXT4_FT_DIR,
658        name: b"..".to_vec(),
659    });
660    entries.extend(children.iter().map(|entry| DirEntrySpec {
661        inode: entry.inode,
662        file_type: entry.file_type,
663        name: entry.name.clone(),
664    }));
665
666    let mut blocks = Vec::new();
667    let mut index = 0usize;
668    while index < entries.len() {
669        let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
670        let mut pos = 0usize;
671        let data_limit = EXT4_BLOCK_SIZE as usize - 12;
672        let block_start = index;
673
674        while index < entries.len() {
675            let min_len = dir_entry_len(entries[index].name.len());
676            let needed = if pos == 0 { min_len } else { pos + min_len };
677            if needed > data_limit {
678                if pos == 0 {
679                    return Err(Ext4Error::Layout(format!(
680                        "directory entry too large for '{path}'"
681                    )));
682                }
683                break;
684            }
685            pos += min_len;
686            index += 1;
687        }
688
689        let mut write_pos = 0usize;
690        for (entry_index, entry) in entries[block_start..index].iter().enumerate() {
691            let is_last = entry_index + 1 == index - block_start;
692            let rec_len = if is_last {
693                (data_limit - write_pos) as u16
694            } else {
695                dir_entry_len(entry.name.len()) as u16
696            };
697            put_le32(&mut block, write_pos, entry.inode);
698            put_le16(&mut block, write_pos + 4, rec_len);
699            block[write_pos + 6] = entry.name.len() as u8;
700            block[write_pos + 7] = entry.file_type;
701            block[write_pos + 8..write_pos + 8 + entry.name.len()].copy_from_slice(&entry.name);
702            write_pos += rec_len as usize;
703        }
704
705        let tail = data_limit;
706        put_le32(&mut block, tail, 0);
707        put_le16(&mut block, tail + 4, 12);
708        block[tail + 6] = 0;
709        block[tail + 7] = 0xDE;
710        blocks.extend_from_slice(&block);
711    }
712
713    if blocks.is_empty() {
714        let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
715        put_le32(&mut block, 0, dir_inode);
716        put_le16(&mut block, 4, 12);
717        block[6] = 1;
718        block[7] = EXT4_FT_DIR;
719        block[8] = b'.';
720        put_le32(&mut block, 12, parent_inode);
721        put_le16(&mut block, 16, (EXT4_BLOCK_SIZE - 24) as u16);
722        block[18] = 2;
723        block[19] = EXT4_FT_DIR;
724        block[20] = b'.';
725        block[21] = b'.';
726        let tail = EXT4_BLOCK_SIZE as usize - 12;
727        put_le32(&mut block, tail, 0);
728        put_le16(&mut block, tail + 4, 12);
729        block[tail + 7] = 0xDE;
730        blocks = block;
731    }
732
733    Ok(blocks)
734}
735
736fn dir_entry_len(name_len: usize) -> usize {
737    (8 + name_len + 3) & !3
738}
739
740fn allocate_node_data(allocator: &mut DataAllocator, plan: &mut NodePlan) -> Result<(), Ext4Error> {
741    if plan.block_count == 0 {
742        plan.block_start = None;
743        return Ok(());
744    }
745
746    plan.block_start = allocator.allocate(plan.block_count, &plan.path)?;
747    Ok(())
748}
749
750impl DataAllocator {
751    fn new(layout: &Layout) -> Self {
752        let mut regions = Vec::new();
753        for group in 0..layout.num_groups {
754            let group_start = layout.group_start_block(group);
755            let group_end = group_start + layout.blocks_in_group(group);
756            let start = layout.group_data_start_block(group);
757            if start < group_end {
758                regions.push((start, group_end - start));
759            }
760        }
761        Self { regions }
762    }
763
764    fn allocate(&mut self, blocks: u32, path: &str) -> Result<Option<u32>, Ext4Error> {
765        if blocks == 0 {
766            return Ok(None);
767        }
768
769        for region in &mut self.regions {
770            if region.1 >= blocks {
771                let start = region.0;
772                region.0 += blocks;
773                region.1 -= blocks;
774                return Ok(Some(start));
775            }
776        }
777
778        Err(Ext4Error::Layout(format!(
779            "not enough space in upper.ext4 for '{path}'"
780        )))
781    }
782}
783
784fn build_block_bitmaps_for_plan(layout: &Layout, plans: &[NodePlan]) -> Vec<Vec<u8>> {
785    let mut used_extents = Vec::new();
786    used_extents.push((
787        layout.first_data_block,
788        layout.journal_start_block - layout.first_data_block,
789    ));
790    used_extents.push((layout.journal_start_block, layout.journal_blocks));
791
792    for plan in plans {
793        if let Some(start) = plan.block_start
794            && plan.block_count > 0
795        {
796            used_extents.push((start, plan.block_count));
797        }
798    }
799
800    (0..layout.num_groups)
801        .map(|group| {
802            let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
803            let group_start = layout.group_start_block(group);
804            let group_end = group_start + layout.blocks_in_group(group);
805
806            for bit in 0..layout.group_metadata_blocks(group) {
807                bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
808            }
809
810            for (start, len) in &used_extents {
811                let extent_start = *start;
812                let extent_end = extent_start + *len;
813                let overlap_start = extent_start.max(group_start);
814                let overlap_end = extent_end.min(group_end);
815                if overlap_start < overlap_end {
816                    for block in overlap_start..overlap_end {
817                        let bit = block - group_start;
818                        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
819                    }
820                }
821            }
822
823            let blocks_in_group = layout.blocks_in_group(group);
824            for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
825                bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
826            }
827
828            bitmap
829        })
830        .collect()
831}
832
833fn build_inode_bitmaps_for_plan(layout: &Layout, plans: &[NodePlan]) -> Vec<Vec<u8>> {
834    let max_used_inode = plans
835        .iter()
836        .map(|plan| plan.inode)
837        .max()
838        .unwrap_or(EXT4_JOURNAL_INO)
839        .max(EXT4_FIRST_INO - 1);
840
841    (0..layout.num_groups)
842        .map(|group| {
843            let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
844            if group == 0 {
845                for bit in 0..max_used_inode {
846                    bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
847                }
848            }
849            for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
850                bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
851            }
852            bitmap
853        })
854        .collect()
855}
856
857fn compute_fs_stats(layout: &Layout, block_bitmaps: &[Vec<u8>], plans: &[NodePlan]) -> FsStats {
858    let max_used_inode = plans
859        .iter()
860        .map(|plan| plan.inode)
861        .max()
862        .unwrap_or(EXT4_JOURNAL_INO)
863        .max(EXT4_FIRST_INO - 1);
864    let dir_count = plans
865        .iter()
866        .filter(|plan| matches!(plan.kind, NodeKind::Directory { .. }))
867        .count() as u32;
868
869    let mut group_free_blocks = Vec::with_capacity(layout.num_groups as usize);
870    let mut total_free_blocks = 0u64;
871    let mut total_used_blocks = 0u64;
872    for (group, bitmap) in block_bitmaps
873        .iter()
874        .enumerate()
875        .take(layout.num_groups as usize)
876    {
877        let blocks_in_group = layout.blocks_in_group(group as u32) as usize;
878        let used = count_used_bits(bitmap, blocks_in_group);
879        let free = blocks_in_group.saturating_sub(used) as u32;
880        group_free_blocks.push(free);
881        total_free_blocks += free as u64;
882        total_used_blocks += used as u64;
883    }
884
885    let mut group_free_inodes = vec![EXT4_INODES_PER_GROUP; layout.num_groups as usize];
886    group_free_inodes[0] = EXT4_INODES_PER_GROUP - max_used_inode;
887    let total_free_inodes = group_free_inodes.iter().map(|count| *count as u64).sum();
888
889    let mut group_used_dirs = vec![0u32; layout.num_groups as usize];
890    group_used_dirs[0] = dir_count;
891
892    FsStats {
893        group_free_blocks,
894        group_free_inodes,
895        group_used_dirs,
896        total_free_blocks,
897        total_free_inodes,
898        total_used_blocks,
899    }
900}
901
902fn count_used_bits(bitmap: &[u8], bits: usize) -> usize {
903    let full_bytes = bits / 8;
904    let mut used: usize = bitmap[..full_bytes]
905        .iter()
906        .map(|b| b.count_ones() as usize)
907        .sum();
908
909    // Count remaining bits in the partial last byte.
910    let remaining = bits % 8;
911    if remaining > 0 {
912        let mask = (1u8 << remaining) - 1;
913        used += (bitmap[full_bytes] & mask).count_ones() as usize;
914    }
915    used
916}
917
918fn write_tree_data(
919    file: &mut (impl std::io::Write + std::io::Seek),
920    layout: &Layout,
921    plans: &[NodePlan],
922) -> Result<(), Ext4Error> {
923    for plan in plans {
924        match &plan.kind {
925            NodeKind::Directory { data, .. } => {
926                let start = plan.block_start.unwrap_or(layout.first_data_block);
927                let mut bytes = data.clone();
928                update_dir_block_checksums(layout.csum_seed, plan.inode, &mut bytes);
929                write_extent_bytes(file, start, &bytes)?;
930            }
931            NodeKind::RegularFile { data } => {
932                if let Some(start) = plan.block_start {
933                    write_extent_bytes(file, start, data)?;
934                }
935            }
936            NodeKind::Symlink { target, inline } => {
937                if !inline && let Some(start) = plan.block_start {
938                    write_extent_bytes(file, start, target)?;
939                }
940            }
941            NodeKind::CharDevice { .. } => {}
942        }
943    }
944
945    Ok(())
946}
947
948fn write_extent_bytes(
949    file: &mut (impl std::io::Write + std::io::Seek),
950    start_block: u32,
951    data: &[u8],
952) -> Result<(), Ext4Error> {
953    let offset = start_block as u64 * EXT4_BLOCK_SIZE as u64;
954    file.seek(SeekFrom::Start(offset))?;
955    file.write_all(data)?;
956
957    let pad = (EXT4_BLOCK_SIZE as usize - (data.len() % EXT4_BLOCK_SIZE as usize))
958        % EXT4_BLOCK_SIZE as usize;
959    if pad > 0 {
960        static ZEROS: [u8; 4096] = [0u8; 4096];
961        file.write_all(&ZEROS[..pad])?;
962    }
963
964    Ok(())
965}
966
967fn update_dir_block_checksums(csum_seed: u32, inode: u32, data: &mut [u8]) {
968    for chunk in data.chunks_exact_mut(EXT4_BLOCK_SIZE as usize) {
969        let tail = EXT4_BLOCK_SIZE as usize - 12;
970        let checksum = dir_block_checksum(csum_seed, inode, 0, &chunk[..tail]);
971        put_le32(chunk, tail + 8, checksum);
972    }
973}
974
975fn write_inode_table_with_plan(
976    file: &mut (impl std::io::Write + std::io::Seek),
977    layout: &Layout,
978    plans: &[NodePlan],
979) -> Result<(), Ext4Error> {
980    let table_offset = layout.inode_table_block as u64 * EXT4_BLOCK_SIZE as u64;
981
982    let root_inode = build_inode_from_plan(layout, &plans[0])?;
983    let root_offset = table_offset + (EXT4_ROOT_INO as u64 - 1) * EXT4_INODE_SIZE as u64;
984    file.seek(SeekFrom::Start(root_offset))?;
985    file.write_all(&root_inode)?;
986
987    let journal_inode = build_journal_inode(layout);
988    let journal_offset = table_offset + (EXT4_JOURNAL_INO as u64 - 1) * EXT4_INODE_SIZE as u64;
989    file.seek(SeekFrom::Start(journal_offset))?;
990    file.write_all(&journal_inode)?;
991
992    for plan in plans.iter().filter(|plan| plan.inode >= EXT4_FIRST_INO) {
993        let inode_bytes = build_inode_from_plan(layout, plan)?;
994        let inode_offset = table_offset + (plan.inode as u64 - 1) * EXT4_INODE_SIZE as u64;
995        file.seek(SeekFrom::Start(inode_offset))?;
996        file.write_all(&inode_bytes)?;
997    }
998
999    Ok(())
1000}
1001
1002fn build_inode_from_plan(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
1003    let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1004    let (mode, size, links_count, extents) = match &plan.kind {
1005        NodeKind::Directory { children, data } => (
1006            S_IFDIR | normalize_dir_permissions(plan.permissions),
1007            data.len() as u64,
1008            2 + *children,
1009            true,
1010        ),
1011        NodeKind::RegularFile { data } => (
1012            S_IFREG | normalize_file_permissions(plan.permissions),
1013            data.len() as u64,
1014            1,
1015            true,
1016        ),
1017        NodeKind::Symlink { target, inline } => (S_IFLNK | 0o777, target.len() as u64, 1, !inline),
1018        NodeKind::CharDevice { .. } => (S_IFCHR | plan.permissions, 0, 1, false),
1019    };
1020
1021    put_le16(&mut inode, 0x00, mode);
1022    put_le16(&mut inode, 0x02, plan.uid);
1023    put_le32(&mut inode, 0x04, size as u32);
1024    put_le16(&mut inode, 0x18, plan.gid);
1025    put_le16(&mut inode, 0x1A, links_count);
1026    put_le32(&mut inode, 0x1C, plan.block_count * (EXT4_BLOCK_SIZE / 512));
1027    if extents {
1028        put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1029    }
1030
1031    match &plan.kind {
1032        NodeKind::Directory { .. } | NodeKind::RegularFile { .. } => {
1033            if let Some(start) = plan.block_start {
1034                write_extent_tree(&mut inode, 0x28, start, plan.block_count as u16);
1035            } else {
1036                write_empty_extent_tree(&mut inode, 0x28);
1037            }
1038        }
1039        NodeKind::Symlink { target, inline } => {
1040            if *inline {
1041                inode[0x28..0x28 + target.len()].copy_from_slice(target);
1042            } else if let Some(start) = plan.block_start {
1043                write_extent_tree(&mut inode, 0x28, start, plan.block_count as u16);
1044            }
1045        }
1046        NodeKind::CharDevice { major, minor } => {
1047            put_le32(&mut inode, 0x28, (*minor & 0xFF) | (major << 8));
1048        }
1049    }
1050
1051    put_le32(&mut inode, 0x64, 0);
1052    put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1053    put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1054
1055    let csum = inode_checksum(layout.csum_seed, plan.inode, 0, &inode);
1056    put_le16(&mut inode, 0x7C, csum as u16);
1057    put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1058
1059    Ok(inode)
1060}
1061
1062fn write_empty_extent_tree(buf: &mut [u8], offset: usize) {
1063    put_le16(buf, offset, EXT4_EH_MAGIC);
1064    put_le16(buf, offset + 2, 0);
1065    put_le16(buf, offset + 4, 4);
1066    put_le16(buf, offset + 6, 0);
1067    put_le32(buf, offset + 8, 0);
1068}
1069
1070fn build_superblock_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1071    let mut block = build_superblock(layout)?;
1072    let sb = &mut block[1024..2048];
1073    put_le32(sb, 0x0C, stats.total_free_blocks as u32);
1074    put_le32(sb, 0x10, stats.total_free_inodes as u32);
1075    put_le32(sb, 0x158, (stats.total_free_blocks >> 32) as u32);
1076    put_le32(sb, 0x194, stats.total_used_blocks as u32);
1077    put_le32(sb, 0x3FC, 0);
1078    let checksum = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
1079    put_le32(sb, 0x3FC, checksum);
1080    Ok(block)
1081}
1082
1083fn build_gdt_with_stats(
1084    layout: &Layout,
1085    stats: &FsStats,
1086    block_bitmaps: &[Vec<u8>],
1087    inode_bitmaps: &[Vec<u8>],
1088) -> Result<Vec<u8>, Ext4Error> {
1089    let desc_size = EXT4_DESC_SIZE as usize;
1090    let mut gdt = vec![0u8; layout.num_groups as usize * desc_size];
1091
1092    for g in 0..layout.num_groups {
1093        let off = g as usize * desc_size;
1094        let desc = &mut gdt[off..off + desc_size];
1095        let bb = layout.group_block_bitmap_block(g);
1096        let ib = layout.group_inode_bitmap_block(g);
1097        let it = layout.group_inode_table_block(g);
1098        let bb_csum = bitmap_checksum(
1099            layout.csum_seed,
1100            &block_bitmaps[g as usize],
1101            EXT4_BLOCK_SIZE as usize,
1102        );
1103        let ib_csum = bitmap_checksum(
1104            layout.csum_seed,
1105            &inode_bitmaps[g as usize],
1106            (EXT4_INODES_PER_GROUP / 8) as usize,
1107        );
1108
1109        put_le32(desc, 0x00, bb);
1110        put_le32(desc, 0x04, ib);
1111        put_le32(desc, 0x08, it);
1112        put_le16(desc, 0x0C, stats.group_free_blocks[g as usize] as u16);
1113        put_le16(desc, 0x0E, stats.group_free_inodes[g as usize] as u16);
1114        put_le16(desc, 0x10, stats.group_used_dirs[g as usize] as u16);
1115        put_le16(desc, 0x12, EXT4_BG_INODE_ZEROED);
1116        put_le16(desc, 0x18, bb_csum as u16);
1117        put_le16(desc, 0x1A, ib_csum as u16);
1118        put_le16(desc, 0x1C, stats.group_free_inodes[g as usize] as u16);
1119        put_le16(desc, 0x38, (bb_csum >> 16) as u16);
1120        put_le16(desc, 0x3A, (ib_csum >> 16) as u16);
1121        put_le16(desc, 0x1E, 0);
1122        let checksum = gdt_checksum(layout.csum_seed, g, desc);
1123        put_le16(desc, 0x1E, checksum);
1124    }
1125
1126    Ok(gdt)
1127}
1128#[cfg(test)]
1129fn build_block_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1130    (0..layout.num_groups)
1131        .map(|group| build_block_bitmap(layout, group))
1132        .collect()
1133}
1134
1135#[cfg(test)]
1136fn build_inode_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1137    (0..layout.num_groups)
1138        .map(|group| build_inode_bitmap(layout, group))
1139        .collect()
1140}
1141
1142#[cfg(test)]
1143fn build_block_bitmap(layout: &Layout, group: u32) -> Vec<u8> {
1144    let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1145
1146    // Metadata, the root directory block, and the journal are permanently
1147    // allocated within the filesystem image.
1148    let used = layout.group_used_blocks(group);
1149    for bit in 0..used {
1150        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1151    }
1152
1153    // Bits beyond the final partial group are permanently unavailable.
1154    let blocks_in_group = layout.blocks_in_group(group);
1155    for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
1156        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1157    }
1158
1159    bitmap
1160}
1161
1162#[cfg(test)]
1163fn build_inode_bitmap(_layout: &Layout, group: u32) -> Vec<u8> {
1164    let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1165
1166    if group == 0 {
1167        // Inode numbering is 1-based; bit 0 corresponds to inode 1.
1168        for bit in 0..(EXT4_FIRST_INO - 1) {
1169            bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1170        }
1171    }
1172
1173    // The inode bitmap consumes only the first inodes-per-group bits; the
1174    // remaining padding bits in the block must stay permanently set.
1175    for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
1176        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1177    }
1178
1179    bitmap
1180}
1181
1182fn write_bitmaps(
1183    file: &mut (impl std::io::Write + std::io::Seek),
1184    layout: &Layout,
1185    block_bitmaps: &[Vec<u8>],
1186    inode_bitmaps: &[Vec<u8>],
1187) -> Result<(), Ext4Error> {
1188    for group in 0..layout.num_groups as usize {
1189        let block_offset =
1190            layout.group_block_bitmap_block(group as u32) as u64 * EXT4_BLOCK_SIZE as u64;
1191        file.seek(SeekFrom::Start(block_offset))?;
1192        file.write_all(&block_bitmaps[group])?;
1193
1194        let inode_offset =
1195            layout.group_inode_bitmap_block(group as u32) as u64 * EXT4_BLOCK_SIZE as u64;
1196        file.seek(SeekFrom::Start(inode_offset))?;
1197        file.write_all(&inode_bitmaps[group])?;
1198    }
1199
1200    Ok(())
1201}
1202
1203/// Build the 256-byte journal inode (inode 8).
1204fn build_journal_inode(layout: &Layout) -> Vec<u8> {
1205    let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1206
1207    let mode = S_IFREG | 0o600;
1208    let size = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1209
1210    // i_mode (offset 0, u16)
1211    put_le16(&mut inode, 0x00, mode);
1212    // i_size_lo (offset 4, u32)
1213    put_le32(&mut inode, 0x04, size as u32);
1214    // i_size_high (offset 108, u32)
1215    put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1216    // i_links_count (offset 26, u16)
1217    put_le16(&mut inode, 0x1A, 1);
1218    // i_blocks_lo (offset 28, u32) -- in 512-byte sectors
1219    let sectors = (layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64) / 512;
1220    put_le32(&mut inode, 0x1C, sectors as u32);
1221    // i_flags (offset 32, u32)
1222    put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1223
1224    // i_block (offset 40, 60 bytes) -- extent tree pointing to journal blocks
1225    write_extent_tree(
1226        &mut inode,
1227        0x28,
1228        layout.journal_start_block,
1229        layout.journal_blocks as u16,
1230    );
1231
1232    // i_generation (offset 100, u32)
1233    put_le32(&mut inode, 0x64, 0);
1234
1235    // -- Extended inode fields --
1236    // i_extra_isize (offset 128, u16)
1237    put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1238
1239    // Inode checksum
1240    let csum = inode_checksum(layout.csum_seed, EXT4_JOURNAL_INO, 0, &inode);
1241    // l_i_checksum_lo (offset 0x7C, u16)
1242    put_le16(&mut inode, 0x7C, csum as u16);
1243    // i_checksum_hi (offset 0x82, u16)
1244    put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1245
1246    inode
1247}
1248
1249/// Write an extent tree header + one extent entry into `buf` at `offset`.
1250///
1251/// The extent tree header is 12 bytes, each extent entry is also 12 bytes.
1252fn write_extent_tree(buf: &mut [u8], offset: usize, start_block: u32, block_count: u16) {
1253    // Extent header (12 bytes)
1254    put_le16(buf, offset, EXT4_EH_MAGIC); // eh_magic
1255    put_le16(buf, offset + 2, 1); // eh_entries
1256    put_le16(buf, offset + 4, 4); // eh_max (for inode: (60-12)/12 = 4)
1257    put_le16(buf, offset + 6, 0); // eh_depth (leaf)
1258    put_le32(buf, offset + 8, 0); // eh_generation
1259
1260    // Extent entry (12 bytes) at offset+12
1261    let ext_off = offset + 12;
1262    put_le32(buf, ext_off, 0); // ee_block (logical block 0)
1263    put_le16(buf, ext_off + 4, block_count); // ee_len
1264    put_le16(buf, ext_off + 6, 0); // ee_start_hi
1265    put_le32(buf, ext_off + 8, start_block); // ee_start_lo
1266}
1267
1268/// Write the journal superblock at the first journal block.
1269fn write_journal(
1270    file: &mut (impl std::io::Write + std::io::Seek),
1271    layout: &Layout,
1272) -> Result<(), Ext4Error> {
1273    let mut jsb = vec![0u8; EXT4_BLOCK_SIZE as usize];
1274
1275    // All jbd2 fields are BIG-ENDIAN.
1276    // Header (12 bytes)
1277    put_be32(&mut jsb, 0, JBD2_MAGIC); // h_magic
1278    put_be32(&mut jsb, 4, JBD2_SUPERBLOCK_V2); // h_blocktype
1279    put_be32(&mut jsb, 8, 0); // h_sequence (not used for sb)
1280
1281    // Journal superblock fields
1282    put_be32(&mut jsb, 12, EXT4_BLOCK_SIZE); // s_blocksize
1283    put_be32(&mut jsb, 16, layout.journal_blocks); // s_maxlen
1284    put_be32(&mut jsb, 20, 1); // s_first (first log block)
1285    put_be32(&mut jsb, 24, 1); // s_sequence (next expected sequence)
1286    put_be32(&mut jsb, 28, 0); // s_start (0 = clean/no recovery needed)
1287
1288    // s_errno (offset 32)
1289    put_be32(&mut jsb, 32, 0);
1290    // s_feature_compat (offset 36)
1291    put_be32(&mut jsb, 36, 0);
1292    // s_feature_incompat (offset 40) = CSUM_V3(0x10) | 64BIT(0x02) | REVOKE(0x01)
1293    put_be32(&mut jsb, 40, 0x13);
1294    // s_feature_ro_compat (offset 44)
1295    put_be32(&mut jsb, 44, 0);
1296
1297    // s_uuid (offset 48, 16 bytes) -- same as filesystem UUID
1298    jsb[48..64].copy_from_slice(&layout.uuid);
1299
1300    // s_nr_users (offset 64, u32)
1301    put_be32(&mut jsb, 64, 1);
1302
1303    // s_dynsuper (offset 68, u32) -- block of dynamic superblock copy
1304    put_be32(&mut jsb, 68, 0);
1305
1306    // s_max_transaction (offset 72), s_max_trans_data (offset 76)
1307    put_be32(&mut jsb, 72, 0);
1308    put_be32(&mut jsb, 76, 0);
1309
1310    // s_checksum_type (offset 80, u8) = 4 (CRC32C)
1311    // Actually offset for checksum_type is at offset 80+... Let me use correct
1312    // offsets from the jbd2 spec:
1313    //   offset 80: padding (u8)
1314    //   offset 81-83: padding
1315    //   offset 84-87: s_padding2
1316    //   offset 88-91: s_num_fc_blks
1317    //   offset 92-95: s_head
1318    //   offset 96-255: s_padding[44]
1319    //   offset 256-271: s_users[16*48] (first 16 bytes = first user UUID)
1320    //
1321    // Correct jbd2 superblock layout (from kernel headers):
1322    //   0x00: h_magic (u32be)
1323    //   0x04: h_blocktype (u32be)
1324    //   0x08: h_sequence (u32be)
1325    //   0x0C: s_blocksize (u32be)
1326    //   0x10: s_maxlen (u32be)
1327    //   0x14: s_first (u32be)
1328    //   0x18: s_sequence (u32be)
1329    //   0x1C: s_start (u32be)
1330    //   0x20: s_errno (u32be)
1331    //   0x24: s_feature_compat (u32be)
1332    //   0x28: s_feature_incompat (u32be)
1333    //   0x2C: s_feature_ro_compat (u32be)
1334    //   0x30: s_uuid[16]
1335    //   0x40: s_nr_users (u32be)
1336    //   0x44: s_dynsuper (u32be)
1337    //   0x48: s_max_transaction (u32be)
1338    //   0x4C: s_max_trans_data (u32be)
1339    //   0x50: s_checksum_type (u8)
1340    //   0x51: s_padding2[3]
1341    //   0x54: s_padding[42] (u32be array = 168 bytes)
1342    //   0xFC: s_checksum (u32be)
1343    //   0x100: s_users[16*48]
1344
1345    jsb[0x50] = 4; // s_checksum_type = CRC32C
1346
1347    // s_checksum (offset 0xFC, u32be), computed over the 1024-byte on-disk
1348    // jbd2 superblock with the checksum field zeroed.
1349    let jsb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb[..JBD2_SUPERBLOCK_SIZE]);
1350    put_be32(&mut jsb, 0xFC, jsb_csum);
1351
1352    let offset = layout.journal_start_block as u64 * EXT4_BLOCK_SIZE as u64;
1353    file.seek(SeekFrom::Start(offset))?;
1354    file.write_all(&jsb)?;
1355
1356    Ok(())
1357}
1358
1359/// Build the 1024-byte ext4 superblock.
1360fn build_superblock(layout: &Layout) -> Result<Vec<u8>, Ext4Error> {
1361    // The superblock is 1024 bytes starting at byte 1024 within block 0.
1362    // We build a full 4096-byte block with the sb at offset 1024.
1363    let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1364    let sb = &mut block[1024..2048]; // 1024-byte superblock
1365
1366    let total_blocks = layout.num_blocks;
1367    let total_inodes = layout.num_groups as u64 * EXT4_INODES_PER_GROUP as u64;
1368
1369    let free_blocks = layout.total_free_blocks();
1370    let free_inodes = layout.total_free_inodes();
1371
1372    // s_inodes_count (0x00, u32)
1373    put_le32(sb, 0x00, total_inodes as u32);
1374    // s_blocks_count_lo (0x04, u32)
1375    put_le32(sb, 0x04, total_blocks as u32);
1376    // s_r_blocks_count_lo (0x08, u32) -- reserved blocks for superuser
1377    put_le32(sb, 0x08, 0);
1378    // s_free_blocks_count_lo (0x0C, u32)
1379    put_le32(sb, 0x0C, free_blocks as u32);
1380    // s_free_inodes_count (0x10, u32)
1381    put_le32(sb, 0x10, free_inodes as u32);
1382    // s_first_data_block (0x14, u32) -- 0 for 4k blocks
1383    put_le32(sb, 0x14, 0);
1384    // s_log_block_size (0x18, u32)
1385    put_le32(sb, 0x18, EXT4_LOG_BLOCK_SIZE);
1386    // s_log_cluster_size (0x1C, u32)
1387    put_le32(sb, 0x1C, EXT4_LOG_BLOCK_SIZE);
1388    // s_blocks_per_group (0x20, u32)
1389    put_le32(sb, 0x20, EXT4_BLOCKS_PER_GROUP);
1390    // s_clusters_per_group (0x24, u32)
1391    put_le32(sb, 0x24, EXT4_BLOCKS_PER_GROUP);
1392    // s_inodes_per_group (0x28, u32)
1393    put_le32(sb, 0x28, EXT4_INODES_PER_GROUP);
1394
1395    // s_mtime (0x2C, u32), s_wtime (0x30, u32)
1396    // Leave as 0.
1397
1398    // s_mnt_count (0x34, u16)
1399    put_le16(sb, 0x34, 0);
1400    // s_max_mnt_count (0x36, u16) -- -1 = no limit
1401    put_le16(sb, 0x36, 0xFFFF);
1402    // s_magic (0x38, u16)
1403    put_le16(sb, 0x38, EXT4_SUPER_MAGIC);
1404    // s_state (0x3A, u16) -- 1 = clean
1405    put_le16(sb, 0x3A, 1);
1406    // s_errors (0x3C, u16) -- 1 = continue
1407    put_le16(sb, 0x3C, 1);
1408    // s_minor_rev_level (0x3E, u16)
1409    put_le16(sb, 0x3E, 0);
1410
1411    // s_lastcheck (0x40, u32), s_checkinterval (0x44, u32)
1412    // Leave as 0.
1413
1414    // s_creator_os (0x48, u32) -- 0 = Linux
1415    put_le32(sb, 0x48, 0);
1416    // s_rev_level (0x4C, u32) -- 1 = dynamic rev
1417    put_le32(sb, 0x4C, 1);
1418
1419    // s_def_resuid (0x50, u16) -- 0
1420    put_le16(sb, 0x50, 0);
1421    // s_def_resgid (0x52, u16) -- 0
1422    put_le16(sb, 0x52, 0);
1423
1424    // --- EXT4_DYNAMIC_REV specific ---
1425    // s_first_ino (0x54, u32)
1426    put_le32(sb, 0x54, EXT4_FIRST_INO);
1427    // s_inode_size (0x58, u16)
1428    put_le16(sb, 0x58, EXT4_INODE_SIZE);
1429    // s_block_group_nr (0x5A, u16) -- block group hosting this superblock
1430    put_le16(sb, 0x5A, 0);
1431
1432    // s_feature_compat (0x5C, u32)
1433    put_le32(sb, 0x5C, layout.feature_compat);
1434    // s_feature_incompat (0x60, u32)
1435    put_le32(sb, 0x60, layout.feature_incompat);
1436    // s_feature_ro_compat (0x64, u32)
1437    put_le32(sb, 0x64, layout.feature_ro_compat);
1438
1439    // s_uuid (0x68, 16 bytes)
1440    sb[0x68..0x78].copy_from_slice(&layout.uuid);
1441
1442    // s_volume_name (0x78, 16 bytes) -- leave empty
1443
1444    // s_last_mounted (0x88, 64 bytes) -- leave empty
1445
1446    // s_algorithm_usage_bitmap (0xC8, u32) -- 0
1447    put_le32(sb, 0xC8, 0);
1448
1449    // s_prealloc_blocks (0xCC, u8), s_prealloc_dir_blocks (0xCD, u8)
1450    sb[0xCC] = 0;
1451    sb[0xCD] = 0;
1452
1453    // s_reserved_gdt_blocks (0xCE, u16)
1454    put_le16(sb, 0xCE, RESERVED_GDT_BLOCKS as u16);
1455
1456    // s_journal_uuid (0xD0, 16 bytes) -- leave zeroed (internal journal)
1457
1458    // s_journal_inum (0xE0, u32)
1459    put_le32(sb, 0xE0, EXT4_JOURNAL_INO);
1460    // s_journal_dev (0xE4, u32) -- 0 (internal)
1461    put_le32(sb, 0xE4, 0);
1462    // s_last_orphan (0xE8, u32)
1463    put_le32(sb, 0xE8, 0);
1464
1465    // s_hash_seed (0xEC, 4*u32 = 16 bytes) -- random
1466    sb[0xEC..0xFC].copy_from_slice(&layout.uuid); // reuse uuid bytes as hash seed
1467
1468    // s_def_hash_version (0xFC, u8) -- 1 = half MD4
1469    sb[0xFC] = 1;
1470    // s_jnl_backup_type (0xFD, u8) -- 1
1471    sb[0xFD] = 1;
1472
1473    // s_desc_size (0xFE, u16)
1474    put_le16(sb, 0xFE, EXT4_DESC_SIZE);
1475
1476    // s_default_mount_opts (0x100, u32) -- 0x000C (user_xattr, acl)
1477    put_le32(sb, 0x100, 0x000C);
1478
1479    // s_first_meta_bg (0x104, u32)
1480    put_le32(sb, 0x104, 0);
1481
1482    // s_mkfs_time (0x108, u32) -- leave 0
1483
1484    // s_jnl_blocks (0x10C, 17*u32 = 68 bytes) -- journal inode i_block backup
1485    // Copy the extent tree from the journal inode.
1486    {
1487        let mut extent_buf = [0u8; 60];
1488        write_extent_tree(
1489            &mut extent_buf,
1490            0,
1491            layout.journal_start_block,
1492            layout.journal_blocks as u16,
1493        );
1494        // Copy 15 u32s (60 bytes) into s_jnl_blocks
1495        sb[0x10C..0x10C + 60].copy_from_slice(&extent_buf);
1496        // s_jnl_blocks[15] = i_size_lo
1497        let jsize = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1498        put_le32(sb, 0x10C + 60, jsize as u32);
1499        // s_jnl_blocks[16] = i_size_hi
1500        put_le32(sb, 0x10C + 64, (jsize >> 32) as u32);
1501    }
1502
1503    // --- 64-bit fields ---
1504    // s_blocks_count_hi (0x150, u32)
1505    put_le32(sb, 0x150, (total_blocks >> 32) as u32);
1506    // s_r_blocks_count_hi (0x154, u32)
1507    put_le32(sb, 0x154, 0);
1508    // s_free_blocks_count_hi (0x158, u32)
1509    put_le32(sb, 0x158, (free_blocks >> 32) as u32);
1510
1511    // s_min_extra_isize (0x15C, u16)
1512    put_le16(sb, 0x15C, EXT4_MIN_EXTRA_ISIZE);
1513    // s_want_extra_isize (0x15E, u16)
1514    put_le16(sb, 0x15E, EXT4_MIN_EXTRA_ISIZE);
1515
1516    // s_flags (0x160, u32)
1517    put_le32(sb, 0x160, 0);
1518
1519    // s_log_groups_per_flex (0x174, u8) -- flex_bg disabled
1520    sb[0x174] = 0;
1521
1522    // s_checksum_type (0x175, u8) -- 1 = CRC32C
1523    sb[0x175] = 1;
1524
1525    // s_kbytes_written (0x178, u64) -- 0
1526    // s_snapshot_inum, etc. -- leave zeroed
1527
1528    // s_overhead_clusters (0x194, u32)
1529    put_le32(sb, 0x194, layout.total_used_blocks() as u32);
1530
1531    // s_checksum_seed (0x270, u32) -- crc32c::crc32c_raw(~0, uuid)
1532    // Only used if INCOMPAT_CSUM_SEED is set. For METADATA_CSUM without
1533    // CSUM_SEED, the kernel computes from the UUID. We don't set
1534    // INCOMPAT_CSUM_SEED so leave this zero.
1535    put_le32(sb, 0x270, 0);
1536
1537    // s_encoding (0x27C, u16) -- 0 (no casefold)
1538    put_le16(sb, 0x27C, 0);
1539
1540    // s_checksum (0x3FC, u32) -- CRC32C of sb bytes 0..0x3FC
1541    let sb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
1542    put_le32(sb, 0x3FC, sb_csum);
1543
1544    Ok(block)
1545}
1546
1547/// Build the group descriptor table (GDT). Returns a byte vector containing
1548/// all group descriptors (64 bytes each).
1549#[cfg(test)]
1550fn build_gdt(
1551    layout: &Layout,
1552    block_bitmaps: &[Vec<u8>],
1553    inode_bitmaps: &[Vec<u8>],
1554) -> Result<Vec<u8>, Ext4Error> {
1555    let desc_size = EXT4_DESC_SIZE as usize;
1556    let mut gdt = vec![0u8; layout.num_groups as usize * desc_size];
1557
1558    for g in 0..layout.num_groups {
1559        let off = g as usize * desc_size;
1560        let desc = &mut gdt[off..off + desc_size];
1561        let bb = layout.group_block_bitmap_block(g);
1562        let ib = layout.group_inode_bitmap_block(g);
1563        let it = layout.group_inode_table_block(g);
1564        let bb_csum = bitmap_checksum(
1565            layout.csum_seed,
1566            &block_bitmaps[g as usize],
1567            EXT4_BLOCK_SIZE as usize,
1568        );
1569        let ib_csum = bitmap_checksum(
1570            layout.csum_seed,
1571            &inode_bitmaps[g as usize],
1572            (EXT4_INODES_PER_GROUP / 8) as usize,
1573        );
1574
1575        put_le32(desc, 0x00, bb);
1576        put_le32(desc, 0x04, ib);
1577        put_le32(desc, 0x08, it);
1578        put_le16(desc, 0x0C, layout.group_free_blocks(g) as u16);
1579        put_le16(desc, 0x0E, layout.group_free_inodes(g) as u16);
1580        put_le16(desc, 0x10, layout.group_used_dirs(g) as u16);
1581        put_le16(desc, 0x12, EXT4_BG_INODE_ZEROED);
1582        put_le32(desc, 0x14, 0);
1583        put_le16(desc, 0x18, bb_csum as u16);
1584        put_le16(desc, 0x1A, ib_csum as u16);
1585        put_le16(desc, 0x1C, layout.group_free_inodes(g) as u16);
1586        put_le32(desc, 0x20, 0);
1587        put_le32(desc, 0x24, 0);
1588        put_le32(desc, 0x28, 0);
1589        put_le16(desc, 0x2C, 0);
1590        put_le16(desc, 0x2E, 0);
1591        put_le16(desc, 0x30, 0);
1592        put_le16(desc, 0x32, 0);
1593        put_le32(desc, 0x34, 0);
1594        put_le16(desc, 0x38, (bb_csum >> 16) as u16);
1595        put_le16(desc, 0x3A, (ib_csum >> 16) as u16);
1596
1597        // GDT entry checksum (stored at bg_checksum, offset 0x1E)
1598        // crc32c(csum_seed, le32(group_num) || desc_bytes_with_checksum_zeroed) & 0xFFFF
1599        // Zero out the checksum field before computing.
1600        put_le16(desc, 0x1E, 0);
1601        let gdt_csum = gdt_checksum(layout.csum_seed, g, desc);
1602        put_le16(desc, 0x1E, gdt_csum);
1603    }
1604
1605    Ok(gdt)
1606}
1607
1608/// Write superblock at the given group's first block.
1609fn write_superblock_at(
1610    file: &mut (impl std::io::Write + std::io::Seek),
1611    group_start_block: u64,
1612    sb_block: &[u8],
1613) -> Result<(), Ext4Error> {
1614    // The superblock is always at byte offset 1024 within the first block of
1615    // the group. For group 0 the offset is 1024. For backup groups the sb is at
1616    // group_start_block * block_size + 0 (the whole block including the padding
1617    // before offset 1024).
1618    let offset = group_start_block * EXT4_BLOCK_SIZE as u64;
1619    file.seek(SeekFrom::Start(offset))?;
1620    file.write_all(sb_block)?;
1621    Ok(())
1622}
1623
1624/// Write GDT at block (group_start_block + 1).
1625fn write_gdt_at(
1626    file: &mut (impl std::io::Write + std::io::Seek),
1627    group_start_block: u64,
1628    gdt: &[u8],
1629) -> Result<(), Ext4Error> {
1630    let offset = (group_start_block + 1) * EXT4_BLOCK_SIZE as u64;
1631    file.seek(SeekFrom::Start(offset))?;
1632    file.write_all(gdt)?;
1633    Ok(())
1634}
1635
1636//--------------------------------------------------------------------------------------------------
1637// Functions: Checksums
1638//--------------------------------------------------------------------------------------------------
1639
1640/// GDT entry checksum (16-bit).
1641fn gdt_checksum(csum_seed: u32, group: u32, desc: &[u8]) -> u16 {
1642    let mut crc = crc32c::crc32c_raw(csum_seed, &group.to_le_bytes());
1643    crc = crc32c::crc32c_raw(crc, desc);
1644    (crc & 0xFFFF) as u16
1645}
1646
1647/// Inode checksum (32-bit, split across lo/hi in the inode).
1648fn inode_checksum(csum_seed: u32, inum: u32, generation: u32, inode_bytes: &[u8]) -> u32 {
1649    let mut crc = crc32c::crc32c_raw(csum_seed, &inum.to_le_bytes());
1650    crc = crc32c::crc32c_raw(crc, &generation.to_le_bytes());
1651    crc = crc32c::crc32c_raw(crc, &inode_bytes[..0x7C]);
1652    crc = crc32c::crc32c_raw(crc, &[0u8; 2]);
1653    crc = crc32c::crc32c_raw(crc, &inode_bytes[0x7E..0x82]);
1654    crc = crc32c::crc32c_raw(crc, &[0u8; 2]);
1655    crc = crc32c::crc32c_raw(crc, &inode_bytes[0x84..]);
1656    crc
1657}
1658
1659/// Bitmap checksum (block bitmap or inode bitmap). The checksum is computed
1660/// over the raw bitmap data and stored in the corresponding GDT fields. This
1661/// helper just computes the raw CRC; the caller reads the bitmap from disk.
1662///
1663/// For now we compute it over an in-memory representation of the bitmap. The
1664/// `_block_addr` and `_bitmap_size` arguments are unused but kept for future
1665/// reference.
1666fn bitmap_checksum(csum_seed: u32, bitmap: &[u8], checksum_len: usize) -> u32 {
1667    crc32c::crc32c_raw(csum_seed, &bitmap[..checksum_len])
1668}
1669
1670/// Directory block checksum.
1671fn dir_block_checksum(csum_seed: u32, inum: u32, generation: u32, data: &[u8]) -> u32 {
1672    let mut crc = crc32c::crc32c_raw(csum_seed, &inum.to_le_bytes());
1673    crc = crc32c::crc32c_raw(crc, &generation.to_le_bytes());
1674    crc = crc32c::crc32c_raw(crc, data);
1675    crc
1676}
1677
1678//--------------------------------------------------------------------------------------------------
1679// Functions: Byte helpers
1680//--------------------------------------------------------------------------------------------------
1681
1682fn put_le16(buf: &mut [u8], off: usize, val: u16) {
1683    buf[off..off + 2].copy_from_slice(&val.to_le_bytes());
1684}
1685
1686fn put_le32(buf: &mut [u8], off: usize, val: u32) {
1687    buf[off..off + 4].copy_from_slice(&val.to_le_bytes());
1688}
1689
1690fn put_be32(buf: &mut [u8], off: usize, val: u32) {
1691    buf[off..off + 4].copy_from_slice(&val.to_be_bytes());
1692}
1693
1694//--------------------------------------------------------------------------------------------------
1695// Re-Exports
1696//--------------------------------------------------------------------------------------------------
1697
1698pub use format::sparse_super_group;
1699
1700//--------------------------------------------------------------------------------------------------
1701// Tests
1702//--------------------------------------------------------------------------------------------------
1703
1704#[cfg(test)]
1705mod tests {
1706    use super::*;
1707
1708    #[test]
1709    fn test_format_creates_file_of_correct_size() {
1710        let dir = tempfile::tempdir().unwrap();
1711        let path = dir.path().join("test.ext4");
1712
1713        let size: u64 = 256 * 1024 * 1024; // 256 MiB
1714        let opts = Ext4FormatOptions {
1715            size_bytes: size,
1716            journal_blocks: 4096, // 16 MiB journal
1717        };
1718
1719        format_ext4(&path, &opts).unwrap();
1720
1721        let meta = std::fs::metadata(&path).unwrap();
1722        assert_eq!(meta.len(), size);
1723    }
1724
1725    #[test]
1726    fn test_format_too_small() {
1727        let dir = tempfile::tempdir().unwrap();
1728        let path = dir.path().join("tiny.ext4");
1729
1730        let opts = Ext4FormatOptions {
1731            size_bytes: 4096, // way too small
1732            journal_blocks: 16384,
1733        };
1734
1735        let result = format_ext4(&path, &opts);
1736        assert!(matches!(result, Err(Ext4Error::TooSmall)));
1737    }
1738
1739    #[test]
1740    fn test_format_default_options() {
1741        let dir = tempfile::tempdir().unwrap();
1742        let path = dir.path().join("default.ext4");
1743
1744        let opts = Ext4FormatOptions::default();
1745        format_ext4(&path, &opts).unwrap();
1746
1747        let meta = std::fs::metadata(&path).unwrap();
1748        assert_eq!(meta.len(), DEFAULT_SIZE_BYTES);
1749    }
1750
1751    #[test]
1752    fn test_superblock_magic() {
1753        let dir = tempfile::tempdir().unwrap();
1754        let path = dir.path().join("magic.ext4");
1755
1756        let opts = Ext4FormatOptions {
1757            size_bytes: 256 * 1024 * 1024,
1758            journal_blocks: 4096,
1759        };
1760        format_ext4(&path, &opts).unwrap();
1761
1762        // Read back and check magic number at offset 1024+0x38
1763        let data = std::fs::read(&path).unwrap();
1764        let magic = u16::from_le_bytes([data[1024 + 0x38], data[1024 + 0x39]]);
1765        assert_eq!(magic, EXT4_SUPER_MAGIC);
1766    }
1767
1768    #[test]
1769    fn test_journal_magic() {
1770        let dir = tempfile::tempdir().unwrap();
1771        let path = dir.path().join("journal.ext4");
1772
1773        let opts = Ext4FormatOptions {
1774            size_bytes: 256 * 1024 * 1024,
1775            journal_blocks: 4096,
1776        };
1777        format_ext4(&path, &opts).unwrap();
1778
1779        let layout = Layout::compute(&opts).unwrap();
1780        let data = std::fs::read(&path).unwrap();
1781
1782        // Journal superblock is at journal_start_block * 4096
1783        let jsb_offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
1784        let magic = u32::from_be_bytes([
1785            data[jsb_offset],
1786            data[jsb_offset + 1],
1787            data[jsb_offset + 2],
1788            data[jsb_offset + 3],
1789        ]);
1790        assert_eq!(magic, JBD2_MAGIC);
1791    }
1792
1793    #[test]
1794    fn test_root_dir_inode_exists() {
1795        let dir = tempfile::tempdir().unwrap();
1796        let path = dir.path().join("rootdir.ext4");
1797
1798        let opts = Ext4FormatOptions {
1799            size_bytes: 256 * 1024 * 1024,
1800            journal_blocks: 4096,
1801        };
1802        format_ext4(&path, &opts).unwrap();
1803
1804        let layout = Layout::compute(&opts).unwrap();
1805        let data = std::fs::read(&path).unwrap();
1806
1807        // Root inode at inode_table_block * 4096 + (2-1)*256
1808        let inode_offset = layout.inode_table_block as usize * EXT4_BLOCK_SIZE as usize
1809            + 1 * EXT4_INODE_SIZE as usize;
1810        let mode = u16::from_le_bytes([data[inode_offset], data[inode_offset + 1]]);
1811        assert_eq!(mode, S_IFDIR | 0o755);
1812    }
1813
1814    #[test]
1815    fn test_backup_group_bitmap_starts_after_backup_metadata() {
1816        let opts = Ext4FormatOptions {
1817            size_bytes: 256 * 1024 * 1024,
1818            journal_blocks: 4096,
1819        };
1820        let layout = Layout::compute(&opts).unwrap();
1821        let block_bitmaps = build_block_bitmaps(&layout);
1822        let inode_bitmaps = build_inode_bitmaps(&layout);
1823        let gdt = build_gdt(&layout, &block_bitmaps, &inode_bitmaps).unwrap();
1824
1825        let desc = &gdt[EXT4_DESC_SIZE as usize..(2 * EXT4_DESC_SIZE as usize)];
1826        let block_bitmap = u32::from_le_bytes([desc[0], desc[1], desc[2], desc[3]]);
1827        let group_start = layout.group_start_block(1);
1828
1829        assert_eq!(block_bitmap, layout.group_block_bitmap_block(1));
1830        assert!(block_bitmap > group_start + layout.gdt_blocks - 1);
1831    }
1832
1833    #[test]
1834    fn test_inode_bitmap_padding_is_marked_used() {
1835        let layout = Layout::compute(&Ext4FormatOptions {
1836            size_bytes: 256 * 1024 * 1024,
1837            journal_blocks: 4096,
1838        })
1839        .unwrap();
1840
1841        let bitmap = build_inode_bitmap(&layout, 0);
1842        for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
1843            assert_ne!(bitmap[(bit / 8) as usize] & (1 << (bit % 8)), 0);
1844        }
1845    }
1846
1847    #[test]
1848    fn test_journal_superblock_checksum_matches_contents() {
1849        let dir = tempfile::tempdir().unwrap();
1850        let path = dir.path().join("journal-csum.ext4");
1851        let opts = Ext4FormatOptions {
1852            size_bytes: 256 * 1024 * 1024,
1853            journal_blocks: 4096,
1854        };
1855
1856        format_ext4(&path, &opts).unwrap();
1857
1858        let layout = Layout::compute(&opts).unwrap();
1859        let data = std::fs::read(&path).unwrap();
1860        let offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
1861        let mut jsb = data[offset..offset + JBD2_SUPERBLOCK_SIZE].to_vec();
1862        let stored = u32::from_be_bytes([jsb[0xFC], jsb[0xFD], jsb[0xFE], jsb[0xFF]]);
1863
1864        jsb[0xFC..0x100].fill(0);
1865        let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb);
1866
1867        assert_eq!(stored, expected);
1868    }
1869
1870    #[test]
1871    fn test_root_dir_checksum_matches_contents() {
1872        let dir = tempfile::tempdir().unwrap();
1873        let path = dir.path().join("rootdir-csum.ext4");
1874        let opts = Ext4FormatOptions {
1875            size_bytes: 256 * 1024 * 1024,
1876            journal_blocks: 4096,
1877        };
1878
1879        format_ext4(&path, &opts).unwrap();
1880
1881        let layout = Layout::compute(&opts).unwrap();
1882        let data = std::fs::read(&path).unwrap();
1883        let sb = &data[1024..2048];
1884        let uuid = &sb[0x68..0x78];
1885        let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, uuid);
1886        let block_offset = layout.first_data_block as usize * EXT4_BLOCK_SIZE as usize;
1887        let tail_offset = block_offset + EXT4_BLOCK_SIZE as usize - 12;
1888        let stored = u32::from_le_bytes([
1889            data[tail_offset + 8],
1890            data[tail_offset + 9],
1891            data[tail_offset + 10],
1892            data[tail_offset + 11],
1893        ]);
1894        let expected = dir_block_checksum(
1895            csum_seed,
1896            EXT4_ROOT_INO,
1897            0,
1898            &data[block_offset..tail_offset],
1899        );
1900
1901        assert_eq!(stored, expected);
1902    }
1903}