Skip to main content

microsandbox_image/ext4/
formatter.rs

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