Skip to main content

microsandbox_image/ext4/
formatter.rs

1use std::collections::HashMap;
2use std::io::{self, BufWriter, SeekFrom, Write};
3#[cfg(windows)]
4use std::os::windows::io::AsRawHandle;
5use std::path::Path;
6#[cfg(windows)]
7use std::ptr;
8
9use super::format::{
10    EXT4_BLOCK_SIZE, EXT4_BLOCKS_PER_GROUP, EXT4_DESC_SIZE, EXT4_EH_MAGIC, EXT4_EXTENTS_FL,
11    EXT4_FEATURE_COMPAT_DIR_INDEX, EXT4_FEATURE_COMPAT_EXT_ATTR, EXT4_FEATURE_COMPAT_HAS_JOURNAL,
12    EXT4_FEATURE_COMPAT_RESIZE_INODE, EXT4_FEATURE_INCOMPAT_64BIT, EXT4_FEATURE_INCOMPAT_EXTENTS,
13    EXT4_FEATURE_INCOMPAT_FILETYPE, EXT4_FEATURE_RO_COMPAT_DIR_NLINK,
14    EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE, EXT4_FEATURE_RO_COMPAT_HUGE_FILE,
15    EXT4_FEATURE_RO_COMPAT_LARGE_FILE, EXT4_FEATURE_RO_COMPAT_METADATA_CSUM,
16    EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER, EXT4_FIRST_INO, EXT4_INODE_SIZE, EXT4_INODES_PER_GROUP,
17    EXT4_JOURNAL_INO, EXT4_LOG_BLOCK_SIZE, EXT4_MIN_EXTRA_ISIZE, EXT4_ROOT_INO,
18    EXT4_SB_ERROR_COUNT_OFFSET, EXT4_SB_OVERHEAD_BLOCKS_OFFSET, EXT4_SUPER_MAGIC, JBD2_MAGIC,
19    JBD2_SUPERBLOCK_V2, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFREG, S_IFSOCK,
20};
21use super::layout::{
22    GroupDescStats, GroupGeometry, MAX_BLOCKS, RESERVED_GDT_BLOCKS, bitmap_checksum,
23    build_block_bitmap_base, build_group_descriptor, build_inode_bitmap_base, count_used_bits,
24    dir_block_checksum, inode_checksum, put_be32, put_le16, put_le32, superblock_checksum,
25    write_backup_superblock_at, write_gdt_at,
26};
27use super::resize_inode::write_resize_inode;
28use crate::crc32c;
29use crate::path_bytes::os_str_bytes;
30use crate::tree::{
31    DirectoryNode, FileData, FileTree, InodeMetadata, RegularFileId, TreeNode, Xattr,
32};
33#[cfg(windows)]
34use windows_sys::Win32::Foundation::HANDLE;
35#[cfg(windows)]
36use windows_sys::Win32::System::IO::DeviceIoControl;
37#[cfg(windows)]
38use windows_sys::Win32::System::Ioctl::FSCTL_SET_SPARSE;
39
40//--------------------------------------------------------------------------------------------------
41// Constants
42//--------------------------------------------------------------------------------------------------
43
44/// Default image size: 4 GiB.
45const DEFAULT_SIZE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
46
47/// Default journal size in blocks (64 MiB at 4 KiB/block = 16384 blocks).
48const DEFAULT_JOURNAL_BLOCKS: u32 = 16384;
49
50/// Maximum initialized extent length that fits in one ext4 extent record.
51const MAX_INITIALIZED_EXTENT_BLOCKS: u32 = 32768;
52const INLINE_EXTENT_CAPACITY: usize = 4;
53const EXTENT_BLOCK_CAPACITY: usize =
54    (EXT4_BLOCK_SIZE as usize - 12 - std::mem::size_of::<u32>()) / 12;
55const EXT4_XATTR_MAGIC: u32 = 0xEA02_0000;
56const EXT4_XATTR_HEADER_SIZE: usize = 32;
57const EXT4_XATTR_ENTRY_SIZE: usize = 16;
58
59/// ext4 directory entry file type: directory.
60const EXT4_FT_DIR: u8 = 2;
61
62/// ext4 directory entry file type: regular file.
63#[allow(dead_code)]
64const EXT4_FT_REG_FILE: u8 = 1;
65
66/// ext4 directory entry file type: character device.
67const EXT4_FT_CHRDEV: u8 = 3;
68
69/// ext4 directory entry file type: block device.
70const EXT4_FT_BLKDEV: u8 = 4;
71
72/// ext4 directory entry file type: FIFO.
73const EXT4_FT_FIFO: u8 = 5;
74
75/// ext4 directory entry file type: socket.
76const EXT4_FT_SOCK: u8 = 6;
77
78/// ext4 directory entry file type: symbolic link.
79const EXT4_FT_SYMLINK: u8 = 7;
80
81/// jbd2 superblocks are always 1024 bytes, even on 4 KiB block filesystems.
82const JBD2_SUPERBLOCK_SIZE: usize = 1024;
83
84//--------------------------------------------------------------------------------------------------
85// Types
86//--------------------------------------------------------------------------------------------------
87
88/// Options for creating an ext4 filesystem image.
89pub struct Ext4FormatOptions {
90    /// Total image size in bytes. Must be large enough to hold metadata and
91    /// journal. Defaults to 4 GiB.
92    pub size_bytes: u64,
93
94    /// Number of 4 KiB blocks to allocate for the journal.
95    /// Defaults to 16384 (64 MiB).
96    pub journal_blocks: u32,
97}
98
99/// Errors that can occur during ext4 formatting.
100#[derive(Debug)]
101pub enum Ext4Error {
102    /// An I/O error occurred while writing the image.
103    Io(io::Error),
104
105    /// The requested image size is not representable by this formatter.
106    InvalidSize(String),
107
108    /// The requested image size is too small to hold the minimum metadata and
109    /// journal.
110    TooSmall,
111
112    /// The requested image size is larger than this formatter can encode.
113    TooLarge {
114        /// Requested 4 KiB block count.
115        requested_blocks: u64,
116
117        /// Maximum supported 4 KiB block count.
118        max_blocks: u64,
119    },
120
121    /// The requested tree cannot be serialized by this minimal formatter.
122    Layout(String),
123
124    /// The image is not one this crate's formatter produced (or is corrupted), so the offline
125    /// resizer refuses to touch it.
126    Unsupported(String),
127
128    /// Growing to the requested size needs more group descriptors than the image's GDT
129    /// (allocated + reserved blocks) can hold; the image must be recreated to grow further.
130    ExceedsGdtCapacity {
131        /// Requested image size in bytes.
132        requested_bytes: u64,
133
134        /// Largest size in bytes this image can grow to in place.
135        max_size_bytes: u64,
136    },
137}
138
139/// Internal layout computed from `Ext4FormatOptions`.
140struct Layout {
141    num_blocks: u64,
142    num_groups: u32,
143    uuid: [u8; 16],
144    gdt_blocks: u32,
145    /// Blocks reserved after the GDT for future offline growth.
146    reserved_gdt_blocks: u32,
147    /// Number of blocks occupied by the inode table in group 0.
148    inode_table_blocks: u32,
149    /// First data block after inode table (root dir block).
150    first_data_block: u64,
151    /// Double-indirect block owned by the reserved-GDT inode. Legacy test
152    /// images intentionally omit this block to reproduce the pre-0.6.9
153    /// formatter layout.
154    resize_inode_block: Option<u64>,
155    /// First block of the journal region.
156    journal_start_block: u64,
157    /// Total journal blocks.
158    journal_blocks: u32,
159    /// CRC32C checksum seed derived from the UUID.
160    csum_seed: u32,
161    /// Feature compat flags.
162    feature_compat: u32,
163    /// Feature incompat flags.
164    feature_incompat: u32,
165    /// Feature ro-compat flags.
166    feature_ro_compat: u32,
167}
168
169struct FsStats {
170    group_free_blocks: Vec<u32>,
171    group_free_inodes: Vec<u32>,
172    group_used_dirs: Vec<u32>,
173    block_bitmap_checksums: Vec<u32>,
174    inode_bitmap_checksums: Vec<u32>,
175    total_free_blocks: u64,
176    total_free_inodes: u64,
177    overhead_blocks: u64,
178}
179
180struct BitmapPlan {
181    block_extents: Vec<(u64, u32)>,
182    max_used_inode: u32,
183    group_used_dirs: Vec<u32>,
184}
185
186enum NodeKind {
187    Directory { data: Vec<u8> },
188    RegularFile { data: FileData },
189    Symlink { target: Vec<u8>, inline: bool },
190    CharDevice { major: u32, minor: u32 },
191    BlockDevice { major: u32, minor: u32 },
192    Fifo,
193    Socket,
194}
195
196struct NodePlan {
197    inode: u32,
198    path: String,
199    metadata: InodeMetadata,
200    links_count: u32,
201    kind: NodeKind,
202    block_count: u32,
203    data_extents: Vec<AllocatedExtent>,
204    extent_tree_block: Option<u64>,
205    xattrs: Vec<Xattr>,
206    inline_xattrs: Option<Vec<u8>>,
207    xattr_block: Option<u64>,
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq)]
211struct AllocatedExtent {
212    logical_block: u32,
213    physical_start: u64,
214    block_count: u32,
215}
216
217#[derive(Clone, Copy)]
218enum TreeEncodingMode {
219    Upper,
220    Rootfs,
221}
222
223struct DraftDirectory {
224    children: u16,
225    data: Vec<u8>,
226    xattrs: Vec<Xattr>,
227}
228
229struct DraftContext<'a> {
230    next_inode: &'a mut u32,
231    plans: &'a mut Vec<NodePlan>,
232    hardlinks: &'a mut HashMap<RegularFileId, (u32, usize)>,
233    encoding_mode: TreeEncodingMode,
234}
235
236struct EncodedXattr<'a> {
237    name_index: u8,
238    name: &'a [u8],
239    value: &'a [u8],
240}
241
242struct DirEntrySpec {
243    inode: u32,
244    file_type: u8,
245    name: Vec<u8>,
246}
247
248struct DataAllocator {
249    regions: Vec<(u64, u32)>,
250}
251
252//--------------------------------------------------------------------------------------------------
253// Methods
254//--------------------------------------------------------------------------------------------------
255
256impl Default for Ext4FormatOptions {
257    fn default() -> Self {
258        Self {
259            size_bytes: DEFAULT_SIZE_BYTES,
260            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
261        }
262    }
263}
264
265impl std::fmt::Display for Ext4Error {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        match self {
268            Ext4Error::Io(e) => write!(f, "ext4 I/O error: {e}"),
269            Ext4Error::InvalidSize(e) => write!(f, "invalid ext4 image size: {e}"),
270            Ext4Error::TooSmall => write!(f, "image size is too small for ext4 formatting"),
271            Ext4Error::TooLarge {
272                requested_blocks,
273                max_blocks,
274            } => write!(
275                f,
276                "image is too large for ext4 formatting: requested {requested_blocks} blocks, maximum is {max_blocks} blocks"
277            ),
278            Ext4Error::Layout(e) => write!(f, "ext4 layout error: {e}"),
279            Ext4Error::Unsupported(e) => write!(f, "unsupported ext4 image: {e}"),
280            Ext4Error::ExceedsGdtCapacity {
281                requested_bytes,
282                max_size_bytes,
283            } => write!(
284                f,
285                "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"
286            ),
287        }
288    }
289}
290
291impl std::error::Error for Ext4Error {
292    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
293        match self {
294            Ext4Error::Io(e) => Some(e),
295            Ext4Error::InvalidSize(_)
296            | Ext4Error::TooSmall
297            | Ext4Error::TooLarge { .. }
298            | Ext4Error::Layout(_)
299            | Ext4Error::Unsupported(_)
300            | Ext4Error::ExceedsGdtCapacity { .. } => None,
301        }
302    }
303}
304
305impl From<io::Error> for Ext4Error {
306    fn from(e: io::Error) -> Self {
307        Ext4Error::Io(e)
308    }
309}
310
311impl Layout {
312    #[cfg(test)]
313    fn compute(opts: &Ext4FormatOptions) -> Result<Self, Ext4Error> {
314        Self::compute_with_root_blocks(opts, 1, RESERVED_GDT_BLOCKS)
315    }
316
317    #[cfg(test)]
318    fn compute_with_root_blocks(
319        opts: &Ext4FormatOptions,
320        root_dir_blocks: u32,
321        reserved_gdt_blocks: u32,
322    ) -> Result<Self, Ext4Error> {
323        Self::compute_with_root_blocks_and_uuid(
324            opts,
325            root_dir_blocks,
326            reserved_gdt_blocks,
327            None,
328            true,
329        )
330    }
331
332    fn compute_with_root_blocks_and_uuid(
333        opts: &Ext4FormatOptions,
334        root_dir_blocks: u32,
335        reserved_gdt_blocks: u32,
336        uuid: Option<[u8; 16]>,
337        with_resize_inode: bool,
338    ) -> Result<Self, Ext4Error> {
339        let block_size = EXT4_BLOCK_SIZE as u64;
340        if !opts.size_bytes.is_multiple_of(block_size) {
341            return Err(Ext4Error::InvalidSize(format!(
342                "image size must be aligned to {block_size} bytes"
343            )));
344        }
345
346        let num_blocks = opts.size_bytes / block_size;
347        if num_blocks > MAX_BLOCKS {
348            return Err(Ext4Error::TooLarge {
349                requested_blocks: num_blocks,
350                max_blocks: MAX_BLOCKS,
351            });
352        }
353
354        if opts.journal_blocks == 0 {
355            return Err(Ext4Error::InvalidSize(
356                "journal must contain at least one block".to_string(),
357            ));
358        }
359        validate_extent_block_count(opts.journal_blocks, "journal")?;
360
361        let num_groups_raw = num_blocks.div_ceil(EXT4_BLOCKS_PER_GROUP as u64);
362        let num_groups = u32::try_from(num_groups_raw).map_err(|_| Ext4Error::TooLarge {
363            requested_blocks: num_blocks,
364            max_blocks: MAX_BLOCKS,
365        })?;
366
367        // We need at least: superblock(1) + GDT + reserved GDT headroom (RESERVED_GDT_BLOCKS) +
368        // bitmaps(2) + inode_table + root_dir(1) + journal
369        let inode_table_blocks =
370            (EXT4_INODES_PER_GROUP as u64 * EXT4_INODE_SIZE as u64 / block_size) as u32;
371        let gdt_blocks = (num_groups as u64 * EXT4_DESC_SIZE as u64).div_ceil(block_size) as u32;
372
373        // Group 0 layout:
374        //   block 0: superblock (bytes 0-4095, sb at offset 1024)
375        //   next block(s): GDT
376        //   next reserved blocks: reserved GDT
377        //   next block: block bitmap
378        //   next block: inode bitmap
379        //   next N blocks: inode table
380        //   next block: resize-inode double-indirect block
381        //   next block: root dir data block
382        //   next M blocks: journal
383
384        let overhead_blocks = 1u64 + gdt_blocks as u64 + reserved_gdt_blocks as u64;
385        let block_bitmap_block = overhead_blocks;
386        let inode_bitmap_block = block_bitmap_block + 1;
387        let inode_table_block = inode_bitmap_block + 1;
388        let inode_table_end = inode_table_block + inode_table_blocks as u64;
389        let resize_inode_block = with_resize_inode.then_some(inode_table_end);
390        let first_data_block = inode_table_end + u64::from(with_resize_inode);
391        let journal_start_block = first_data_block + root_dir_blocks as u64;
392
393        let min_blocks = journal_start_block + opts.journal_blocks as u64 + 1; // +1 slack
394        if num_blocks < min_blocks {
395            return Err(Ext4Error::TooSmall);
396        }
397
398        // Ephemeral uppers retain random identity while shared rootfs artifacts provide a stable
399        // UUID derived from their complete materialization inputs.
400        let uuid = uuid.unwrap_or_else(Self::generate_uuid);
401
402        let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, &uuid);
403
404        let mut feature_compat = EXT4_FEATURE_COMPAT_HAS_JOURNAL
405            | EXT4_FEATURE_COMPAT_EXT_ATTR
406            | EXT4_FEATURE_COMPAT_DIR_INDEX;
407        if with_resize_inode {
408            feature_compat |= EXT4_FEATURE_COMPAT_RESIZE_INODE;
409        }
410
411        let feature_incompat = EXT4_FEATURE_INCOMPAT_FILETYPE
412            | EXT4_FEATURE_INCOMPAT_EXTENTS
413            | EXT4_FEATURE_INCOMPAT_64BIT;
414
415        let feature_ro_compat = EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER
416            | EXT4_FEATURE_RO_COMPAT_LARGE_FILE
417            | EXT4_FEATURE_RO_COMPAT_HUGE_FILE
418            | EXT4_FEATURE_RO_COMPAT_DIR_NLINK
419            | EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE
420            | EXT4_FEATURE_RO_COMPAT_METADATA_CSUM;
421
422        let layout = Layout {
423            num_blocks,
424            num_groups,
425            uuid,
426            gdt_blocks,
427            reserved_gdt_blocks,
428            inode_table_blocks,
429            first_data_block,
430            resize_inode_block,
431            journal_start_block,
432            journal_blocks: opts.journal_blocks,
433            csum_seed,
434            feature_compat,
435            feature_incompat,
436            feature_ro_compat,
437        };
438        layout.validate_group_metadata()?;
439
440        Ok(layout)
441    }
442
443    fn generate_uuid() -> [u8; 16] {
444        // Simple random UUID using /dev/urandom or fallback to timestamp-based
445        let mut uuid = [0u8; 16];
446        if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
447            use std::io::Read;
448            let _ = f.read_exact(&mut uuid);
449        } else {
450            // Fallback: use system time as entropy source
451            let now = std::time::SystemTime::now()
452                .duration_since(std::time::UNIX_EPOCH)
453                .unwrap_or_default();
454            let nanos = now.as_nanos();
455            uuid[..8].copy_from_slice(&(nanos as u64).to_le_bytes());
456            uuid[8..16].copy_from_slice(&((nanos >> 64) as u64).to_le_bytes());
457        }
458        // Set UUID version 4 and variant bits
459        uuid[6] = (uuid[6] & 0x0F) | 0x40;
460        uuid[7] = (uuid[7] & 0x3F) | 0x80;
461        uuid
462    }
463
464    /// Group-relative layout math shared with the resizer.
465    fn geometry(&self) -> GroupGeometry {
466        GroupGeometry {
467            num_blocks: self.num_blocks,
468            gdt_blocks: self.gdt_blocks,
469            reserved_gdt_blocks: self.reserved_gdt_blocks,
470            inode_table_blocks: self.inode_table_blocks,
471        }
472    }
473
474    fn group_start_block(&self, group: u32) -> u64 {
475        self.geometry().group_start_block(group)
476    }
477
478    fn blocks_in_group(&self, group: u32) -> u32 {
479        self.geometry().blocks_in_group(group)
480    }
481
482    fn group_block_bitmap_block(&self, group: u32) -> u64 {
483        self.geometry().group_block_bitmap_block(group)
484    }
485
486    fn group_inode_bitmap_block(&self, group: u32) -> u64 {
487        self.geometry().group_inode_bitmap_block(group)
488    }
489
490    fn group_inode_table_block(&self, group: u32) -> u64 {
491        self.geometry().group_inode_table_block(group)
492    }
493
494    fn group_data_start_block(&self, group: u32) -> u64 {
495        let mut start = self.group_start_block(group) + self.group_metadata_blocks(group) as u64;
496        if group == 0 {
497            start = self.journal_start_block + self.journal_blocks as u64;
498        }
499        start
500    }
501
502    fn group_metadata_blocks(&self, group: u32) -> u32 {
503        self.geometry().group_metadata_blocks(group)
504    }
505
506    fn group_used_blocks(&self, group: u32) -> u32 {
507        let mut used = self.group_metadata_blocks(group);
508        if group == 0 {
509            // Root directory + journal, plus the reserved-GDT inode's pointer block in the
510            // modern layout. Pre-0.6.9 images did not allocate that final block.
511            used += 1 + self.journal_blocks + u32::from(self.resize_inode_block.is_some());
512        }
513        used.min(self.blocks_in_group(group))
514    }
515
516    fn group_free_blocks(&self, group: u32) -> u32 {
517        self.blocks_in_group(group)
518            .saturating_sub(self.group_used_blocks(group))
519    }
520
521    fn group_free_inodes(&self, group: u32) -> u32 {
522        if group == 0 {
523            EXT4_INODES_PER_GROUP - (EXT4_FIRST_INO - 1)
524        } else {
525            EXT4_INODES_PER_GROUP
526        }
527    }
528
529    #[cfg(test)]
530    fn group_used_dirs(&self, group: u32) -> u32 {
531        if group == 0 { 1 } else { 0 }
532    }
533
534    fn total_free_blocks(&self) -> u64 {
535        (0..self.num_groups)
536            .map(|group| self.group_free_blocks(group) as u64)
537            .sum()
538    }
539
540    fn total_free_inodes(&self) -> u64 {
541        (0..self.num_groups)
542            .map(|group| self.group_free_inodes(group) as u64)
543            .sum()
544    }
545
546    fn total_overhead_blocks(&self) -> u64 {
547        (0..self.num_groups)
548            .map(|group| u64::from(self.group_metadata_blocks(group)))
549            .sum()
550    }
551
552    fn recorded_overhead_blocks(&self, total_free_blocks: u64) -> (usize, u64) {
553        if self.resize_inode_block.is_none() {
554            // The released pre-0.6.9 formatter wrote total used blocks at 0x194, before that
555            // offset was corrected to ext4's actual error counter and overhead moved to 0x248.
556            (
557                EXT4_SB_ERROR_COUNT_OFFSET,
558                self.num_blocks - total_free_blocks,
559            )
560        } else {
561            (EXT4_SB_OVERHEAD_BLOCKS_OFFSET, self.total_overhead_blocks())
562        }
563    }
564
565    fn validate_group_metadata(&self) -> Result<(), Ext4Error> {
566        for group in 0..self.num_groups {
567            let blocks_in_group = self.blocks_in_group(group);
568            let metadata_blocks = self.group_metadata_blocks(group);
569            if blocks_in_group < metadata_blocks {
570                return Err(Ext4Error::InvalidSize(format!(
571                    "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"
572                )));
573            }
574        }
575
576        Ok(())
577    }
578}
579
580impl BitmapPlan {
581    fn new(layout: &Layout, plans: &[NodePlan]) -> Self {
582        let mut block_extents = Vec::new();
583        if let Some(block) = layout.resize_inode_block {
584            block_extents.push((block, 1));
585        }
586        block_extents.push((
587            layout.first_data_block,
588            (layout.journal_start_block - layout.first_data_block) as u32,
589        ));
590        block_extents.push((layout.journal_start_block, layout.journal_blocks));
591
592        for plan in plans {
593            for extent in &plan.data_extents {
594                block_extents.push((extent.physical_start, extent.block_count));
595            }
596            if let Some(block) = plan.extent_tree_block {
597                block_extents.push((block, 1));
598            }
599            if let Some(block) = plan.xattr_block {
600                block_extents.push((block, 1));
601            }
602        }
603
604        let max_used_inode = plans
605            .iter()
606            .map(|plan| plan.inode)
607            .max()
608            .unwrap_or(EXT4_JOURNAL_INO)
609            .max(EXT4_FIRST_INO - 1);
610        let mut group_used_dirs = vec![0u32; layout.num_groups as usize];
611        for plan in plans
612            .iter()
613            .filter(|plan| matches!(plan.kind, NodeKind::Directory { .. }))
614        {
615            let group = (plan.inode - 1) / EXT4_INODES_PER_GROUP;
616            group_used_dirs[group as usize] += 1;
617        }
618
619        Self {
620            block_extents,
621            max_used_inode,
622            group_used_dirs,
623        }
624    }
625}
626
627//--------------------------------------------------------------------------------------------------
628// Functions
629//--------------------------------------------------------------------------------------------------
630
631/// Create and format a sparse ext4 filesystem image at `path`.
632///
633/// The image is suitable for use as an overlayfs upper layer. It is created as
634/// a sparse file so the initial on-disk footprint is minimal despite the large
635/// logical size.
636pub fn format_ext4(path: &Path, options: &Ext4FormatOptions) -> Result<(), Ext4Error> {
637    let tree = FileTree::new();
638    format_ext4_with_tree(path, options, tree)
639}
640
641pub fn format_ext4_with_tree(
642    path: &Path,
643    options: &Ext4FormatOptions,
644    tree: FileTree,
645) -> Result<(), Ext4Error> {
646    format_ext4_with_tree_and_reserved(path, options, tree, RESERVED_GDT_BLOCKS)
647}
648
649/// Test hook: format with a custom reserved-GDT-block count so the resizer's over-capacity
650/// handling of pre-headroom images (formatted before growth headroom existed) stays covered.
651#[cfg(test)]
652pub(super) fn format_ext4_for_test_with_reserved_gdt(
653    path: &Path,
654    options: &Ext4FormatOptions,
655    reserved_gdt_blocks: u32,
656) -> Result<(), Ext4Error> {
657    format_ext4_with_tree_and_reserved(path, options, FileTree::new(), reserved_gdt_blocks)
658}
659
660fn format_ext4_with_tree_and_reserved(
661    path: &Path,
662    options: &Ext4FormatOptions,
663    tree: FileTree,
664    reserved_gdt_blocks: u32,
665) -> Result<(), Ext4Error> {
666    format_ext4_with_tree_and_reserved_uuid(
667        path,
668        options,
669        tree,
670        reserved_gdt_blocks,
671        None,
672        TreeEncodingMode::Upper,
673        true,
674    )
675}
676
677pub(super) fn format_ext4_rootfs_with_tree_and_uuid(
678    path: &Path,
679    options: &Ext4FormatOptions,
680    tree: FileTree,
681    uuid: [u8; 16],
682) -> Result<(), Ext4Error> {
683    format_ext4_with_tree_and_reserved_uuid(
684        path,
685        options,
686        tree,
687        RESERVED_GDT_BLOCKS,
688        Some(uuid),
689        TreeEncodingMode::Rootfs,
690        true,
691    )
692}
693
694/// Reproduce the resize-metadata shape written before v0.6.9.
695///
696/// This is deliberately test-only: production formatting must always create
697/// the resize inode and its ownership graph.
698#[cfg(test)]
699pub(super) fn format_ext4_legacy_for_test(
700    path: &Path,
701    options: &Ext4FormatOptions,
702) -> Result<(), Ext4Error> {
703    format_ext4_with_tree_and_reserved_uuid(
704        path,
705        options,
706        FileTree::new(),
707        RESERVED_GDT_BLOCKS,
708        None,
709        TreeEncodingMode::Upper,
710        false,
711    )
712}
713
714fn format_ext4_with_tree_and_reserved_uuid(
715    path: &Path,
716    options: &Ext4FormatOptions,
717    tree: FileTree,
718    reserved_gdt_blocks: u32,
719    uuid: Option<[u8; 16]>,
720    encoding_mode: TreeEncodingMode,
721    with_resize_inode: bool,
722) -> Result<(), Ext4Error> {
723    let mut next_inode = EXT4_FIRST_INO;
724    let mut plans = Vec::new();
725    let root_metadata = planned_metadata(&tree.root.metadata, true, encoding_mode);
726    let mut hardlinks = HashMap::new();
727    let root_draft = draft_directory(
728        "/",
729        tree.root,
730        EXT4_ROOT_INO,
731        EXT4_ROOT_INO,
732        &mut DraftContext {
733            next_inode: &mut next_inode,
734            plans: &mut plans,
735            hardlinks: &mut hardlinks,
736            encoding_mode,
737        },
738    )?;
739    let root_dir_blocks = blocks_for_len(root_draft.data.len());
740    let layout = Layout::compute_with_root_blocks_and_uuid(
741        options,
742        root_dir_blocks.max(1),
743        reserved_gdt_blocks,
744        uuid,
745        with_resize_inode,
746    )?;
747    let max_inode = next_inode.saturating_sub(1);
748    let available_inodes = layout.num_groups.saturating_mul(EXT4_INODES_PER_GROUP);
749    if max_inode > available_inodes {
750        return Err(Ext4Error::Layout(format!(
751            "rootfs needs inode {max_inode}, but the filesystem geometry provides only {available_inodes} inodes"
752        )));
753    }
754    let mut allocator = DataAllocator::new(&layout);
755    let root_data_extents = split_physical_range(layout.first_data_block, root_dir_blocks.max(1));
756    let root_extent_tree_block = if root_data_extents.len() > INLINE_EXTENT_CAPACITY {
757        Some(allocator.allocate_block("/")?)
758    } else {
759        None
760    };
761    let root_inline_xattrs = build_inline_xattrs(&root_draft.xattrs, "/")?;
762    let root_xattr_block = if root_draft.xattrs.is_empty() || root_inline_xattrs.is_some() {
763        None
764    } else {
765        Some(allocator.allocate_block("/")?)
766    };
767
768    for plan in &mut plans {
769        allocate_node_data(&mut allocator, plan)?;
770    }
771
772    let mut all_plans = Vec::with_capacity(plans.len() + 1);
773    all_plans.push(NodePlan {
774        inode: EXT4_ROOT_INO,
775        path: "/".to_string(),
776        metadata: root_metadata,
777        links_count: 2 + u32::from(root_draft.children),
778        kind: NodeKind::Directory {
779            data: root_draft.data,
780        },
781        block_count: root_dir_blocks.max(1),
782        data_extents: root_data_extents,
783        extent_tree_block: root_extent_tree_block,
784        xattrs: root_draft.xattrs,
785        inline_xattrs: root_inline_xattrs,
786        xattr_block: root_xattr_block,
787    });
788    all_plans.extend(plans);
789    all_plans.sort_by_key(|plan| plan.inode);
790    validate_node_extents(&all_plans)?;
791
792    let bitmap_plan = BitmapPlan::new(&layout, &all_plans);
793    let stats = compute_fs_stats(&layout, &bitmap_plan);
794
795    let raw_file = std::fs::File::create(path)?;
796    mark_sparse(&raw_file)?;
797    raw_file.set_len(options.size_bytes)?;
798    let mut file = BufWriter::new(raw_file);
799
800    write_bitmaps(&mut file, &layout, &bitmap_plan)?;
801    write_tree_data(&mut file, &layout, &all_plans)?;
802    write_inode_table_with_plan(&mut file, &layout, &all_plans)?;
803    if let Some(resize_inode_block) = layout.resize_inode_block {
804        write_resize_inode(
805            &mut file,
806            &layout.geometry(),
807            layout.group_inode_table_block(0),
808            resize_inode_block,
809            layout.csum_seed,
810            layout.num_groups,
811        )?;
812    }
813    write_journal(&mut file, &layout)?;
814
815    let sb_bytes = build_superblock_with_stats(&layout, &stats)?;
816    write_primary_superblock_at(&mut file, &sb_bytes)?;
817
818    let gdt_bytes = build_gdt_with_stats(&layout, &stats)?;
819    write_gdt_at(&mut file, 0, &gdt_bytes)?;
820
821    for g in 1..layout.num_groups {
822        if sparse_super_group(g) {
823            let backup_sb_bytes = build_backup_superblock_with_stats(&layout, &stats, g)?;
824            write_backup_superblock_at(&mut file, layout.group_start_block(g), &backup_sb_bytes)?;
825            write_gdt_at(&mut file, layout.group_start_block(g), &gdt_bytes)?;
826        }
827    }
828
829    file.flush()?;
830    // No sync_all() — the image is read from page cache by the VM on the
831    // same host. Fsync would add 1-10ms for no benefit.
832
833    Ok(())
834}
835
836#[cfg(not(windows))]
837pub(super) fn mark_sparse(_file: &std::fs::File) -> Result<(), Ext4Error> {
838    Ok(())
839}
840
841#[cfg(windows)]
842pub(super) fn mark_sparse(file: &std::fs::File) -> Result<(), Ext4Error> {
843    let mut bytes_returned = 0;
844    let ok = unsafe {
845        DeviceIoControl(
846            file.as_raw_handle() as HANDLE,
847            FSCTL_SET_SPARSE,
848            ptr::null(),
849            0,
850            ptr::null_mut(),
851            0,
852            &mut bytes_returned,
853            ptr::null_mut(),
854        )
855    };
856    if ok == 0 {
857        return Err(Ext4Error::Io(io::Error::last_os_error()));
858    }
859
860    Ok(())
861}
862
863fn draft_directory(
864    path: &str,
865    dir: DirectoryNode,
866    inode: u32,
867    parent_inode: u32,
868    context: &mut DraftContext<'_>,
869) -> Result<DraftDirectory, Ext4Error> {
870    let DirectoryNode {
871        entries, xattrs, ..
872    } = dir;
873
874    let mut children = Vec::new();
875    let mut child_dir_count = 0u16;
876
877    for (name, node) in entries {
878        let name_bytes = os_str_bytes(name.as_os_str()).to_vec();
879        let child_path = child_path(path, &name_bytes);
880
881        // A hardlink alias adds another directory entry to the existing inode. It must not consume
882        // a new inode or duplicate the file's data blocks.
883        if let TreeNode::RegularFile(file) = &node
884            && let Some((inode, plan_index)) = context.hardlinks.get(&file.id).copied()
885        {
886            let plan = &mut context.plans[plan_index];
887            if !hardlink_matches_plan(plan, file, context.encoding_mode) {
888                return Err(Ext4Error::Layout(format!(
889                    "hardlink alias '{child_path}' conflicts with the metadata or content of inode {inode}"
890                )));
891            }
892            plan.links_count = plan.links_count.checked_add(1).ok_or_else(|| {
893                Ext4Error::Layout(format!("too many hardlinks for '{child_path}'"))
894            })?;
895            children.push(DirEntrySpec {
896                inode,
897                file_type: EXT4_FT_REG_FILE,
898                name: name_bytes,
899            });
900            continue;
901        }
902
903        let child_inode = *context.next_inode;
904        *context.next_inode = child_inode.checked_add(1).ok_or_else(|| {
905            Ext4Error::Layout("rootfs inode numbering overflowed u32".to_string())
906        })?;
907
908        match node {
909            TreeNode::Directory(child_dir) => {
910                child_dir_count = child_dir_count.saturating_add(1);
911                let metadata = planned_metadata(&child_dir.metadata, true, context.encoding_mode);
912                let child_draft =
913                    draft_directory(&child_path, child_dir, child_inode, inode, context)?;
914                let block_count = blocks_for_len(child_draft.data.len());
915                context.plans.push(NodePlan {
916                    inode: child_inode,
917                    path: child_path.clone(),
918                    metadata,
919                    links_count: 2 + u32::from(child_draft.children),
920                    kind: NodeKind::Directory {
921                        data: child_draft.data,
922                    },
923                    block_count,
924                    data_extents: Vec::new(),
925                    extent_tree_block: None,
926                    xattrs: child_draft.xattrs,
927                    inline_xattrs: None,
928                    xattr_block: None,
929                });
930                children.push(DirEntrySpec {
931                    inode: child_inode,
932                    file_type: EXT4_FT_DIR,
933                    name: name_bytes,
934                });
935            }
936            TreeNode::RegularFile(file) => {
937                let plan_index = context.plans.len();
938                let file_id = file.id;
939                context.plans.push(NodePlan {
940                    inode: child_inode,
941                    path: child_path.clone(),
942                    metadata: planned_metadata(&file.metadata, false, context.encoding_mode),
943                    links_count: 1,
944                    block_count: blocks_for_len(file.data.len()),
945                    kind: NodeKind::RegularFile { data: file.data },
946                    data_extents: Vec::new(),
947                    extent_tree_block: None,
948                    xattrs: file.xattrs,
949                    inline_xattrs: None,
950                    xattr_block: None,
951                });
952                context.hardlinks.insert(file_id, (child_inode, plan_index));
953                children.push(DirEntrySpec {
954                    inode: child_inode,
955                    file_type: EXT4_FT_REG_FILE,
956                    name: name_bytes,
957                });
958            }
959            TreeNode::Symlink(symlink) => {
960                let target_len = symlink.target.len();
961                let inline = target_len <= 59;
962                let block_count = if inline {
963                    0
964                } else {
965                    blocks_for_len(target_len)
966                };
967                context.plans.push(NodePlan {
968                    inode: child_inode,
969                    path: child_path.clone(),
970                    metadata: planned_metadata(&symlink.metadata, false, context.encoding_mode),
971                    links_count: 1,
972                    kind: NodeKind::Symlink {
973                        target: symlink.target,
974                        inline,
975                    },
976                    block_count,
977                    data_extents: Vec::new(),
978                    extent_tree_block: None,
979                    xattrs: Vec::new(),
980                    inline_xattrs: None,
981                    xattr_block: None,
982                });
983                children.push(DirEntrySpec {
984                    inode: child_inode,
985                    file_type: EXT4_FT_SYMLINK,
986                    name: name_bytes,
987                });
988            }
989            TreeNode::CharDevice(device) => {
990                context.plans.push(NodePlan {
991                    inode: child_inode,
992                    path: child_path.clone(),
993                    metadata: planned_metadata(&device.metadata, false, context.encoding_mode),
994                    links_count: 1,
995                    kind: NodeKind::CharDevice {
996                        major: device.major,
997                        minor: device.minor,
998                    },
999                    block_count: 0,
1000                    data_extents: Vec::new(),
1001                    extent_tree_block: None,
1002                    xattrs: Vec::new(),
1003                    inline_xattrs: None,
1004                    xattr_block: None,
1005                });
1006                children.push(DirEntrySpec {
1007                    inode: child_inode,
1008                    file_type: EXT4_FT_CHRDEV,
1009                    name: name_bytes,
1010                });
1011            }
1012            TreeNode::BlockDevice(device) => {
1013                context.plans.push(NodePlan {
1014                    inode: child_inode,
1015                    path: child_path.clone(),
1016                    metadata: planned_metadata(&device.metadata, false, context.encoding_mode),
1017                    links_count: 1,
1018                    kind: NodeKind::BlockDevice {
1019                        major: device.major,
1020                        minor: device.minor,
1021                    },
1022                    block_count: 0,
1023                    data_extents: Vec::new(),
1024                    extent_tree_block: None,
1025                    xattrs: Vec::new(),
1026                    inline_xattrs: None,
1027                    xattr_block: None,
1028                });
1029                children.push(DirEntrySpec {
1030                    inode: child_inode,
1031                    file_type: EXT4_FT_BLKDEV,
1032                    name: name_bytes,
1033                });
1034            }
1035            TreeNode::Fifo(metadata) => {
1036                context.plans.push(NodePlan {
1037                    inode: child_inode,
1038                    path: child_path.clone(),
1039                    metadata: planned_metadata(&metadata, false, context.encoding_mode),
1040                    links_count: 1,
1041                    kind: NodeKind::Fifo,
1042                    block_count: 0,
1043                    data_extents: Vec::new(),
1044                    extent_tree_block: None,
1045                    xattrs: Vec::new(),
1046                    inline_xattrs: None,
1047                    xattr_block: None,
1048                });
1049                children.push(DirEntrySpec {
1050                    inode: child_inode,
1051                    file_type: EXT4_FT_FIFO,
1052                    name: name_bytes,
1053                });
1054            }
1055            TreeNode::Socket(metadata) => {
1056                context.plans.push(NodePlan {
1057                    inode: child_inode,
1058                    path: child_path.clone(),
1059                    metadata: planned_metadata(&metadata, false, context.encoding_mode),
1060                    links_count: 1,
1061                    kind: NodeKind::Socket,
1062                    block_count: 0,
1063                    data_extents: Vec::new(),
1064                    extent_tree_block: None,
1065                    xattrs: Vec::new(),
1066                    inline_xattrs: None,
1067                    xattr_block: None,
1068                });
1069                children.push(DirEntrySpec {
1070                    inode: child_inode,
1071                    file_type: EXT4_FT_SOCK,
1072                    name: name_bytes,
1073                });
1074            }
1075        }
1076    }
1077
1078    let data = build_directory_data(inode, parent_inode, &children, path)?;
1079    Ok(DraftDirectory {
1080        children: child_dir_count,
1081        data,
1082        xattrs,
1083    })
1084}
1085
1086fn planned_metadata(
1087    metadata: &InodeMetadata,
1088    directory: bool,
1089    encoding_mode: TreeEncodingMode,
1090) -> InodeMetadata {
1091    match encoding_mode {
1092        // Ownership, mtime and directory modes come from the tree:
1093        // patch-created entries carry root:root, 0755 and mtime 0, while
1094        // directories copied up from a lower layer keep the lower directory's
1095        // values, including an explicit 0000 mode.
1096        TreeEncodingMode::Upper => InodeMetadata {
1097            uid: metadata.uid,
1098            gid: metadata.gid,
1099            mode: if directory {
1100                metadata.mode & 0o7777
1101            } else {
1102                normalize_file_permissions(metadata.mode)
1103            },
1104            mtime: metadata.mtime,
1105            mtime_nsec: metadata.mtime_nsec,
1106        },
1107        TreeEncodingMode::Rootfs => InodeMetadata {
1108            uid: metadata.uid,
1109            gid: metadata.gid,
1110            mode: metadata.mode & 0o7777,
1111            mtime: metadata.mtime,
1112            mtime_nsec: metadata.mtime_nsec,
1113        },
1114    }
1115}
1116
1117fn child_path(parent: &str, name: &[u8]) -> String {
1118    let name = String::from_utf8_lossy(name);
1119    if parent == "/" {
1120        format!("/{name}")
1121    } else {
1122        format!("{parent}/{name}")
1123    }
1124}
1125
1126fn normalize_file_permissions(mode: u16) -> u16 {
1127    let perms = mode & 0o7777;
1128    if perms == 0 { 0o644 } else { perms }
1129}
1130
1131fn hardlink_matches_plan(
1132    plan: &NodePlan,
1133    file: &crate::tree::RegularFileNode,
1134    encoding_mode: TreeEncodingMode,
1135) -> bool {
1136    let metadata = &plan.metadata;
1137    let candidate = planned_metadata(&file.metadata, false, encoding_mode);
1138    let metadata_matches = metadata.uid == candidate.uid
1139        && metadata.gid == candidate.gid
1140        && metadata.mode == candidate.mode
1141        && metadata.mtime == candidate.mtime
1142        && metadata.mtime_nsec == candidate.mtime_nsec;
1143    let xattrs_match = plan.xattrs.len() == file.xattrs.len()
1144        && plan.xattrs.iter().all(|left| {
1145            file.xattrs
1146                .iter()
1147                .any(|right| left.name == right.name && left.value == right.value)
1148        });
1149    let data_matches = match (&plan.kind, &file.data) {
1150        (NodeKind::RegularFile { data: left }, right) => same_file_data(left, right),
1151        _ => false,
1152    };
1153    metadata_matches && xattrs_match && data_matches
1154}
1155
1156fn same_file_data(left: &FileData, right: &FileData) -> bool {
1157    match (left, right) {
1158        (FileData::Memory(left), FileData::Memory(right)) => left == right,
1159        (FileData::SharedMemory(left), FileData::SharedMemory(right)) => {
1160            std::sync::Arc::ptr_eq(left, right) || left.as_ref() == right.as_ref()
1161        }
1162        (FileData::Memory(left), FileData::SharedMemory(right))
1163        | (FileData::SharedMemory(right), FileData::Memory(left)) => {
1164            left.as_slice() == right.as_ref()
1165        }
1166        (
1167            FileData::Spool {
1168                spool: left_spool,
1169                offset: left_offset,
1170                len: left_len,
1171            },
1172            FileData::Spool {
1173                spool: right_spool,
1174                offset: right_offset,
1175                len: right_len,
1176            },
1177        ) => {
1178            std::sync::Arc::ptr_eq(left_spool, right_spool)
1179                && left_offset == right_offset
1180                && left_len == right_len
1181        }
1182        _ => false,
1183    }
1184}
1185
1186fn blocks_for_len(len: usize) -> u32 {
1187    if len == 0 {
1188        0
1189    } else {
1190        (len as u64).div_ceil(EXT4_BLOCK_SIZE as u64) as u32
1191    }
1192}
1193
1194fn validate_node_extents(plans: &[NodePlan]) -> Result<(), Ext4Error> {
1195    for plan in plans {
1196        if plan.data_extents.len() > EXTENT_BLOCK_CAPACITY {
1197            return Err(Ext4Error::Layout(format!(
1198                "'{}' needs {} extents, but a depth-one ext4 extent tree holds at most {EXTENT_BLOCK_CAPACITY}",
1199                plan.path,
1200                plan.data_extents.len()
1201            )));
1202        }
1203        for extent in &plan.data_extents {
1204            validate_extent_block_count(extent.block_count, &plan.path)?;
1205        }
1206    }
1207
1208    Ok(())
1209}
1210
1211fn validate_extent_block_count(block_count: u32, label: &str) -> Result<(), Ext4Error> {
1212    if block_count > MAX_INITIALIZED_EXTENT_BLOCKS {
1213        return Err(Ext4Error::Layout(format!(
1214            "'{label}' needs {block_count} blocks but this formatter currently supports one initialized extent of at most {MAX_INITIALIZED_EXTENT_BLOCKS} blocks"
1215        )));
1216    }
1217
1218    Ok(())
1219}
1220
1221fn split_physical_range(start: u64, blocks: u32) -> Vec<AllocatedExtent> {
1222    let mut extents = Vec::new();
1223    let mut remaining = blocks;
1224    let mut logical_block = 0u32;
1225    let mut physical_start = start;
1226    while remaining > 0 {
1227        let block_count = remaining.min(MAX_INITIALIZED_EXTENT_BLOCKS);
1228        extents.push(AllocatedExtent {
1229            logical_block,
1230            physical_start,
1231            block_count,
1232        });
1233        logical_block += block_count;
1234        physical_start += u64::from(block_count);
1235        remaining -= block_count;
1236    }
1237    extents
1238}
1239
1240fn build_directory_data(
1241    dir_inode: u32,
1242    parent_inode: u32,
1243    children: &[DirEntrySpec],
1244    path: &str,
1245) -> Result<Vec<u8>, Ext4Error> {
1246    let mut entries = Vec::with_capacity(children.len() + 2);
1247    entries.push(DirEntrySpec {
1248        inode: dir_inode,
1249        file_type: EXT4_FT_DIR,
1250        name: b".".to_vec(),
1251    });
1252    entries.push(DirEntrySpec {
1253        inode: parent_inode,
1254        file_type: EXT4_FT_DIR,
1255        name: b"..".to_vec(),
1256    });
1257    entries.extend(children.iter().map(|entry| DirEntrySpec {
1258        inode: entry.inode,
1259        file_type: entry.file_type,
1260        name: entry.name.clone(),
1261    }));
1262
1263    let mut blocks = Vec::new();
1264    let mut index = 0usize;
1265    while index < entries.len() {
1266        let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1267        let mut pos = 0usize;
1268        let data_limit = EXT4_BLOCK_SIZE as usize - 12;
1269        let block_start = index;
1270
1271        while index < entries.len() {
1272            let min_len = dir_entry_len(entries[index].name.len());
1273            let needed = if pos == 0 { min_len } else { pos + min_len };
1274            if needed > data_limit {
1275                if pos == 0 {
1276                    return Err(Ext4Error::Layout(format!(
1277                        "directory entry too large for '{path}'"
1278                    )));
1279                }
1280                break;
1281            }
1282            pos += min_len;
1283            index += 1;
1284        }
1285
1286        let mut write_pos = 0usize;
1287        for (entry_index, entry) in entries[block_start..index].iter().enumerate() {
1288            let is_last = entry_index + 1 == index - block_start;
1289            let rec_len = if is_last {
1290                (data_limit - write_pos) as u16
1291            } else {
1292                dir_entry_len(entry.name.len()) as u16
1293            };
1294            put_le32(&mut block, write_pos, entry.inode);
1295            put_le16(&mut block, write_pos + 4, rec_len);
1296            block[write_pos + 6] = entry.name.len() as u8;
1297            block[write_pos + 7] = entry.file_type;
1298            block[write_pos + 8..write_pos + 8 + entry.name.len()].copy_from_slice(&entry.name);
1299            write_pos += rec_len as usize;
1300        }
1301
1302        let tail = data_limit;
1303        put_le32(&mut block, tail, 0);
1304        put_le16(&mut block, tail + 4, 12);
1305        block[tail + 6] = 0;
1306        block[tail + 7] = 0xDE;
1307        blocks.extend_from_slice(&block);
1308    }
1309
1310    if blocks.is_empty() {
1311        let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1312        put_le32(&mut block, 0, dir_inode);
1313        put_le16(&mut block, 4, 12);
1314        block[6] = 1;
1315        block[7] = EXT4_FT_DIR;
1316        block[8] = b'.';
1317        put_le32(&mut block, 12, parent_inode);
1318        put_le16(&mut block, 16, (EXT4_BLOCK_SIZE - 24) as u16);
1319        block[18] = 2;
1320        block[19] = EXT4_FT_DIR;
1321        block[20] = b'.';
1322        block[21] = b'.';
1323        let tail = EXT4_BLOCK_SIZE as usize - 12;
1324        put_le32(&mut block, tail, 0);
1325        put_le16(&mut block, tail + 4, 12);
1326        block[tail + 7] = 0xDE;
1327        blocks = block;
1328    }
1329
1330    Ok(blocks)
1331}
1332
1333fn dir_entry_len(name_len: usize) -> usize {
1334    (8 + name_len + 3) & !3
1335}
1336
1337fn allocate_node_data(allocator: &mut DataAllocator, plan: &mut NodePlan) -> Result<(), Ext4Error> {
1338    if plan.block_count > 0 {
1339        plan.data_extents = allocator.allocate_extents(plan.block_count, &plan.path)?;
1340        if plan.data_extents.len() > INLINE_EXTENT_CAPACITY {
1341            plan.extent_tree_block = Some(allocator.allocate_block(&plan.path)?);
1342        }
1343    }
1344
1345    if !plan.xattrs.is_empty() {
1346        plan.inline_xattrs = build_inline_xattrs(&plan.xattrs, &plan.path)?;
1347        if plan.inline_xattrs.is_none() {
1348            plan.xattr_block = Some(allocator.allocate_block(&plan.path)?);
1349        }
1350    }
1351    Ok(())
1352}
1353
1354impl DataAllocator {
1355    fn new(layout: &Layout) -> Self {
1356        let mut regions = Vec::new();
1357        for group in 0..layout.num_groups {
1358            let group_start = layout.group_start_block(group);
1359            let group_end = group_start + layout.blocks_in_group(group) as u64;
1360            let start = layout.group_data_start_block(group);
1361            if start < group_end {
1362                regions.push((start, (group_end - start) as u32));
1363            }
1364        }
1365        Self { regions }
1366    }
1367
1368    fn allocate_extents(
1369        &mut self,
1370        blocks: u32,
1371        path: &str,
1372    ) -> Result<Vec<AllocatedExtent>, Ext4Error> {
1373        let mut extents = Vec::new();
1374        let mut remaining = blocks;
1375        let mut logical_block = 0u32;
1376
1377        for region in &mut self.regions {
1378            while remaining > 0 && region.1 > 0 {
1379                let block_count = remaining.min(region.1).min(MAX_INITIALIZED_EXTENT_BLOCKS);
1380                extents.push(AllocatedExtent {
1381                    logical_block,
1382                    physical_start: region.0,
1383                    block_count,
1384                });
1385                region.0 += u64::from(block_count);
1386                region.1 -= block_count;
1387                remaining -= block_count;
1388                logical_block += block_count;
1389            }
1390            if remaining == 0 {
1391                return Ok(extents);
1392            }
1393        }
1394
1395        Err(Ext4Error::Layout(format!(
1396            "not enough space in ext4 image for '{path}'"
1397        )))
1398    }
1399
1400    fn allocate_block(&mut self, path: &str) -> Result<u64, Ext4Error> {
1401        self.allocate_extents(1, path)?
1402            .first()
1403            .map(|extent| extent.physical_start)
1404            .ok_or_else(|| {
1405                Ext4Error::Layout(format!("could not allocate extent tree for '{path}'"))
1406            })
1407    }
1408}
1409
1410fn build_block_bitmap_for_plan(layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
1411    let mut bitmap = build_block_bitmap_base(&layout.geometry(), group);
1412    let group_start = layout.group_start_block(group);
1413    let group_end = group_start + layout.blocks_in_group(group) as u64;
1414
1415    for (start, len) in &plan.block_extents {
1416        let extent_start = *start;
1417        let extent_end = extent_start + *len as u64;
1418        let overlap_start = extent_start.max(group_start);
1419        let overlap_end = extent_end.min(group_end);
1420        if overlap_start < overlap_end {
1421            for block in overlap_start..overlap_end {
1422                let bit = block - group_start;
1423                bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1424            }
1425        }
1426    }
1427
1428    bitmap
1429}
1430
1431fn build_inode_bitmap_for_plan(_layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
1432    let first_inode = group * EXT4_INODES_PER_GROUP + 1;
1433    let last_inode = first_inode + EXT4_INODES_PER_GROUP - 1;
1434    let used_inodes = if plan.max_used_inode < first_inode {
1435        0
1436    } else {
1437        plan.max_used_inode.min(last_inode) - first_inode + 1
1438    };
1439    build_inode_bitmap_base(used_inodes)
1440}
1441
1442fn compute_fs_stats(layout: &Layout, plan: &BitmapPlan) -> FsStats {
1443    let mut group_free_blocks = Vec::with_capacity(layout.num_groups as usize);
1444    let mut block_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
1445    let mut inode_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
1446
1447    let mut total_free_blocks = 0u64;
1448    for group in 0..layout.num_groups {
1449        let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
1450        let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
1451        let blocks_in_group = layout.blocks_in_group(group) as usize;
1452        let used = count_used_bits(&block_bitmap, blocks_in_group);
1453        let free = blocks_in_group.saturating_sub(used) as u32;
1454        group_free_blocks.push(free);
1455        block_bitmap_checksums.push(bitmap_checksum(
1456            layout.csum_seed,
1457            &block_bitmap,
1458            EXT4_BLOCK_SIZE as usize,
1459        ));
1460        inode_bitmap_checksums.push(bitmap_checksum(
1461            layout.csum_seed,
1462            &inode_bitmap,
1463            (EXT4_INODES_PER_GROUP / 8) as usize,
1464        ));
1465        total_free_blocks += free as u64;
1466    }
1467
1468    let group_free_inodes = (0..layout.num_groups)
1469        .map(|group| {
1470            let first_inode = group * EXT4_INODES_PER_GROUP + 1;
1471            let last_inode = first_inode + EXT4_INODES_PER_GROUP - 1;
1472            let used_inodes = if plan.max_used_inode < first_inode {
1473                0
1474            } else {
1475                plan.max_used_inode.min(last_inode) - first_inode + 1
1476            };
1477            EXT4_INODES_PER_GROUP - used_inodes
1478        })
1479        .collect::<Vec<_>>();
1480    let total_free_inodes = group_free_inodes.iter().map(|count| *count as u64).sum();
1481
1482    let group_used_dirs = plan.group_used_dirs.clone();
1483
1484    FsStats {
1485        group_free_blocks,
1486        group_free_inodes,
1487        group_used_dirs,
1488        block_bitmap_checksums,
1489        inode_bitmap_checksums,
1490        total_free_blocks,
1491        total_free_inodes,
1492        overhead_blocks: layout.total_overhead_blocks(),
1493    }
1494}
1495
1496fn write_tree_data(
1497    file: &mut (impl std::io::Write + std::io::Seek),
1498    layout: &Layout,
1499    plans: &[NodePlan],
1500) -> Result<(), Ext4Error> {
1501    for plan in plans {
1502        match &plan.kind {
1503            NodeKind::Directory { data, .. } => {
1504                let mut bytes = data.clone();
1505                update_dir_block_checksums(layout.csum_seed, plan.inode, &mut bytes);
1506                write_bytes_to_extents(file, &plan.data_extents, &bytes)?;
1507            }
1508            NodeKind::RegularFile { data } => {
1509                write_file_data(file, &plan.data_extents, data)?;
1510            }
1511            NodeKind::Symlink { target, inline } => {
1512                if !inline {
1513                    write_bytes_to_extents(file, &plan.data_extents, target)?;
1514                }
1515            }
1516            NodeKind::CharDevice { .. }
1517            | NodeKind::BlockDevice { .. }
1518            | NodeKind::Fifo
1519            | NodeKind::Socket => {}
1520        }
1521
1522        if let Some(block) = plan.extent_tree_block {
1523            let bytes = build_extent_leaf_block(layout, plan)?;
1524            write_extent_bytes(file, block, &bytes)?;
1525        }
1526        if let Some(block) = plan.xattr_block {
1527            let bytes = build_xattr_block(layout, block, &plan.xattrs, &plan.path)?;
1528            write_extent_bytes(file, block, &bytes)?;
1529        }
1530    }
1531
1532    Ok(())
1533}
1534
1535fn write_file_data(
1536    file: &mut (impl std::io::Write + std::io::Seek),
1537    extents: &[AllocatedExtent],
1538    data: &FileData,
1539) -> Result<(), Ext4Error> {
1540    let mut data_offset = 0usize;
1541    for extent in extents {
1542        let remaining = data.len().saturating_sub(data_offset);
1543        let extent_bytes = extent.block_count as usize * EXT4_BLOCK_SIZE as usize;
1544        let write_len = remaining.min(extent_bytes);
1545        file.seek(SeekFrom::Start(
1546            extent.physical_start * EXT4_BLOCK_SIZE as u64,
1547        ))?;
1548        data.write_range(data_offset, write_len, file)?;
1549        write_zero_padding(file, extent_bytes - write_len)?;
1550        data_offset += write_len;
1551    }
1552
1553    debug_assert_eq!(data_offset, data.len());
1554
1555    Ok(())
1556}
1557
1558fn write_bytes_to_extents(
1559    file: &mut (impl std::io::Write + std::io::Seek),
1560    extents: &[AllocatedExtent],
1561    data: &[u8],
1562) -> Result<(), Ext4Error> {
1563    let mut data_offset = 0usize;
1564    for extent in extents {
1565        let remaining = data.len().saturating_sub(data_offset);
1566        let extent_bytes = extent.block_count as usize * EXT4_BLOCK_SIZE as usize;
1567        let write_len = remaining.min(extent_bytes);
1568        file.seek(SeekFrom::Start(
1569            extent.physical_start * EXT4_BLOCK_SIZE as u64,
1570        ))?;
1571        file.write_all(&data[data_offset..data_offset + write_len])?;
1572        write_zero_padding(file, extent_bytes - write_len)?;
1573        data_offset += write_len;
1574    }
1575
1576    debug_assert_eq!(data_offset, data.len());
1577    Ok(())
1578}
1579
1580fn write_zero_padding(file: &mut impl std::io::Write, mut len: usize) -> Result<(), Ext4Error> {
1581    static ZEROS: [u8; EXT4_BLOCK_SIZE as usize] = [0u8; EXT4_BLOCK_SIZE as usize];
1582    while len > 0 {
1583        let chunk = len.min(ZEROS.len());
1584        file.write_all(&ZEROS[..chunk])?;
1585        len -= chunk;
1586    }
1587    Ok(())
1588}
1589
1590fn write_extent_bytes(
1591    file: &mut (impl std::io::Write + std::io::Seek),
1592    start_block: u64,
1593    data: &[u8],
1594) -> Result<(), Ext4Error> {
1595    let offset = start_block * EXT4_BLOCK_SIZE as u64;
1596    file.seek(SeekFrom::Start(offset))?;
1597    file.write_all(data)?;
1598
1599    let pad = (EXT4_BLOCK_SIZE as usize - (data.len() % EXT4_BLOCK_SIZE as usize))
1600        % EXT4_BLOCK_SIZE as usize;
1601    if pad > 0 {
1602        static ZEROS: [u8; 4096] = [0u8; 4096];
1603        file.write_all(&ZEROS[..pad])?;
1604    }
1605
1606    Ok(())
1607}
1608
1609fn update_dir_block_checksums(csum_seed: u32, inode: u32, data: &mut [u8]) {
1610    for chunk in data.as_chunks_mut::<{ EXT4_BLOCK_SIZE as usize }>().0 {
1611        let tail = EXT4_BLOCK_SIZE as usize - 12;
1612        let checksum = dir_block_checksum(csum_seed, inode, 0, &chunk[..tail]);
1613        put_le32(chunk, tail + 8, checksum);
1614    }
1615}
1616
1617fn write_inode_table_with_plan(
1618    file: &mut (impl std::io::Write + std::io::Seek),
1619    layout: &Layout,
1620    plans: &[NodePlan],
1621) -> Result<(), Ext4Error> {
1622    let root_inode = build_inode_from_plan(layout, &plans[0])?;
1623    let root_offset = inode_offset(layout, EXT4_ROOT_INO);
1624    file.seek(SeekFrom::Start(root_offset))?;
1625    file.write_all(&root_inode)?;
1626
1627    let journal_inode = build_journal_inode(layout)?;
1628    let journal_offset = inode_offset(layout, EXT4_JOURNAL_INO);
1629    file.seek(SeekFrom::Start(journal_offset))?;
1630    file.write_all(&journal_inode)?;
1631
1632    for plan in plans.iter().filter(|plan| plan.inode >= EXT4_FIRST_INO) {
1633        let inode_bytes = build_inode_from_plan(layout, plan)?;
1634        file.seek(SeekFrom::Start(inode_offset(layout, plan.inode)))?;
1635        file.write_all(&inode_bytes)?;
1636    }
1637
1638    Ok(())
1639}
1640
1641fn inode_offset(layout: &Layout, inode: u32) -> u64 {
1642    let zero_based = inode - 1;
1643    let group = zero_based / EXT4_INODES_PER_GROUP;
1644    let index = zero_based % EXT4_INODES_PER_GROUP;
1645    layout.group_inode_table_block(group) * EXT4_BLOCK_SIZE as u64
1646        + u64::from(index) * EXT4_INODE_SIZE as u64
1647}
1648
1649fn build_inode_from_plan(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
1650    let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1651    let (mode, size, links_count, extents) = match &plan.kind {
1652        NodeKind::Directory { data, .. } => (
1653            S_IFDIR | plan.metadata.mode,
1654            data.len() as u64,
1655            plan.links_count,
1656            true,
1657        ),
1658        NodeKind::RegularFile { data } => (
1659            S_IFREG | plan.metadata.mode,
1660            data.len() as u64,
1661            plan.links_count,
1662            true,
1663        ),
1664        NodeKind::Symlink { target, inline } => (
1665            S_IFLNK | plan.metadata.mode,
1666            target.len() as u64,
1667            plan.links_count,
1668            !inline,
1669        ),
1670        NodeKind::CharDevice { .. } => (S_IFCHR | plan.metadata.mode, 0, plan.links_count, false),
1671        NodeKind::BlockDevice { .. } => (S_IFBLK | plan.metadata.mode, 0, plan.links_count, false),
1672        NodeKind::Fifo => (S_IFIFO | plan.metadata.mode, 0, plan.links_count, false),
1673        NodeKind::Socket => (S_IFSOCK | plan.metadata.mode, 0, plan.links_count, false),
1674    };
1675
1676    put_le16(&mut inode, 0x00, mode);
1677    put_le16(&mut inode, 0x02, plan.metadata.uid as u16);
1678    put_le32(&mut inode, 0x04, size as u32);
1679    write_inode_time(&mut inode, 0x08, 0x8c, &plan.metadata)?;
1680    write_inode_time(&mut inode, 0x0c, 0x84, &plan.metadata)?;
1681    write_inode_time(&mut inode, 0x10, 0x88, &plan.metadata)?;
1682    put_le16(&mut inode, 0x18, plan.metadata.gid as u16);
1683    put_le16(
1684        &mut inode,
1685        0x1A,
1686        u16::try_from(links_count).map_err(|_| {
1687            Ext4Error::Layout(format!(
1688                "link count exceeds ext4 inode field for '{}'",
1689                plan.path
1690            ))
1691        })?,
1692    );
1693    let allocated_blocks = plan.block_count
1694        + u32::from(plan.extent_tree_block.is_some())
1695        + u32::from(plan.xattr_block.is_some());
1696    put_le32(&mut inode, 0x1C, allocated_blocks * (EXT4_BLOCK_SIZE / 512));
1697    if extents {
1698        put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1699    }
1700
1701    match &plan.kind {
1702        NodeKind::Directory { .. } | NodeKind::RegularFile { .. } => {
1703            write_extent_plan(&mut inode, 0x28, plan)?;
1704        }
1705        NodeKind::Symlink { target, inline } => {
1706            if *inline {
1707                inode[0x28..0x28 + target.len()].copy_from_slice(target);
1708            } else {
1709                write_extent_plan(&mut inode, 0x28, plan)?;
1710            }
1711        }
1712        NodeKind::CharDevice { major, minor } | NodeKind::BlockDevice { major, minor } => {
1713            write_device_number(&mut inode, *major, *minor, &plan.path)?;
1714        }
1715        NodeKind::Fifo | NodeKind::Socket => {}
1716    }
1717
1718    put_le32(&mut inode, 0x64, 0);
1719    if let Some(block) = plan.xattr_block {
1720        put_le32(&mut inode, 0x68, block as u32);
1721        put_le16(&mut inode, 0x76, (block >> 32) as u16);
1722    }
1723    put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1724    put_le16(&mut inode, 0x78, (plan.metadata.uid >> 16) as u16);
1725    put_le16(&mut inode, 0x7A, (plan.metadata.gid >> 16) as u16);
1726    put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1727    write_inode_time(&mut inode, 0x90, 0x94, &plan.metadata)?;
1728    if let Some(xattrs) = &plan.inline_xattrs {
1729        inode[0xA0..0xA0 + xattrs.len()].copy_from_slice(xattrs);
1730    }
1731
1732    let csum = inode_checksum(layout.csum_seed, plan.inode, 0, &inode);
1733    put_le16(&mut inode, 0x7C, csum as u16);
1734    put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1735
1736    Ok(inode)
1737}
1738
1739fn write_inode_time(
1740    inode: &mut [u8],
1741    seconds_offset: usize,
1742    extra_offset: usize,
1743    metadata: &InodeMetadata,
1744) -> Result<(), Ext4Error> {
1745    if metadata.mtime_nsec >= 1_000_000_000 {
1746        return Err(Ext4Error::Layout(format!(
1747            "inode timestamp nanoseconds are out of range: {}",
1748            metadata.mtime_nsec
1749        )));
1750    }
1751    if metadata.mtime > 0x3_ffff_ffff {
1752        return Err(Ext4Error::Layout(format!(
1753            "inode timestamp seconds exceed ext4 epoch range: {}",
1754            metadata.mtime
1755        )));
1756    }
1757
1758    put_le32(inode, seconds_offset, metadata.mtime as u32);
1759    let epoch = ((metadata.mtime >> 32) & 0x3) as u32;
1760    put_le32(inode, extra_offset, (metadata.mtime_nsec << 2) | epoch);
1761    Ok(())
1762}
1763
1764fn write_device_number(
1765    inode: &mut [u8],
1766    major: u32,
1767    minor: u32,
1768    path: &str,
1769) -> Result<(), Ext4Error> {
1770    if major > 0x0fff || minor > 0x0f_ffff {
1771        return Err(Ext4Error::Layout(format!(
1772            "device number is out of Linux ext4 range for '{path}': {major}:{minor}"
1773        )));
1774    }
1775
1776    if major < 256 && minor < 256 {
1777        put_le32(inode, 0x28, (major << 8) | minor);
1778    } else {
1779        let encoded = (minor & 0xff) | (major << 8) | ((minor & !0xff) << 12);
1780        put_le32(inode, 0x2c, encoded);
1781    }
1782    Ok(())
1783}
1784
1785fn write_empty_extent_tree(buf: &mut [u8], offset: usize) {
1786    put_le16(buf, offset, EXT4_EH_MAGIC);
1787    put_le16(buf, offset + 2, 0);
1788    put_le16(buf, offset + 4, 4);
1789    put_le16(buf, offset + 6, 0);
1790    put_le32(buf, offset + 8, 0);
1791}
1792
1793fn build_superblock_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1794    build_superblock_with_stats_for_group(layout, stats, 0, true)
1795}
1796
1797fn build_backup_superblock_with_stats(
1798    layout: &Layout,
1799    stats: &FsStats,
1800    group: u32,
1801) -> Result<Vec<u8>, Ext4Error> {
1802    build_superblock_with_stats_for_group(layout, stats, group, false)
1803}
1804
1805fn build_superblock_with_stats_for_group(
1806    layout: &Layout,
1807    stats: &FsStats,
1808    group: u32,
1809    padded_primary: bool,
1810) -> Result<Vec<u8>, Ext4Error> {
1811    let mut block = build_superblock(layout, group, padded_primary)?;
1812    let sb_offset = if padded_primary { 1024 } else { 0 };
1813    let sb = &mut block[sb_offset..sb_offset + 1024];
1814    put_le32(sb, 0x0C, stats.total_free_blocks as u32);
1815    put_le32(sb, 0x10, stats.total_free_inodes as u32);
1816    put_le32(sb, 0x158, (stats.total_free_blocks >> 32) as u32);
1817    let (overhead_offset, overhead_blocks) = if layout.resize_inode_block.is_none() {
1818        layout.recorded_overhead_blocks(stats.total_free_blocks)
1819    } else {
1820        (EXT4_SB_OVERHEAD_BLOCKS_OFFSET, stats.overhead_blocks)
1821    };
1822    put_le32(sb, overhead_offset, overhead_blocks as u32);
1823    let checksum = superblock_checksum(sb);
1824    put_le32(sb, 0x3FC, checksum);
1825    Ok(block)
1826}
1827
1828fn build_gdt_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1829    let desc_size = EXT4_DESC_SIZE as usize;
1830    let geometry = layout.geometry();
1831    let mut gdt = Vec::with_capacity(layout.num_groups as usize * desc_size);
1832
1833    for g in 0..layout.num_groups {
1834        let desc_stats = GroupDescStats {
1835            free_blocks: stats.group_free_blocks[g as usize],
1836            free_inodes: stats.group_free_inodes[g as usize],
1837            used_dirs: stats.group_used_dirs[g as usize],
1838            block_bitmap_csum: stats.block_bitmap_checksums[g as usize],
1839            inode_bitmap_csum: stats.inode_bitmap_checksums[g as usize],
1840        };
1841        gdt.extend_from_slice(&build_group_descriptor(
1842            &geometry,
1843            g,
1844            &desc_stats,
1845            layout.csum_seed,
1846        ));
1847    }
1848
1849    Ok(gdt)
1850}
1851#[cfg(test)]
1852fn build_block_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1853    (0..layout.num_groups)
1854        .map(|group| build_block_bitmap(layout, group))
1855        .collect()
1856}
1857
1858#[cfg(test)]
1859fn build_inode_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1860    (0..layout.num_groups)
1861        .map(|group| build_inode_bitmap(layout, group))
1862        .collect()
1863}
1864
1865#[cfg(test)]
1866fn build_block_bitmap(layout: &Layout, group: u32) -> Vec<u8> {
1867    let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1868
1869    // Metadata, the root directory block, and the journal are permanently
1870    // allocated within the filesystem image.
1871    let used = layout.group_used_blocks(group);
1872    for bit in 0..used {
1873        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1874    }
1875
1876    // Bits beyond the final partial group are permanently unavailable.
1877    let blocks_in_group = layout.blocks_in_group(group);
1878    for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
1879        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1880    }
1881
1882    bitmap
1883}
1884
1885#[cfg(test)]
1886fn build_inode_bitmap(_layout: &Layout, group: u32) -> Vec<u8> {
1887    let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1888
1889    if group == 0 {
1890        // Inode numbering is 1-based; bit 0 corresponds to inode 1.
1891        for bit in 0..(EXT4_FIRST_INO - 1) {
1892            bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1893        }
1894    }
1895
1896    // The inode bitmap consumes only the first inodes-per-group bits; the
1897    // remaining padding bits in the block must stay permanently set.
1898    for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
1899        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1900    }
1901
1902    bitmap
1903}
1904
1905fn write_bitmaps(
1906    file: &mut (impl std::io::Write + std::io::Seek),
1907    layout: &Layout,
1908    plan: &BitmapPlan,
1909) -> Result<(), Ext4Error> {
1910    for group in 0..layout.num_groups {
1911        let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
1912        let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
1913        let block_offset = layout.group_block_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1914        file.seek(SeekFrom::Start(block_offset))?;
1915        file.write_all(&block_bitmap)?;
1916
1917        let inode_offset = layout.group_inode_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1918        file.seek(SeekFrom::Start(inode_offset))?;
1919        file.write_all(&inode_bitmap)?;
1920    }
1921
1922    Ok(())
1923}
1924
1925/// Build the 256-byte journal inode (inode 8).
1926fn build_journal_inode(layout: &Layout) -> Result<Vec<u8>, Ext4Error> {
1927    let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1928
1929    let mode = S_IFREG | 0o600;
1930    let size = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1931
1932    // i_mode (offset 0, u16)
1933    put_le16(&mut inode, 0x00, mode);
1934    // i_size_lo (offset 4, u32)
1935    put_le32(&mut inode, 0x04, size as u32);
1936    // i_size_high (offset 108, u32)
1937    put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1938    // i_links_count (offset 26, u16)
1939    put_le16(&mut inode, 0x1A, 1);
1940    // i_blocks_lo (offset 28, u32) -- in 512-byte sectors
1941    let sectors = (layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64) / 512;
1942    put_le32(&mut inode, 0x1C, sectors as u32);
1943    // i_flags (offset 32, u32)
1944    put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1945
1946    // i_block (offset 40, 60 bytes) -- extent tree pointing to journal blocks
1947    write_extent_tree(
1948        &mut inode,
1949        0x28,
1950        layout.journal_start_block,
1951        layout.journal_blocks,
1952        "journal",
1953    )?;
1954
1955    // i_generation (offset 100, u32)
1956    put_le32(&mut inode, 0x64, 0);
1957
1958    // -- Extended inode fields --
1959    // i_extra_isize (offset 128, u16)
1960    put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1961
1962    // Inode checksum
1963    let csum = inode_checksum(layout.csum_seed, EXT4_JOURNAL_INO, 0, &inode);
1964    // l_i_checksum_lo (offset 0x7C, u16)
1965    put_le16(&mut inode, 0x7C, csum as u16);
1966    // i_checksum_hi (offset 0x82, u16)
1967    put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1968
1969    Ok(inode)
1970}
1971
1972/// Write an extent tree header + one extent entry into `buf` at `offset`.
1973///
1974/// The extent tree header is 12 bytes, each extent entry is also 12 bytes.
1975fn write_extent_tree(
1976    buf: &mut [u8],
1977    offset: usize,
1978    start_block: u64,
1979    block_count: u32,
1980    label: &str,
1981) -> Result<(), Ext4Error> {
1982    validate_extent_block_count(block_count, label)?;
1983    if start_block > u32::MAX as u64 {
1984        return Err(Ext4Error::TooLarge {
1985            requested_blocks: start_block,
1986            max_blocks: u32::MAX as u64,
1987        });
1988    }
1989
1990    // Extent header (12 bytes)
1991    put_le16(buf, offset, EXT4_EH_MAGIC); // eh_magic
1992    put_le16(buf, offset + 2, 1); // eh_entries
1993    put_le16(buf, offset + 4, 4); // eh_max (for inode: (60-12)/12 = 4)
1994    put_le16(buf, offset + 6, 0); // eh_depth (leaf)
1995    put_le32(buf, offset + 8, 0); // eh_generation
1996
1997    // Extent entry (12 bytes) at offset+12
1998    let ext_off = offset + 12;
1999    put_le32(buf, ext_off, 0); // ee_block (logical block 0)
2000    put_le16(buf, ext_off + 4, block_count as u16); // ee_len
2001    put_le16(buf, ext_off + 6, (start_block >> 32) as u16); // ee_start_hi
2002    put_le32(buf, ext_off + 8, start_block as u32); // ee_start_lo
2003
2004    Ok(())
2005}
2006
2007fn write_extent_plan(buf: &mut [u8], offset: usize, plan: &NodePlan) -> Result<(), Ext4Error> {
2008    if plan.data_extents.is_empty() {
2009        write_empty_extent_tree(buf, offset);
2010        return Ok(());
2011    }
2012
2013    if plan.data_extents.len() <= INLINE_EXTENT_CAPACITY {
2014        write_extent_header(
2015            buf,
2016            offset,
2017            plan.data_extents.len() as u16,
2018            INLINE_EXTENT_CAPACITY as u16,
2019            0,
2020        );
2021        for (index, extent) in plan.data_extents.iter().enumerate() {
2022            write_extent_entry(buf, offset + 12 + index * 12, extent, &plan.path)?;
2023        }
2024        return Ok(());
2025    }
2026
2027    let leaf = plan.extent_tree_block.ok_or_else(|| {
2028        Ext4Error::Layout(format!("missing extent-tree block for '{}'", plan.path))
2029    })?;
2030    write_extent_header(buf, offset, 1, INLINE_EXTENT_CAPACITY as u16, 1);
2031    let index_offset = offset + 12;
2032    put_le32(buf, index_offset, plan.data_extents[0].logical_block);
2033    put_le32(buf, index_offset + 4, leaf as u32);
2034    put_le16(buf, index_offset + 8, (leaf >> 32) as u16);
2035    put_le16(buf, index_offset + 10, 0);
2036    Ok(())
2037}
2038
2039fn build_extent_leaf_block(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
2040    if plan.data_extents.len() > EXTENT_BLOCK_CAPACITY {
2041        return Err(Ext4Error::Layout(format!(
2042            "'{}' exceeds the depth-one extent-tree capacity",
2043            plan.path
2044        )));
2045    }
2046
2047    let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
2048    write_extent_header(
2049        &mut block,
2050        0,
2051        plan.data_extents.len() as u16,
2052        EXTENT_BLOCK_CAPACITY as u16,
2053        0,
2054    );
2055    for (index, extent) in plan.data_extents.iter().enumerate() {
2056        write_extent_entry(&mut block, 12 + index * 12, extent, &plan.path)?;
2057    }
2058
2059    let tail = EXT4_BLOCK_SIZE as usize - std::mem::size_of::<u32>();
2060    let checksum = dir_block_checksum(layout.csum_seed, plan.inode, 0, &block[..tail]);
2061    put_le32(&mut block, tail, checksum);
2062    Ok(block)
2063}
2064
2065fn build_xattr_block(
2066    layout: &Layout,
2067    block_number: u64,
2068    xattrs: &[Xattr],
2069    path: &str,
2070) -> Result<Vec<u8>, Ext4Error> {
2071    let encoded = encode_xattrs(xattrs, path)?;
2072
2073    let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
2074    put_le32(&mut block, 0, EXT4_XATTR_MAGIC);
2075    put_le32(&mut block, 4, 1);
2076    put_le32(&mut block, 8, 1);
2077
2078    let mut entry_offset = EXT4_XATTR_HEADER_SIZE;
2079    let mut value_offset = EXT4_BLOCK_SIZE as usize;
2080    let mut block_hash = 0u32;
2081    for xattr in encoded {
2082        let entry_len = align_four(EXT4_XATTR_ENTRY_SIZE + xattr.name.len());
2083        let padded_value_len = align_four(xattr.value.len());
2084        value_offset = value_offset.checked_sub(padded_value_len).ok_or_else(|| {
2085            Ext4Error::Layout(format!(
2086                "extended attributes on '{path}' exceed an ext4 block"
2087            ))
2088        })?;
2089        if entry_offset + entry_len + 4 > value_offset {
2090            return Err(Ext4Error::Layout(format!(
2091                "extended attributes on '{path}' do not fit in one ext4 xattr block"
2092            )));
2093        }
2094
2095        block[value_offset..value_offset + xattr.value.len()].copy_from_slice(xattr.value);
2096        let entry_hash = xattr_entry_hash(
2097            xattr.name,
2098            &block[value_offset..value_offset + padded_value_len],
2099        );
2100        block[entry_offset] = xattr.name.len() as u8;
2101        block[entry_offset + 1] = xattr.name_index;
2102        put_le16(&mut block, entry_offset + 2, value_offset as u16);
2103        put_le32(&mut block, entry_offset + 4, 0);
2104        put_le32(&mut block, entry_offset + 8, xattr.value.len() as u32);
2105        put_le32(&mut block, entry_offset + 12, entry_hash);
2106        block[entry_offset + 16..entry_offset + 16 + xattr.name.len()].copy_from_slice(xattr.name);
2107        entry_offset += entry_len;
2108        block_hash = block_hash.rotate_left(16) ^ entry_hash;
2109    }
2110
2111    put_le32(&mut block, 12, block_hash);
2112    let checksum = xattr_block_checksum(layout.csum_seed, block_number, &block);
2113    put_le32(&mut block, 16, checksum);
2114    Ok(block)
2115}
2116
2117fn build_inline_xattrs(xattrs: &[Xattr], path: &str) -> Result<Option<Vec<u8>>, Ext4Error> {
2118    if xattrs.is_empty() {
2119        return Ok(None);
2120    }
2121
2122    let encoded = encode_xattrs(xattrs, path)?;
2123    let mut bytes = vec![0u8; EXT4_INODE_SIZE as usize - 0xA0];
2124    put_le32(&mut bytes, 0, EXT4_XATTR_MAGIC);
2125    let first_entry = 4usize;
2126    let mut entry_offset = first_entry;
2127    let mut value_offset = bytes.len();
2128    for xattr in encoded {
2129        let entry_len = align_four(EXT4_XATTR_ENTRY_SIZE + xattr.name.len());
2130        let padded_value_len = align_four(xattr.value.len());
2131        let Some(next_value_offset) = value_offset.checked_sub(padded_value_len) else {
2132            return Ok(None);
2133        };
2134        if entry_offset + entry_len + 4 > next_value_offset {
2135            return Ok(None);
2136        }
2137        value_offset = next_value_offset;
2138        bytes[value_offset..value_offset + xattr.value.len()].copy_from_slice(xattr.value);
2139        bytes[entry_offset] = xattr.name.len() as u8;
2140        bytes[entry_offset + 1] = xattr.name_index;
2141        put_le16(
2142            &mut bytes,
2143            entry_offset + 2,
2144            (value_offset - first_entry) as u16,
2145        );
2146        put_le32(&mut bytes, entry_offset + 4, 0);
2147        put_le32(&mut bytes, entry_offset + 8, xattr.value.len() as u32);
2148        put_le32(&mut bytes, entry_offset + 12, 0);
2149        bytes[entry_offset + 16..entry_offset + 16 + xattr.name.len()].copy_from_slice(xattr.name);
2150        entry_offset += entry_len;
2151    }
2152
2153    Ok(Some(bytes))
2154}
2155
2156fn encode_xattrs<'a>(xattrs: &'a [Xattr], path: &str) -> Result<Vec<EncodedXattr<'a>>, Ext4Error> {
2157    let mut encoded = xattrs
2158        .iter()
2159        .map(|xattr| encode_xattr_name(xattr, path))
2160        .collect::<Result<Vec<_>, _>>()?;
2161    encoded.sort_by(|left, right| {
2162        (left.name_index, left.name.len(), left.name).cmp(&(
2163            right.name_index,
2164            right.name.len(),
2165            right.name,
2166        ))
2167    });
2168    if encoded
2169        .windows(2)
2170        .any(|pair| pair[0].name_index == pair[1].name_index && pair[0].name == pair[1].name)
2171    {
2172        return Err(Ext4Error::Layout(format!(
2173            "duplicate extended attribute on '{path}'"
2174        )));
2175    }
2176    Ok(encoded)
2177}
2178
2179fn encode_xattr_name<'a>(xattr: &'a Xattr, path: &str) -> Result<EncodedXattr<'a>, Ext4Error> {
2180    if xattr.name.is_empty() || xattr.name.len() > 255 || xattr.name.contains(&0) {
2181        return Err(Ext4Error::Layout(format!(
2182            "invalid extended-attribute name on '{path}'"
2183        )));
2184    }
2185
2186    let (name_index, name) = if xattr.name.as_slice() == b"system.posix_acl_access" {
2187        (2, &[][..])
2188    } else if xattr.name.as_slice() == b"system.posix_acl_default" {
2189        (3, &[][..])
2190    } else if let Some(name) = xattr.name.strip_prefix(b"user.") {
2191        (1, name)
2192    } else if let Some(name) = xattr.name.strip_prefix(b"trusted.") {
2193        (4, name)
2194    } else if let Some(name) = xattr.name.strip_prefix(b"security.") {
2195        (6, name)
2196    } else {
2197        return Err(Ext4Error::Layout(format!(
2198            "unsupported extended-attribute namespace on '{path}'"
2199        )));
2200    };
2201    if name.is_empty() && name_index != 2 && name_index != 3 {
2202        return Err(Ext4Error::Layout(format!(
2203            "extended-attribute namespace has an empty name on '{path}'"
2204        )));
2205    }
2206
2207    Ok(EncodedXattr {
2208        name_index,
2209        name,
2210        value: &xattr.value,
2211    })
2212}
2213
2214fn xattr_entry_hash(name: &[u8], padded_value: &[u8]) -> u32 {
2215    let mut hash = 0u32;
2216    for byte in name {
2217        hash = hash.rotate_left(5) ^ u32::from(*byte);
2218    }
2219    for word in padded_value.as_chunks::<4>().0 {
2220        hash = hash.rotate_left(16) ^ u32::from_le_bytes(*word);
2221    }
2222    hash
2223}
2224
2225fn xattr_block_checksum(csum_seed: u32, block_number: u64, block: &[u8]) -> u32 {
2226    let mut checksum = crc32c::crc32c_raw(csum_seed, &block_number.to_le_bytes());
2227    checksum = crc32c::crc32c_raw(checksum, &block[..16]);
2228    checksum = crc32c::crc32c_raw(checksum, &[0u8; 4]);
2229    crc32c::crc32c_raw(checksum, &block[20..])
2230}
2231
2232fn align_four(value: usize) -> usize {
2233    (value + 3) & !3
2234}
2235
2236fn write_extent_header(buf: &mut [u8], offset: usize, entries: u16, max: u16, depth: u16) {
2237    put_le16(buf, offset, EXT4_EH_MAGIC);
2238    put_le16(buf, offset + 2, entries);
2239    put_le16(buf, offset + 4, max);
2240    put_le16(buf, offset + 6, depth);
2241    put_le32(buf, offset + 8, 0);
2242}
2243
2244fn write_extent_entry(
2245    buf: &mut [u8],
2246    offset: usize,
2247    extent: &AllocatedExtent,
2248    label: &str,
2249) -> Result<(), Ext4Error> {
2250    validate_extent_block_count(extent.block_count, label)?;
2251    put_le32(buf, offset, extent.logical_block);
2252    put_le16(buf, offset + 4, extent.block_count as u16);
2253    put_le16(buf, offset + 6, (extent.physical_start >> 32) as u16);
2254    put_le32(buf, offset + 8, extent.physical_start as u32);
2255    Ok(())
2256}
2257
2258/// Write the journal superblock at the first journal block.
2259fn write_journal(
2260    file: &mut (impl std::io::Write + std::io::Seek),
2261    layout: &Layout,
2262) -> Result<(), Ext4Error> {
2263    let mut jsb = vec![0u8; EXT4_BLOCK_SIZE as usize];
2264
2265    // All jbd2 fields are BIG-ENDIAN.
2266    // Header (12 bytes)
2267    put_be32(&mut jsb, 0, JBD2_MAGIC); // h_magic
2268    put_be32(&mut jsb, 4, JBD2_SUPERBLOCK_V2); // h_blocktype
2269    put_be32(&mut jsb, 8, 0); // h_sequence (not used for sb)
2270
2271    // Journal superblock fields
2272    put_be32(&mut jsb, 12, EXT4_BLOCK_SIZE); // s_blocksize
2273    put_be32(&mut jsb, 16, layout.journal_blocks); // s_maxlen
2274    put_be32(&mut jsb, 20, 1); // s_first (first log block)
2275    put_be32(&mut jsb, 24, 1); // s_sequence (next expected sequence)
2276    put_be32(&mut jsb, 28, 0); // s_start (0 = clean/no recovery needed)
2277
2278    // s_errno (offset 32)
2279    put_be32(&mut jsb, 32, 0);
2280    // s_feature_compat (offset 36)
2281    put_be32(&mut jsb, 36, 0);
2282    // s_feature_incompat (offset 40) = CSUM_V3(0x10) | 64BIT(0x02) | REVOKE(0x01)
2283    put_be32(&mut jsb, 40, 0x13);
2284    // s_feature_ro_compat (offset 44)
2285    put_be32(&mut jsb, 44, 0);
2286
2287    // s_uuid (offset 48, 16 bytes) -- same as filesystem UUID
2288    jsb[48..64].copy_from_slice(&layout.uuid);
2289
2290    // s_nr_users (offset 64, u32)
2291    put_be32(&mut jsb, 64, 1);
2292
2293    // s_dynsuper (offset 68, u32) -- block of dynamic superblock copy
2294    put_be32(&mut jsb, 68, 0);
2295
2296    // s_max_transaction (offset 72), s_max_trans_data (offset 76)
2297    put_be32(&mut jsb, 72, 0);
2298    put_be32(&mut jsb, 76, 0);
2299
2300    // s_checksum_type (offset 80, u8) = 4 (CRC32C)
2301    // Actually offset for checksum_type is at offset 80+... Let me use correct
2302    // offsets from the jbd2 spec:
2303    //   offset 80: padding (u8)
2304    //   offset 81-83: padding
2305    //   offset 84-87: s_padding2
2306    //   offset 88-91: s_num_fc_blks
2307    //   offset 92-95: s_head
2308    //   offset 96-255: s_padding[44]
2309    //   offset 256-271: s_users[16*48] (first 16 bytes = first user UUID)
2310    //
2311    // Correct jbd2 superblock layout (from kernel headers):
2312    //   0x00: h_magic (u32be)
2313    //   0x04: h_blocktype (u32be)
2314    //   0x08: h_sequence (u32be)
2315    //   0x0C: s_blocksize (u32be)
2316    //   0x10: s_maxlen (u32be)
2317    //   0x14: s_first (u32be)
2318    //   0x18: s_sequence (u32be)
2319    //   0x1C: s_start (u32be)
2320    //   0x20: s_errno (u32be)
2321    //   0x24: s_feature_compat (u32be)
2322    //   0x28: s_feature_incompat (u32be)
2323    //   0x2C: s_feature_ro_compat (u32be)
2324    //   0x30: s_uuid[16]
2325    //   0x40: s_nr_users (u32be)
2326    //   0x44: s_dynsuper (u32be)
2327    //   0x48: s_max_transaction (u32be)
2328    //   0x4C: s_max_trans_data (u32be)
2329    //   0x50: s_checksum_type (u8)
2330    //   0x51: s_padding2[3]
2331    //   0x54: s_padding[42] (u32be array = 168 bytes)
2332    //   0xFC: s_checksum (u32be)
2333    //   0x100: s_users[16*48]
2334
2335    jsb[0x50] = 4; // s_checksum_type = CRC32C
2336
2337    // s_checksum (offset 0xFC, u32be), computed over the 1024-byte on-disk
2338    // jbd2 superblock with the checksum field zeroed.
2339    let jsb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb[..JBD2_SUPERBLOCK_SIZE]);
2340    put_be32(&mut jsb, 0xFC, jsb_csum);
2341
2342    let offset = layout.journal_start_block * EXT4_BLOCK_SIZE as u64;
2343    file.seek(SeekFrom::Start(offset))?;
2344    file.write_all(&jsb)?;
2345
2346    Ok(())
2347}
2348
2349/// Build an ext4 superblock image.
2350fn build_superblock(
2351    layout: &Layout,
2352    group: u32,
2353    padded_primary: bool,
2354) -> Result<Vec<u8>, Ext4Error> {
2355    let mut block = if padded_primary {
2356        vec![0u8; EXT4_BLOCK_SIZE as usize]
2357    } else {
2358        vec![0u8; 1024]
2359    };
2360    let sb_offset = if padded_primary { 1024 } else { 0 };
2361    let sb = &mut block[sb_offset..sb_offset + 1024];
2362
2363    let total_blocks = layout.num_blocks;
2364    let total_inodes = layout.num_groups as u64 * EXT4_INODES_PER_GROUP as u64;
2365
2366    let free_blocks = layout.total_free_blocks();
2367    let free_inodes = layout.total_free_inodes();
2368
2369    // s_inodes_count (0x00, u32)
2370    put_le32(sb, 0x00, total_inodes as u32);
2371    // s_blocks_count_lo (0x04, u32)
2372    put_le32(sb, 0x04, total_blocks as u32);
2373    // s_r_blocks_count_lo (0x08, u32) -- reserved blocks for superuser
2374    put_le32(sb, 0x08, 0);
2375    // s_free_blocks_count_lo (0x0C, u32)
2376    put_le32(sb, 0x0C, free_blocks as u32);
2377    // s_free_inodes_count (0x10, u32)
2378    put_le32(sb, 0x10, free_inodes as u32);
2379    // s_first_data_block (0x14, u32) -- 0 for 4k blocks
2380    put_le32(sb, 0x14, 0);
2381    // s_log_block_size (0x18, u32)
2382    put_le32(sb, 0x18, EXT4_LOG_BLOCK_SIZE);
2383    // s_log_cluster_size (0x1C, u32)
2384    put_le32(sb, 0x1C, EXT4_LOG_BLOCK_SIZE);
2385    // s_blocks_per_group (0x20, u32)
2386    put_le32(sb, 0x20, EXT4_BLOCKS_PER_GROUP);
2387    // s_clusters_per_group (0x24, u32)
2388    put_le32(sb, 0x24, EXT4_BLOCKS_PER_GROUP);
2389    // s_inodes_per_group (0x28, u32)
2390    put_le32(sb, 0x28, EXT4_INODES_PER_GROUP);
2391
2392    // s_mtime (0x2C, u32), s_wtime (0x30, u32)
2393    // Leave as 0.
2394
2395    // s_mnt_count (0x34, u16)
2396    put_le16(sb, 0x34, 0);
2397    // s_max_mnt_count (0x36, u16) -- -1 = no limit
2398    put_le16(sb, 0x36, 0xFFFF);
2399    // s_magic (0x38, u16)
2400    put_le16(sb, 0x38, EXT4_SUPER_MAGIC);
2401    // s_state (0x3A, u16) -- 1 = clean
2402    put_le16(sb, 0x3A, 1);
2403    // s_errors (0x3C, u16) -- 1 = continue
2404    put_le16(sb, 0x3C, 1);
2405    // s_minor_rev_level (0x3E, u16)
2406    put_le16(sb, 0x3E, 0);
2407
2408    // s_lastcheck (0x40, u32), s_checkinterval (0x44, u32)
2409    // Leave as 0.
2410
2411    // s_creator_os (0x48, u32) -- 0 = Linux
2412    put_le32(sb, 0x48, 0);
2413    // s_rev_level (0x4C, u32) -- 1 = dynamic rev
2414    put_le32(sb, 0x4C, 1);
2415
2416    // s_def_resuid (0x50, u16) -- 0
2417    put_le16(sb, 0x50, 0);
2418    // s_def_resgid (0x52, u16) -- 0
2419    put_le16(sb, 0x52, 0);
2420
2421    // --- EXT4_DYNAMIC_REV specific ---
2422    // s_first_ino (0x54, u32)
2423    put_le32(sb, 0x54, EXT4_FIRST_INO);
2424    // s_inode_size (0x58, u16)
2425    put_le16(sb, 0x58, EXT4_INODE_SIZE);
2426    // s_block_group_nr (0x5A, u16) -- block group hosting this superblock
2427    put_le16(sb, 0x5A, group as u16);
2428
2429    // s_feature_compat (0x5C, u32)
2430    put_le32(sb, 0x5C, layout.feature_compat);
2431    // s_feature_incompat (0x60, u32)
2432    put_le32(sb, 0x60, layout.feature_incompat);
2433    // s_feature_ro_compat (0x64, u32)
2434    put_le32(sb, 0x64, layout.feature_ro_compat);
2435
2436    // s_uuid (0x68, 16 bytes)
2437    sb[0x68..0x78].copy_from_slice(&layout.uuid);
2438
2439    // s_volume_name (0x78, 16 bytes) -- leave empty
2440
2441    // s_last_mounted (0x88, 64 bytes) -- leave empty
2442
2443    // s_algorithm_usage_bitmap (0xC8, u32) -- 0
2444    put_le32(sb, 0xC8, 0);
2445
2446    // s_prealloc_blocks (0xCC, u8), s_prealloc_dir_blocks (0xCD, u8)
2447    sb[0xCC] = 0;
2448    sb[0xCD] = 0;
2449
2450    // s_reserved_gdt_blocks (0xCE, u16)
2451    put_le16(sb, 0xCE, layout.reserved_gdt_blocks as u16);
2452
2453    // s_journal_uuid (0xD0, 16 bytes) -- leave zeroed (internal journal)
2454
2455    // s_journal_inum (0xE0, u32)
2456    put_le32(sb, 0xE0, EXT4_JOURNAL_INO);
2457    // s_journal_dev (0xE4, u32) -- 0 (internal)
2458    put_le32(sb, 0xE4, 0);
2459    // s_last_orphan (0xE8, u32)
2460    put_le32(sb, 0xE8, 0);
2461
2462    // s_hash_seed (0xEC, 4*u32 = 16 bytes) -- random
2463    sb[0xEC..0xFC].copy_from_slice(&layout.uuid); // reuse uuid bytes as hash seed
2464
2465    // s_def_hash_version (0xFC, u8) -- 1 = half MD4
2466    sb[0xFC] = 1;
2467    // s_jnl_backup_type (0xFD, u8) -- 1
2468    sb[0xFD] = 1;
2469
2470    // s_desc_size (0xFE, u16)
2471    put_le16(sb, 0xFE, EXT4_DESC_SIZE);
2472
2473    // s_default_mount_opts (0x100, u32) -- 0x000C (user_xattr, acl)
2474    put_le32(sb, 0x100, 0x000C);
2475
2476    // s_first_meta_bg (0x104, u32)
2477    put_le32(sb, 0x104, 0);
2478
2479    // s_mkfs_time (0x108, u32) -- leave 0
2480
2481    // s_jnl_blocks (0x10C, 17*u32 = 68 bytes) -- journal inode i_block backup
2482    // Copy the extent tree from the journal inode.
2483    {
2484        let mut extent_buf = [0u8; 60];
2485        write_extent_tree(
2486            &mut extent_buf,
2487            0,
2488            layout.journal_start_block,
2489            layout.journal_blocks,
2490            "journal",
2491        )?;
2492        // Copy 15 u32s (60 bytes) into s_jnl_blocks
2493        sb[0x10C..0x10C + 60].copy_from_slice(&extent_buf);
2494        // s_jnl_blocks[15] = i_size_lo
2495        let jsize = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
2496        put_le32(sb, 0x10C + 60, jsize as u32);
2497        // s_jnl_blocks[16] = i_size_hi
2498        put_le32(sb, 0x10C + 64, (jsize >> 32) as u32);
2499    }
2500
2501    // --- 64-bit fields ---
2502    // s_blocks_count_hi (0x150, u32)
2503    put_le32(sb, 0x150, (total_blocks >> 32) as u32);
2504    // s_r_blocks_count_hi (0x154, u32)
2505    put_le32(sb, 0x154, 0);
2506    // s_free_blocks_count_hi (0x158, u32)
2507    put_le32(sb, 0x158, (free_blocks >> 32) as u32);
2508
2509    // s_min_extra_isize (0x15C, u16)
2510    put_le16(sb, 0x15C, EXT4_MIN_EXTRA_ISIZE);
2511    // s_want_extra_isize (0x15E, u16)
2512    put_le16(sb, 0x15E, EXT4_MIN_EXTRA_ISIZE);
2513
2514    // s_flags (0x160, u32)
2515    put_le32(sb, 0x160, 0);
2516
2517    // s_log_groups_per_flex (0x174, u8) -- flex_bg disabled
2518    sb[0x174] = 0;
2519
2520    // s_checksum_type (0x175, u8) -- 1 = CRC32C
2521    sb[0x175] = 1;
2522
2523    // s_kbytes_written (0x178, u64) -- 0
2524    // s_snapshot_inum, etc. -- leave zeroed
2525
2526    // Modern images use s_overhead_clusters at 0x248. The test-only legacy formatter preserves
2527    // the pre-0.6.9 field placement at 0x194 so compatibility tests exercise released bytes.
2528    let (overhead_offset, overhead_blocks) =
2529        layout.recorded_overhead_blocks(layout.total_free_blocks());
2530    put_le32(sb, overhead_offset, overhead_blocks as u32);
2531
2532    // s_checksum_seed (0x270, u32) -- crc32c::crc32c_raw(~0, uuid)
2533    // Only used if INCOMPAT_CSUM_SEED is set. For METADATA_CSUM without
2534    // CSUM_SEED, the kernel computes from the UUID. We don't set
2535    // INCOMPAT_CSUM_SEED so leave this zero.
2536    put_le32(sb, 0x270, 0);
2537
2538    // s_encoding (0x27C, u16) -- 0 (no casefold)
2539    put_le16(sb, 0x27C, 0);
2540
2541    // s_checksum (0x3FC, u32) -- CRC32C of sb bytes 0..0x3FC
2542    let sb_csum = superblock_checksum(sb);
2543    put_le32(sb, 0x3FC, sb_csum);
2544
2545    Ok(block)
2546}
2547
2548/// Build the group descriptor table (GDT). Returns a byte vector containing
2549/// all group descriptors (64 bytes each).
2550#[cfg(test)]
2551fn build_gdt(
2552    layout: &Layout,
2553    block_bitmaps: &[Vec<u8>],
2554    inode_bitmaps: &[Vec<u8>],
2555) -> Result<Vec<u8>, Ext4Error> {
2556    let geometry = layout.geometry();
2557    let mut gdt = Vec::with_capacity(layout.num_groups as usize * EXT4_DESC_SIZE as usize);
2558
2559    for g in 0..layout.num_groups {
2560        let desc_stats = GroupDescStats {
2561            free_blocks: layout.group_free_blocks(g),
2562            free_inodes: layout.group_free_inodes(g),
2563            used_dirs: layout.group_used_dirs(g),
2564            block_bitmap_csum: bitmap_checksum(
2565                layout.csum_seed,
2566                &block_bitmaps[g as usize],
2567                EXT4_BLOCK_SIZE as usize,
2568            ),
2569            inode_bitmap_csum: bitmap_checksum(
2570                layout.csum_seed,
2571                &inode_bitmaps[g as usize],
2572                (EXT4_INODES_PER_GROUP / 8) as usize,
2573            ),
2574        };
2575        gdt.extend_from_slice(&build_group_descriptor(
2576            &geometry,
2577            g,
2578            &desc_stats,
2579            layout.csum_seed,
2580        ));
2581    }
2582
2583    Ok(gdt)
2584}
2585
2586/// Write the primary superblock block at the start of the image.
2587fn write_primary_superblock_at(
2588    file: &mut (impl std::io::Write + std::io::Seek),
2589    sb_block: &[u8],
2590) -> Result<(), Ext4Error> {
2591    file.seek(SeekFrom::Start(0))?;
2592    file.write_all(sb_block)?;
2593    Ok(())
2594}
2595
2596//--------------------------------------------------------------------------------------------------
2597// Re-Exports
2598//--------------------------------------------------------------------------------------------------
2599
2600pub use super::format::sparse_super_group;
2601
2602//--------------------------------------------------------------------------------------------------
2603// Tests
2604//--------------------------------------------------------------------------------------------------
2605
2606#[cfg(test)]
2607mod tests {
2608    use super::*;
2609    use std::io::{Read, Seek, SeekFrom};
2610    #[cfg(windows)]
2611    use std::os::windows::fs::MetadataExt;
2612
2613    #[cfg(windows)]
2614    use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SPARSE_FILE;
2615
2616    fn le_u16(bytes: &[u8], offset: usize) -> u16 {
2617        u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
2618    }
2619
2620    fn le_u32(bytes: &[u8], offset: usize) -> u32 {
2621        u32::from_le_bytes([
2622            bytes[offset],
2623            bytes[offset + 1],
2624            bytes[offset + 2],
2625            bytes[offset + 3],
2626        ])
2627    }
2628
2629    fn read_exact_at(path: &Path, offset: u64, len: usize) -> Vec<u8> {
2630        let mut file = std::fs::File::open(path).unwrap();
2631        file.seek(SeekFrom::Start(offset)).unwrap();
2632        let mut bytes = vec![0u8; len];
2633        file.read_exact(&mut bytes).unwrap();
2634        bytes
2635    }
2636
2637    fn assert_backup_superblock(path: &Path, layout: &Layout, group: u32) {
2638        assert!(sparse_super_group(group));
2639        let backup_offset = layout.group_start_block(group) * EXT4_BLOCK_SIZE as u64;
2640        let bytes = read_exact_at(path, backup_offset, 2048);
2641
2642        assert_eq!(le_u16(&bytes, 0x38), EXT4_SUPER_MAGIC);
2643        assert_eq!(le_u16(&bytes, 0x5A), group as u16);
2644        assert_ne!(le_u16(&bytes, 1024 + 0x38), EXT4_SUPER_MAGIC);
2645
2646        let mut sb = bytes[..1024].to_vec();
2647        let stored = le_u32(&sb, 0x3FC);
2648        put_le32(&mut sb, 0x3FC, 0);
2649        let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
2650
2651        assert_eq!(stored, expected);
2652    }
2653
2654    #[test]
2655    fn test_upper_metadata_keeps_tree_ownership_and_directory_mode() {
2656        let copied_up = InodeMetadata {
2657            uid: 0x12345,
2658            gid: 1000,
2659            mode: 0o2755,
2660            mtime: 1_800_000_000,
2661            mtime_nsec: 123,
2662        };
2663        let planned = planned_metadata(&copied_up, true, TreeEncodingMode::Upper);
2664        assert_eq!(
2665            (
2666                planned.uid,
2667                planned.gid,
2668                planned.mode,
2669                planned.mtime,
2670                planned.mtime_nsec
2671            ),
2672            (0x12345, 1000, 0o2755, 1_800_000_000, 123)
2673        );
2674
2675        let empty = InodeMetadata {
2676            uid: 0,
2677            gid: 0,
2678            mode: 0,
2679            mtime: 0,
2680            mtime_nsec: 0,
2681        };
2682        // A directory's 0000 mode is kept (e.g. copied up from a lower
2683        // layer); only an empty file mode still defaults to 0644.
2684        assert_eq!(
2685            planned_metadata(&empty, true, TreeEncodingMode::Upper).mode,
2686            0
2687        );
2688        assert_eq!(
2689            planned_metadata(&empty, false, TreeEncodingMode::Upper).mode,
2690            0o644
2691        );
2692    }
2693
2694    #[test]
2695    fn test_format_creates_file_of_correct_size() {
2696        let dir = tempfile::tempdir().unwrap();
2697        let path = dir.path().join("test.ext4");
2698
2699        let size: u64 = 256 * 1024 * 1024; // 256 MiB
2700        let opts = Ext4FormatOptions {
2701            size_bytes: size,
2702            journal_blocks: 4096, // 16 MiB journal
2703        };
2704
2705        format_ext4(&path, &opts).unwrap();
2706
2707        let meta = std::fs::metadata(&path).unwrap();
2708        assert_eq!(meta.len(), size);
2709    }
2710
2711    #[test]
2712    fn test_inode_allocation_crosses_block_group_boundary() {
2713        let opts = Ext4FormatOptions {
2714            size_bytes: 256 * 1024 * 1024,
2715            journal_blocks: 4096,
2716        };
2717        let layout = Layout::compute(&opts).unwrap();
2718        let plan = BitmapPlan {
2719            block_extents: Vec::new(),
2720            max_used_inode: EXT4_INODES_PER_GROUP + 1,
2721            group_used_dirs: vec![1, 0],
2722        };
2723
2724        let first_bitmap = build_inode_bitmap_for_plan(&layout, &plan, 0);
2725        let second_bitmap = build_inode_bitmap_for_plan(&layout, &plan, 1);
2726        assert!(
2727            first_bitmap[..(EXT4_INODES_PER_GROUP / 8) as usize]
2728                .iter()
2729                .all(|byte| *byte == 0xff)
2730        );
2731        assert_eq!(second_bitmap[0] & 0x01, 0x01);
2732        assert_eq!(second_bitmap[0] & 0xfe, 0);
2733
2734        assert_eq!(
2735            inode_offset(&layout, EXT4_INODES_PER_GROUP + 1),
2736            layout.group_inode_table_block(1) * EXT4_BLOCK_SIZE as u64
2737        );
2738    }
2739
2740    #[test]
2741    fn test_allocator_builds_depth_one_extent_tree_across_groups() {
2742        let opts = Ext4FormatOptions {
2743            size_bytes: 1024 * 1024 * 1024,
2744            journal_blocks: 4096,
2745        };
2746        let layout = Layout::compute(&opts).unwrap();
2747        let mut allocator = DataAllocator::new(&layout);
2748        let block_count = 150_000;
2749        let data_extents = allocator
2750            .allocate_extents(block_count, "/large-layer")
2751            .unwrap();
2752        assert!(data_extents.len() > INLINE_EXTENT_CAPACITY);
2753        assert_eq!(
2754            data_extents
2755                .iter()
2756                .map(|extent| extent.block_count)
2757                .sum::<u32>(),
2758            block_count
2759        );
2760        for pair in data_extents.windows(2) {
2761            assert_eq!(
2762                pair[1].logical_block,
2763                pair[0].logical_block + pair[0].block_count
2764            );
2765        }
2766
2767        let extent_tree_block = allocator.allocate_block("/large-layer").unwrap();
2768        let plan = NodePlan {
2769            inode: EXT4_FIRST_INO,
2770            path: "/large-layer".to_string(),
2771            metadata: InodeMetadata::default(),
2772            links_count: 1,
2773            kind: NodeKind::RegularFile {
2774                data: FileData::Memory(Vec::new()),
2775            },
2776            block_count,
2777            data_extents,
2778            extent_tree_block: Some(extent_tree_block),
2779            xattrs: Vec::new(),
2780            inline_xattrs: None,
2781            xattr_block: None,
2782        };
2783        let mut inode_block = [0u8; 60];
2784        write_extent_plan(&mut inode_block, 0, &plan).unwrap();
2785        assert_eq!(le_u16(&inode_block, 0), EXT4_EH_MAGIC);
2786        assert_eq!(le_u16(&inode_block, 2), 1);
2787        assert_eq!(le_u16(&inode_block, 6), 1);
2788
2789        let leaf = build_extent_leaf_block(&layout, &plan).unwrap();
2790        assert_eq!(le_u16(&leaf, 0), EXT4_EH_MAGIC);
2791        assert_eq!(le_u16(&leaf, 2) as usize, plan.data_extents.len());
2792        assert_eq!(le_u16(&leaf, 6), 0);
2793        let tail = EXT4_BLOCK_SIZE as usize - 4;
2794        assert_eq!(
2795            le_u32(&leaf, tail),
2796            dir_block_checksum(layout.csum_seed, plan.inode, 0, &leaf[..tail])
2797        );
2798    }
2799
2800    #[test]
2801    fn test_xattr_block_is_sorted_and_checksummed() {
2802        let opts = Ext4FormatOptions {
2803            size_bytes: 256 * 1024 * 1024,
2804            journal_blocks: 4096,
2805        };
2806        let layout = Layout::compute(&opts).unwrap();
2807        let block_number = layout.group_data_start_block(1);
2808        let xattrs = vec![
2809            Xattr {
2810                name: b"security.capability".to_vec(),
2811                value: (0u8..20).collect(),
2812            },
2813            Xattr {
2814                name: b"user.owner".to_vec(),
2815                value: b"microsandbox".to_vec(),
2816            },
2817        ];
2818
2819        let block = build_xattr_block(&layout, block_number, &xattrs, "/bin/tool").unwrap();
2820        assert_eq!(le_u32(&block, 0), EXT4_XATTR_MAGIC);
2821        assert_eq!(le_u32(&block, 4), 1);
2822        assert_eq!(le_u32(&block, 8), 1);
2823        assert_eq!(block[EXT4_XATTR_HEADER_SIZE + 1], 1);
2824        let second_entry = EXT4_XATTR_HEADER_SIZE + align_four(EXT4_XATTR_ENTRY_SIZE + 5);
2825        assert_eq!(block[second_entry + 1], 6);
2826
2827        let stored_checksum = le_u32(&block, 16);
2828        let mut checksum_input = block.clone();
2829        put_le32(&mut checksum_input, 16, 0);
2830        assert_eq!(
2831            stored_checksum,
2832            xattr_block_checksum(layout.csum_seed, block_number, &checksum_input)
2833        );
2834    }
2835
2836    #[test]
2837    fn test_small_xattr_is_encoded_in_inode_body() {
2838        let xattrs = vec![Xattr {
2839            name: b"security.capability".to_vec(),
2840            value: (0u8..20).collect(),
2841        }];
2842
2843        let bytes = build_inline_xattrs(&xattrs, "/bin/tool").unwrap().unwrap();
2844        assert_eq!(le_u32(&bytes, 0), EXT4_XATTR_MAGIC);
2845        assert_eq!(bytes[5], 6);
2846        assert_eq!(&bytes[20..30], b"capability");
2847        assert_eq!(le_u32(&bytes, 16), 0);
2848    }
2849
2850    #[test]
2851    fn test_format_too_small() {
2852        let dir = tempfile::tempdir().unwrap();
2853        let path = dir.path().join("tiny.ext4");
2854
2855        let opts = Ext4FormatOptions {
2856            size_bytes: 4096, // way too small
2857            journal_blocks: 16384,
2858        };
2859
2860        let result = format_ext4(&path, &opts);
2861        assert!(matches!(result, Err(Ext4Error::TooSmall)));
2862    }
2863
2864    #[test]
2865    fn test_layout_rejects_unaligned_size() {
2866        let opts = Ext4FormatOptions {
2867            size_bytes: DEFAULT_SIZE_BYTES + 1,
2868            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2869        };
2870
2871        let result = Layout::compute(&opts);
2872        assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
2873    }
2874
2875    #[test]
2876    fn test_layout_rejects_tiny_final_group() {
2877        let opts = Ext4FormatOptions {
2878            size_bytes: 128 * 1024 * 1024 + EXT4_BLOCK_SIZE as u64,
2879            journal_blocks: 4096,
2880        };
2881
2882        let result = Layout::compute(&opts);
2883        assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
2884    }
2885
2886    #[test]
2887    fn test_layout_rejects_size_beyond_32_bit_block_addresses() {
2888        let opts = Ext4FormatOptions {
2889            size_bytes: (MAX_BLOCKS + 1) * EXT4_BLOCK_SIZE as u64,
2890            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2891        };
2892
2893        let result = Layout::compute(&opts);
2894        assert!(matches!(result, Err(Ext4Error::TooLarge { .. })));
2895    }
2896
2897    #[test]
2898    fn test_layout_uses_actual_group_count_beyond_four_gib() {
2899        let opts = Ext4FormatOptions {
2900            size_bytes: 8 * 1024 * 1024 * 1024,
2901            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2902        };
2903
2904        let layout = Layout::compute(&opts).unwrap();
2905
2906        assert_eq!(layout.num_blocks, opts.size_bytes / EXT4_BLOCK_SIZE as u64);
2907        assert_eq!(layout.num_groups, 64);
2908        assert_eq!(layout.gdt_blocks, 1);
2909    }
2910
2911    #[test]
2912    fn test_format_default_options() {
2913        let dir = tempfile::tempdir().unwrap();
2914        let path = dir.path().join("default.ext4");
2915
2916        let opts = Ext4FormatOptions::default();
2917        format_ext4(&path, &opts).unwrap();
2918
2919        let meta = std::fs::metadata(&path).unwrap();
2920        assert_eq!(meta.len(), DEFAULT_SIZE_BYTES);
2921    }
2922
2923    #[test]
2924    fn test_superblock_magic() {
2925        let dir = tempfile::tempdir().unwrap();
2926        let path = dir.path().join("magic.ext4");
2927
2928        let opts = Ext4FormatOptions {
2929            size_bytes: 256 * 1024 * 1024,
2930            journal_blocks: 4096,
2931        };
2932        format_ext4(&path, &opts).unwrap();
2933
2934        // Read back and check magic number at offset 1024+0x38
2935        let data = std::fs::read(&path).unwrap();
2936        let magic = u16::from_le_bytes([data[1024 + 0x38], data[1024 + 0x39]]);
2937        assert_eq!(magic, EXT4_SUPER_MAGIC);
2938    }
2939
2940    #[test]
2941    fn test_journal_magic() {
2942        let dir = tempfile::tempdir().unwrap();
2943        let path = dir.path().join("journal.ext4");
2944
2945        let opts = Ext4FormatOptions {
2946            size_bytes: 256 * 1024 * 1024,
2947            journal_blocks: 4096,
2948        };
2949        format_ext4(&path, &opts).unwrap();
2950
2951        let layout = Layout::compute(&opts).unwrap();
2952        let data = std::fs::read(&path).unwrap();
2953
2954        // Journal superblock is at journal_start_block * 4096
2955        let jsb_offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
2956        let magic = u32::from_be_bytes([
2957            data[jsb_offset],
2958            data[jsb_offset + 1],
2959            data[jsb_offset + 2],
2960            data[jsb_offset + 3],
2961        ]);
2962        assert_eq!(magic, JBD2_MAGIC);
2963    }
2964
2965    #[test]
2966    fn test_root_dir_inode_exists() {
2967        let dir = tempfile::tempdir().unwrap();
2968        let path = dir.path().join("rootdir.ext4");
2969
2970        let opts = Ext4FormatOptions {
2971            size_bytes: 256 * 1024 * 1024,
2972            journal_blocks: 4096,
2973        };
2974        format_ext4(&path, &opts).unwrap();
2975
2976        let layout = Layout::compute(&opts).unwrap();
2977        let data = std::fs::read(&path).unwrap();
2978
2979        // Root inode at inode_table_block * 4096 + (2-1)*256
2980        let inode_offset = layout.group_inode_table_block(0) as usize * EXT4_BLOCK_SIZE as usize
2981            + (EXT4_INODE_SIZE as usize);
2982        let mode = u16::from_le_bytes([data[inode_offset], data[inode_offset + 1]]);
2983        assert_eq!(mode, S_IFDIR | 0o755);
2984    }
2985
2986    #[test]
2987    fn test_backup_group_bitmap_starts_after_backup_metadata() {
2988        let opts = Ext4FormatOptions {
2989            size_bytes: 256 * 1024 * 1024,
2990            journal_blocks: 4096,
2991        };
2992        let layout = Layout::compute(&opts).unwrap();
2993        let block_bitmaps = build_block_bitmaps(&layout);
2994        let inode_bitmaps = build_inode_bitmaps(&layout);
2995        let gdt = build_gdt(&layout, &block_bitmaps, &inode_bitmaps).unwrap();
2996
2997        let desc = &gdt[EXT4_DESC_SIZE as usize..(2 * EXT4_DESC_SIZE as usize)];
2998        let block_bitmap = u32::from_le_bytes([desc[0], desc[1], desc[2], desc[3]]) as u64;
2999        let group_start = layout.group_start_block(1);
3000
3001        assert_eq!(block_bitmap, layout.group_block_bitmap_block(1));
3002        assert!(block_bitmap > group_start + layout.gdt_blocks as u64 - 1);
3003    }
3004
3005    #[test]
3006    fn test_backup_superblock_is_written_at_backup_group_start() {
3007        let dir = tempfile::tempdir().unwrap();
3008        let path = dir.path().join("backup-super.ext4");
3009        let opts = Ext4FormatOptions {
3010            size_bytes: 256 * 1024 * 1024,
3011            journal_blocks: 4096,
3012        };
3013
3014        format_ext4(&path, &opts).unwrap();
3015
3016        let layout = Layout::compute(&opts).unwrap();
3017        assert_backup_superblock(&path, &layout, 1);
3018    }
3019
3020    #[test]
3021    fn test_backup_superblock_is_group_specific_in_sixteen_gib_image() {
3022        let dir = tempfile::tempdir().unwrap();
3023        let path = dir.path().join("backup-super-16g.ext4");
3024        let opts = Ext4FormatOptions {
3025            size_bytes: 16 * 1024 * 1024 * 1024,
3026            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
3027        };
3028
3029        format_ext4(&path, &opts).unwrap();
3030
3031        let layout = Layout::compute(&opts).unwrap();
3032        assert_eq!(layout.num_groups, 128);
3033        assert_backup_superblock(&path, &layout, 81);
3034    }
3035
3036    #[test]
3037    fn test_format_sparse_image_larger_than_four_gib() {
3038        let dir = tempfile::tempdir().unwrap();
3039        let path = dir.path().join("large.ext4");
3040        let opts = Ext4FormatOptions {
3041            size_bytes: 8 * 1024 * 1024 * 1024,
3042            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
3043        };
3044
3045        format_ext4(&path, &opts).unwrap();
3046
3047        let meta = std::fs::metadata(&path).unwrap();
3048        assert_eq!(meta.len(), opts.size_bytes);
3049
3050        let layout = Layout::compute(&opts).unwrap();
3051        let sb = read_exact_at(&path, 1024, 1024);
3052        let blocks = le_u32(&sb, 0x04) as u64 | ((le_u32(&sb, 0x150) as u64) << 32);
3053        let inodes = le_u32(&sb, 0x00);
3054        assert_eq!(blocks, layout.num_blocks);
3055        assert_eq!(inodes, layout.num_groups * EXT4_INODES_PER_GROUP);
3056
3057        let desc_offset = EXT4_BLOCK_SIZE as u64 + 63 * EXT4_DESC_SIZE as u64;
3058        let desc = read_exact_at(&path, desc_offset, EXT4_DESC_SIZE as usize);
3059        let block_bitmap = le_u32(&desc, 0x00) as u64 | ((le_u32(&desc, 0x20) as u64) << 32);
3060        let inode_bitmap = le_u32(&desc, 0x04) as u64 | ((le_u32(&desc, 0x24) as u64) << 32);
3061
3062        assert_eq!(block_bitmap, layout.group_block_bitmap_block(63));
3063        assert_eq!(inode_bitmap, layout.group_inode_bitmap_block(63));
3064    }
3065
3066    #[cfg(windows)]
3067    #[test]
3068    fn test_format_marks_image_sparse_on_windows() {
3069        let dir = tempfile::tempdir().unwrap();
3070        let path = dir.path().join("sparse.ext4");
3071        let opts = Ext4FormatOptions {
3072            size_bytes: 256 * 1024 * 1024,
3073            journal_blocks: 4096,
3074        };
3075
3076        format_ext4(&path, &opts).unwrap();
3077
3078        let meta = std::fs::metadata(&path).unwrap();
3079        assert_eq!(meta.len(), opts.size_bytes);
3080        assert_ne!(meta.file_attributes() & FILE_ATTRIBUTE_SPARSE_FILE, 0);
3081    }
3082
3083    #[test]
3084    fn test_inode_bitmap_padding_is_marked_used() {
3085        let layout = Layout::compute(&Ext4FormatOptions {
3086            size_bytes: 256 * 1024 * 1024,
3087            journal_blocks: 4096,
3088        })
3089        .unwrap();
3090
3091        let bitmap = build_inode_bitmap(&layout, 0);
3092        for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
3093            assert_ne!(bitmap[(bit / 8) as usize] & (1 << (bit % 8)), 0);
3094        }
3095    }
3096
3097    #[test]
3098    fn test_journal_superblock_checksum_matches_contents() {
3099        let dir = tempfile::tempdir().unwrap();
3100        let path = dir.path().join("journal-csum.ext4");
3101        let opts = Ext4FormatOptions {
3102            size_bytes: 256 * 1024 * 1024,
3103            journal_blocks: 4096,
3104        };
3105
3106        format_ext4(&path, &opts).unwrap();
3107
3108        let layout = Layout::compute(&opts).unwrap();
3109        let data = std::fs::read(&path).unwrap();
3110        let offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
3111        let mut jsb = data[offset..offset + JBD2_SUPERBLOCK_SIZE].to_vec();
3112        let stored = u32::from_be_bytes([jsb[0xFC], jsb[0xFD], jsb[0xFE], jsb[0xFF]]);
3113
3114        jsb[0xFC..0x100].fill(0);
3115        let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb);
3116
3117        assert_eq!(stored, expected);
3118    }
3119
3120    #[test]
3121    fn test_root_dir_checksum_matches_contents() {
3122        let dir = tempfile::tempdir().unwrap();
3123        let path = dir.path().join("rootdir-csum.ext4");
3124        let opts = Ext4FormatOptions {
3125            size_bytes: 256 * 1024 * 1024,
3126            journal_blocks: 4096,
3127        };
3128
3129        format_ext4(&path, &opts).unwrap();
3130
3131        let layout = Layout::compute(&opts).unwrap();
3132        let data = std::fs::read(&path).unwrap();
3133        let sb = &data[1024..2048];
3134        let uuid = &sb[0x68..0x78];
3135        let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, uuid);
3136        let block_offset = layout.first_data_block as usize * EXT4_BLOCK_SIZE as usize;
3137        let tail_offset = block_offset + EXT4_BLOCK_SIZE as usize - 12;
3138        let stored = u32::from_le_bytes([
3139            data[tail_offset + 8],
3140            data[tail_offset + 9],
3141            data[tail_offset + 10],
3142            data[tail_offset + 11],
3143        ]);
3144        let expected = dir_block_checksum(
3145            csum_seed,
3146            EXT4_ROOT_INO,
3147            0,
3148            &data[block_offset..tail_offset],
3149        );
3150
3151        assert_eq!(stored, expected);
3152    }
3153}