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