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        TreeEncodingMode::Upper => InodeMetadata {
1093            uid: 0,
1094            gid: 0,
1095            mode: if directory {
1096                normalize_dir_permissions(metadata.mode)
1097            } else {
1098                normalize_file_permissions(metadata.mode)
1099            },
1100            mtime: 0,
1101            mtime_nsec: 0,
1102        },
1103        TreeEncodingMode::Rootfs => InodeMetadata {
1104            uid: metadata.uid,
1105            gid: metadata.gid,
1106            mode: metadata.mode & 0o7777,
1107            mtime: metadata.mtime,
1108            mtime_nsec: metadata.mtime_nsec,
1109        },
1110    }
1111}
1112
1113fn child_path(parent: &str, name: &[u8]) -> String {
1114    let name = String::from_utf8_lossy(name);
1115    if parent == "/" {
1116        format!("/{name}")
1117    } else {
1118        format!("{parent}/{name}")
1119    }
1120}
1121
1122fn normalize_file_permissions(mode: u16) -> u16 {
1123    let perms = mode & 0o7777;
1124    if perms == 0 { 0o644 } else { perms }
1125}
1126
1127fn hardlink_matches_plan(
1128    plan: &NodePlan,
1129    file: &crate::tree::RegularFileNode,
1130    encoding_mode: TreeEncodingMode,
1131) -> bool {
1132    let metadata = &plan.metadata;
1133    let candidate = planned_metadata(&file.metadata, false, encoding_mode);
1134    let metadata_matches = metadata.uid == candidate.uid
1135        && metadata.gid == candidate.gid
1136        && metadata.mode == candidate.mode
1137        && metadata.mtime == candidate.mtime
1138        && metadata.mtime_nsec == candidate.mtime_nsec;
1139    let xattrs_match = plan.xattrs.len() == file.xattrs.len()
1140        && plan.xattrs.iter().all(|left| {
1141            file.xattrs
1142                .iter()
1143                .any(|right| left.name == right.name && left.value == right.value)
1144        });
1145    let data_matches = match (&plan.kind, &file.data) {
1146        (NodeKind::RegularFile { data: left }, right) => same_file_data(left, right),
1147        _ => false,
1148    };
1149    metadata_matches && xattrs_match && data_matches
1150}
1151
1152fn same_file_data(left: &FileData, right: &FileData) -> bool {
1153    match (left, right) {
1154        (FileData::Memory(left), FileData::Memory(right)) => left == right,
1155        (FileData::SharedMemory(left), FileData::SharedMemory(right)) => {
1156            std::sync::Arc::ptr_eq(left, right) || left.as_ref() == right.as_ref()
1157        }
1158        (FileData::Memory(left), FileData::SharedMemory(right))
1159        | (FileData::SharedMemory(right), FileData::Memory(left)) => {
1160            left.as_slice() == right.as_ref()
1161        }
1162        (
1163            FileData::Spool {
1164                spool: left_spool,
1165                offset: left_offset,
1166                len: left_len,
1167            },
1168            FileData::Spool {
1169                spool: right_spool,
1170                offset: right_offset,
1171                len: right_len,
1172            },
1173        ) => {
1174            std::sync::Arc::ptr_eq(left_spool, right_spool)
1175                && left_offset == right_offset
1176                && left_len == right_len
1177        }
1178        _ => false,
1179    }
1180}
1181
1182fn normalize_dir_permissions(mode: u16) -> u16 {
1183    let perms = mode & 0o7777;
1184    if perms == 0 { 0o755 } else { perms }
1185}
1186
1187fn blocks_for_len(len: usize) -> u32 {
1188    if len == 0 {
1189        0
1190    } else {
1191        (len as u64).div_ceil(EXT4_BLOCK_SIZE as u64) as u32
1192    }
1193}
1194
1195fn validate_node_extents(plans: &[NodePlan]) -> Result<(), Ext4Error> {
1196    for plan in plans {
1197        if plan.data_extents.len() > EXTENT_BLOCK_CAPACITY {
1198            return Err(Ext4Error::Layout(format!(
1199                "'{}' needs {} extents, but a depth-one ext4 extent tree holds at most {EXTENT_BLOCK_CAPACITY}",
1200                plan.path,
1201                plan.data_extents.len()
1202            )));
1203        }
1204        for extent in &plan.data_extents {
1205            validate_extent_block_count(extent.block_count, &plan.path)?;
1206        }
1207    }
1208
1209    Ok(())
1210}
1211
1212fn validate_extent_block_count(block_count: u32, label: &str) -> Result<(), Ext4Error> {
1213    if block_count > MAX_INITIALIZED_EXTENT_BLOCKS {
1214        return Err(Ext4Error::Layout(format!(
1215            "'{label}' needs {block_count} blocks but this formatter currently supports one initialized extent of at most {MAX_INITIALIZED_EXTENT_BLOCKS} blocks"
1216        )));
1217    }
1218
1219    Ok(())
1220}
1221
1222fn split_physical_range(start: u64, blocks: u32) -> Vec<AllocatedExtent> {
1223    let mut extents = Vec::new();
1224    let mut remaining = blocks;
1225    let mut logical_block = 0u32;
1226    let mut physical_start = start;
1227    while remaining > 0 {
1228        let block_count = remaining.min(MAX_INITIALIZED_EXTENT_BLOCKS);
1229        extents.push(AllocatedExtent {
1230            logical_block,
1231            physical_start,
1232            block_count,
1233        });
1234        logical_block += block_count;
1235        physical_start += u64::from(block_count);
1236        remaining -= block_count;
1237    }
1238    extents
1239}
1240
1241fn build_directory_data(
1242    dir_inode: u32,
1243    parent_inode: u32,
1244    children: &[DirEntrySpec],
1245    path: &str,
1246) -> Result<Vec<u8>, Ext4Error> {
1247    let mut entries = Vec::with_capacity(children.len() + 2);
1248    entries.push(DirEntrySpec {
1249        inode: dir_inode,
1250        file_type: EXT4_FT_DIR,
1251        name: b".".to_vec(),
1252    });
1253    entries.push(DirEntrySpec {
1254        inode: parent_inode,
1255        file_type: EXT4_FT_DIR,
1256        name: b"..".to_vec(),
1257    });
1258    entries.extend(children.iter().map(|entry| DirEntrySpec {
1259        inode: entry.inode,
1260        file_type: entry.file_type,
1261        name: entry.name.clone(),
1262    }));
1263
1264    let mut blocks = Vec::new();
1265    let mut index = 0usize;
1266    while index < entries.len() {
1267        let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1268        let mut pos = 0usize;
1269        let data_limit = EXT4_BLOCK_SIZE as usize - 12;
1270        let block_start = index;
1271
1272        while index < entries.len() {
1273            let min_len = dir_entry_len(entries[index].name.len());
1274            let needed = if pos == 0 { min_len } else { pos + min_len };
1275            if needed > data_limit {
1276                if pos == 0 {
1277                    return Err(Ext4Error::Layout(format!(
1278                        "directory entry too large for '{path}'"
1279                    )));
1280                }
1281                break;
1282            }
1283            pos += min_len;
1284            index += 1;
1285        }
1286
1287        let mut write_pos = 0usize;
1288        for (entry_index, entry) in entries[block_start..index].iter().enumerate() {
1289            let is_last = entry_index + 1 == index - block_start;
1290            let rec_len = if is_last {
1291                (data_limit - write_pos) as u16
1292            } else {
1293                dir_entry_len(entry.name.len()) as u16
1294            };
1295            put_le32(&mut block, write_pos, entry.inode);
1296            put_le16(&mut block, write_pos + 4, rec_len);
1297            block[write_pos + 6] = entry.name.len() as u8;
1298            block[write_pos + 7] = entry.file_type;
1299            block[write_pos + 8..write_pos + 8 + entry.name.len()].copy_from_slice(&entry.name);
1300            write_pos += rec_len as usize;
1301        }
1302
1303        let tail = data_limit;
1304        put_le32(&mut block, tail, 0);
1305        put_le16(&mut block, tail + 4, 12);
1306        block[tail + 6] = 0;
1307        block[tail + 7] = 0xDE;
1308        blocks.extend_from_slice(&block);
1309    }
1310
1311    if blocks.is_empty() {
1312        let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
1313        put_le32(&mut block, 0, dir_inode);
1314        put_le16(&mut block, 4, 12);
1315        block[6] = 1;
1316        block[7] = EXT4_FT_DIR;
1317        block[8] = b'.';
1318        put_le32(&mut block, 12, parent_inode);
1319        put_le16(&mut block, 16, (EXT4_BLOCK_SIZE - 24) as u16);
1320        block[18] = 2;
1321        block[19] = EXT4_FT_DIR;
1322        block[20] = b'.';
1323        block[21] = b'.';
1324        let tail = EXT4_BLOCK_SIZE as usize - 12;
1325        put_le32(&mut block, tail, 0);
1326        put_le16(&mut block, tail + 4, 12);
1327        block[tail + 7] = 0xDE;
1328        blocks = block;
1329    }
1330
1331    Ok(blocks)
1332}
1333
1334fn dir_entry_len(name_len: usize) -> usize {
1335    (8 + name_len + 3) & !3
1336}
1337
1338fn allocate_node_data(allocator: &mut DataAllocator, plan: &mut NodePlan) -> Result<(), Ext4Error> {
1339    if plan.block_count > 0 {
1340        plan.data_extents = allocator.allocate_extents(plan.block_count, &plan.path)?;
1341        if plan.data_extents.len() > INLINE_EXTENT_CAPACITY {
1342            plan.extent_tree_block = Some(allocator.allocate_block(&plan.path)?);
1343        }
1344    }
1345
1346    if !plan.xattrs.is_empty() {
1347        plan.inline_xattrs = build_inline_xattrs(&plan.xattrs, &plan.path)?;
1348        if plan.inline_xattrs.is_none() {
1349            plan.xattr_block = Some(allocator.allocate_block(&plan.path)?);
1350        }
1351    }
1352    Ok(())
1353}
1354
1355impl DataAllocator {
1356    fn new(layout: &Layout) -> Self {
1357        let mut regions = Vec::new();
1358        for group in 0..layout.num_groups {
1359            let group_start = layout.group_start_block(group);
1360            let group_end = group_start + layout.blocks_in_group(group) as u64;
1361            let start = layout.group_data_start_block(group);
1362            if start < group_end {
1363                regions.push((start, (group_end - start) as u32));
1364            }
1365        }
1366        Self { regions }
1367    }
1368
1369    fn allocate_extents(
1370        &mut self,
1371        blocks: u32,
1372        path: &str,
1373    ) -> Result<Vec<AllocatedExtent>, Ext4Error> {
1374        let mut extents = Vec::new();
1375        let mut remaining = blocks;
1376        let mut logical_block = 0u32;
1377
1378        for region in &mut self.regions {
1379            while remaining > 0 && region.1 > 0 {
1380                let block_count = remaining.min(region.1).min(MAX_INITIALIZED_EXTENT_BLOCKS);
1381                extents.push(AllocatedExtent {
1382                    logical_block,
1383                    physical_start: region.0,
1384                    block_count,
1385                });
1386                region.0 += u64::from(block_count);
1387                region.1 -= block_count;
1388                remaining -= block_count;
1389                logical_block += block_count;
1390            }
1391            if remaining == 0 {
1392                return Ok(extents);
1393            }
1394        }
1395
1396        Err(Ext4Error::Layout(format!(
1397            "not enough space in ext4 image for '{path}'"
1398        )))
1399    }
1400
1401    fn allocate_block(&mut self, path: &str) -> Result<u64, Ext4Error> {
1402        self.allocate_extents(1, path)?
1403            .first()
1404            .map(|extent| extent.physical_start)
1405            .ok_or_else(|| {
1406                Ext4Error::Layout(format!("could not allocate extent tree for '{path}'"))
1407            })
1408    }
1409}
1410
1411fn build_block_bitmap_for_plan(layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
1412    let mut bitmap = build_block_bitmap_base(&layout.geometry(), group);
1413    let group_start = layout.group_start_block(group);
1414    let group_end = group_start + layout.blocks_in_group(group) as u64;
1415
1416    for (start, len) in &plan.block_extents {
1417        let extent_start = *start;
1418        let extent_end = extent_start + *len as u64;
1419        let overlap_start = extent_start.max(group_start);
1420        let overlap_end = extent_end.min(group_end);
1421        if overlap_start < overlap_end {
1422            for block in overlap_start..overlap_end {
1423                let bit = block - group_start;
1424                bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1425            }
1426        }
1427    }
1428
1429    bitmap
1430}
1431
1432fn build_inode_bitmap_for_plan(_layout: &Layout, plan: &BitmapPlan, group: u32) -> Vec<u8> {
1433    let first_inode = group * EXT4_INODES_PER_GROUP + 1;
1434    let last_inode = first_inode + EXT4_INODES_PER_GROUP - 1;
1435    let used_inodes = if plan.max_used_inode < first_inode {
1436        0
1437    } else {
1438        plan.max_used_inode.min(last_inode) - first_inode + 1
1439    };
1440    build_inode_bitmap_base(used_inodes)
1441}
1442
1443fn compute_fs_stats(layout: &Layout, plan: &BitmapPlan) -> FsStats {
1444    let mut group_free_blocks = Vec::with_capacity(layout.num_groups as usize);
1445    let mut block_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
1446    let mut inode_bitmap_checksums = Vec::with_capacity(layout.num_groups as usize);
1447
1448    let mut total_free_blocks = 0u64;
1449    for group in 0..layout.num_groups {
1450        let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
1451        let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
1452        let blocks_in_group = layout.blocks_in_group(group) as usize;
1453        let used = count_used_bits(&block_bitmap, blocks_in_group);
1454        let free = blocks_in_group.saturating_sub(used) as u32;
1455        group_free_blocks.push(free);
1456        block_bitmap_checksums.push(bitmap_checksum(
1457            layout.csum_seed,
1458            &block_bitmap,
1459            EXT4_BLOCK_SIZE as usize,
1460        ));
1461        inode_bitmap_checksums.push(bitmap_checksum(
1462            layout.csum_seed,
1463            &inode_bitmap,
1464            (EXT4_INODES_PER_GROUP / 8) as usize,
1465        ));
1466        total_free_blocks += free as u64;
1467    }
1468
1469    let group_free_inodes = (0..layout.num_groups)
1470        .map(|group| {
1471            let first_inode = group * EXT4_INODES_PER_GROUP + 1;
1472            let last_inode = first_inode + EXT4_INODES_PER_GROUP - 1;
1473            let used_inodes = if plan.max_used_inode < first_inode {
1474                0
1475            } else {
1476                plan.max_used_inode.min(last_inode) - first_inode + 1
1477            };
1478            EXT4_INODES_PER_GROUP - used_inodes
1479        })
1480        .collect::<Vec<_>>();
1481    let total_free_inodes = group_free_inodes.iter().map(|count| *count as u64).sum();
1482
1483    let group_used_dirs = plan.group_used_dirs.clone();
1484
1485    FsStats {
1486        group_free_blocks,
1487        group_free_inodes,
1488        group_used_dirs,
1489        block_bitmap_checksums,
1490        inode_bitmap_checksums,
1491        total_free_blocks,
1492        total_free_inodes,
1493        overhead_blocks: layout.total_overhead_blocks(),
1494    }
1495}
1496
1497fn write_tree_data(
1498    file: &mut (impl std::io::Write + std::io::Seek),
1499    layout: &Layout,
1500    plans: &[NodePlan],
1501) -> Result<(), Ext4Error> {
1502    for plan in plans {
1503        match &plan.kind {
1504            NodeKind::Directory { data, .. } => {
1505                let mut bytes = data.clone();
1506                update_dir_block_checksums(layout.csum_seed, plan.inode, &mut bytes);
1507                write_bytes_to_extents(file, &plan.data_extents, &bytes)?;
1508            }
1509            NodeKind::RegularFile { data } => {
1510                write_file_data(file, &plan.data_extents, data)?;
1511            }
1512            NodeKind::Symlink { target, inline } => {
1513                if !inline {
1514                    write_bytes_to_extents(file, &plan.data_extents, target)?;
1515                }
1516            }
1517            NodeKind::CharDevice { .. }
1518            | NodeKind::BlockDevice { .. }
1519            | NodeKind::Fifo
1520            | NodeKind::Socket => {}
1521        }
1522
1523        if let Some(block) = plan.extent_tree_block {
1524            let bytes = build_extent_leaf_block(layout, plan)?;
1525            write_extent_bytes(file, block, &bytes)?;
1526        }
1527        if let Some(block) = plan.xattr_block {
1528            let bytes = build_xattr_block(layout, block, &plan.xattrs, &plan.path)?;
1529            write_extent_bytes(file, block, &bytes)?;
1530        }
1531    }
1532
1533    Ok(())
1534}
1535
1536fn write_file_data(
1537    file: &mut (impl std::io::Write + std::io::Seek),
1538    extents: &[AllocatedExtent],
1539    data: &FileData,
1540) -> Result<(), Ext4Error> {
1541    let mut data_offset = 0usize;
1542    for extent in extents {
1543        let remaining = data.len().saturating_sub(data_offset);
1544        let extent_bytes = extent.block_count as usize * EXT4_BLOCK_SIZE as usize;
1545        let write_len = remaining.min(extent_bytes);
1546        file.seek(SeekFrom::Start(
1547            extent.physical_start * EXT4_BLOCK_SIZE as u64,
1548        ))?;
1549        data.write_range(data_offset, write_len, file)?;
1550        write_zero_padding(file, extent_bytes - write_len)?;
1551        data_offset += write_len;
1552    }
1553
1554    debug_assert_eq!(data_offset, data.len());
1555
1556    Ok(())
1557}
1558
1559fn write_bytes_to_extents(
1560    file: &mut (impl std::io::Write + std::io::Seek),
1561    extents: &[AllocatedExtent],
1562    data: &[u8],
1563) -> Result<(), Ext4Error> {
1564    let mut data_offset = 0usize;
1565    for extent in extents {
1566        let remaining = data.len().saturating_sub(data_offset);
1567        let extent_bytes = extent.block_count as usize * EXT4_BLOCK_SIZE as usize;
1568        let write_len = remaining.min(extent_bytes);
1569        file.seek(SeekFrom::Start(
1570            extent.physical_start * EXT4_BLOCK_SIZE as u64,
1571        ))?;
1572        file.write_all(&data[data_offset..data_offset + write_len])?;
1573        write_zero_padding(file, extent_bytes - write_len)?;
1574        data_offset += write_len;
1575    }
1576
1577    debug_assert_eq!(data_offset, data.len());
1578    Ok(())
1579}
1580
1581fn write_zero_padding(file: &mut impl std::io::Write, mut len: usize) -> Result<(), Ext4Error> {
1582    static ZEROS: [u8; EXT4_BLOCK_SIZE as usize] = [0u8; EXT4_BLOCK_SIZE as usize];
1583    while len > 0 {
1584        let chunk = len.min(ZEROS.len());
1585        file.write_all(&ZEROS[..chunk])?;
1586        len -= chunk;
1587    }
1588    Ok(())
1589}
1590
1591fn write_extent_bytes(
1592    file: &mut (impl std::io::Write + std::io::Seek),
1593    start_block: u64,
1594    data: &[u8],
1595) -> Result<(), Ext4Error> {
1596    let offset = start_block * EXT4_BLOCK_SIZE as u64;
1597    file.seek(SeekFrom::Start(offset))?;
1598    file.write_all(data)?;
1599
1600    let pad = (EXT4_BLOCK_SIZE as usize - (data.len() % EXT4_BLOCK_SIZE as usize))
1601        % EXT4_BLOCK_SIZE as usize;
1602    if pad > 0 {
1603        static ZEROS: [u8; 4096] = [0u8; 4096];
1604        file.write_all(&ZEROS[..pad])?;
1605    }
1606
1607    Ok(())
1608}
1609
1610fn update_dir_block_checksums(csum_seed: u32, inode: u32, data: &mut [u8]) {
1611    for chunk in data.as_chunks_mut::<{ EXT4_BLOCK_SIZE as usize }>().0 {
1612        let tail = EXT4_BLOCK_SIZE as usize - 12;
1613        let checksum = dir_block_checksum(csum_seed, inode, 0, &chunk[..tail]);
1614        put_le32(chunk, tail + 8, checksum);
1615    }
1616}
1617
1618fn write_inode_table_with_plan(
1619    file: &mut (impl std::io::Write + std::io::Seek),
1620    layout: &Layout,
1621    plans: &[NodePlan],
1622) -> Result<(), Ext4Error> {
1623    let root_inode = build_inode_from_plan(layout, &plans[0])?;
1624    let root_offset = inode_offset(layout, EXT4_ROOT_INO);
1625    file.seek(SeekFrom::Start(root_offset))?;
1626    file.write_all(&root_inode)?;
1627
1628    let journal_inode = build_journal_inode(layout)?;
1629    let journal_offset = inode_offset(layout, EXT4_JOURNAL_INO);
1630    file.seek(SeekFrom::Start(journal_offset))?;
1631    file.write_all(&journal_inode)?;
1632
1633    for plan in plans.iter().filter(|plan| plan.inode >= EXT4_FIRST_INO) {
1634        let inode_bytes = build_inode_from_plan(layout, plan)?;
1635        file.seek(SeekFrom::Start(inode_offset(layout, plan.inode)))?;
1636        file.write_all(&inode_bytes)?;
1637    }
1638
1639    Ok(())
1640}
1641
1642fn inode_offset(layout: &Layout, inode: u32) -> u64 {
1643    let zero_based = inode - 1;
1644    let group = zero_based / EXT4_INODES_PER_GROUP;
1645    let index = zero_based % EXT4_INODES_PER_GROUP;
1646    layout.group_inode_table_block(group) * EXT4_BLOCK_SIZE as u64
1647        + u64::from(index) * EXT4_INODE_SIZE as u64
1648}
1649
1650fn build_inode_from_plan(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
1651    let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1652    let (mode, size, links_count, extents) = match &plan.kind {
1653        NodeKind::Directory { data, .. } => (
1654            S_IFDIR | plan.metadata.mode,
1655            data.len() as u64,
1656            plan.links_count,
1657            true,
1658        ),
1659        NodeKind::RegularFile { data } => (
1660            S_IFREG | plan.metadata.mode,
1661            data.len() as u64,
1662            plan.links_count,
1663            true,
1664        ),
1665        NodeKind::Symlink { target, inline } => (
1666            S_IFLNK | plan.metadata.mode,
1667            target.len() as u64,
1668            plan.links_count,
1669            !inline,
1670        ),
1671        NodeKind::CharDevice { .. } => (S_IFCHR | plan.metadata.mode, 0, plan.links_count, false),
1672        NodeKind::BlockDevice { .. } => (S_IFBLK | plan.metadata.mode, 0, plan.links_count, false),
1673        NodeKind::Fifo => (S_IFIFO | plan.metadata.mode, 0, plan.links_count, false),
1674        NodeKind::Socket => (S_IFSOCK | plan.metadata.mode, 0, plan.links_count, false),
1675    };
1676
1677    put_le16(&mut inode, 0x00, mode);
1678    put_le16(&mut inode, 0x02, plan.metadata.uid as u16);
1679    put_le32(&mut inode, 0x04, size as u32);
1680    write_inode_time(&mut inode, 0x08, 0x8c, &plan.metadata)?;
1681    write_inode_time(&mut inode, 0x0c, 0x84, &plan.metadata)?;
1682    write_inode_time(&mut inode, 0x10, 0x88, &plan.metadata)?;
1683    put_le16(&mut inode, 0x18, plan.metadata.gid as u16);
1684    put_le16(
1685        &mut inode,
1686        0x1A,
1687        u16::try_from(links_count).map_err(|_| {
1688            Ext4Error::Layout(format!(
1689                "link count exceeds ext4 inode field for '{}'",
1690                plan.path
1691            ))
1692        })?,
1693    );
1694    let allocated_blocks = plan.block_count
1695        + u32::from(plan.extent_tree_block.is_some())
1696        + u32::from(plan.xattr_block.is_some());
1697    put_le32(&mut inode, 0x1C, allocated_blocks * (EXT4_BLOCK_SIZE / 512));
1698    if extents {
1699        put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1700    }
1701
1702    match &plan.kind {
1703        NodeKind::Directory { .. } | NodeKind::RegularFile { .. } => {
1704            write_extent_plan(&mut inode, 0x28, plan)?;
1705        }
1706        NodeKind::Symlink { target, inline } => {
1707            if *inline {
1708                inode[0x28..0x28 + target.len()].copy_from_slice(target);
1709            } else {
1710                write_extent_plan(&mut inode, 0x28, plan)?;
1711            }
1712        }
1713        NodeKind::CharDevice { major, minor } | NodeKind::BlockDevice { major, minor } => {
1714            write_device_number(&mut inode, *major, *minor, &plan.path)?;
1715        }
1716        NodeKind::Fifo | NodeKind::Socket => {}
1717    }
1718
1719    put_le32(&mut inode, 0x64, 0);
1720    if let Some(block) = plan.xattr_block {
1721        put_le32(&mut inode, 0x68, block as u32);
1722        put_le16(&mut inode, 0x76, (block >> 32) as u16);
1723    }
1724    put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1725    put_le16(&mut inode, 0x78, (plan.metadata.uid >> 16) as u16);
1726    put_le16(&mut inode, 0x7A, (plan.metadata.gid >> 16) as u16);
1727    put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1728    write_inode_time(&mut inode, 0x90, 0x94, &plan.metadata)?;
1729    if let Some(xattrs) = &plan.inline_xattrs {
1730        inode[0xA0..0xA0 + xattrs.len()].copy_from_slice(xattrs);
1731    }
1732
1733    let csum = inode_checksum(layout.csum_seed, plan.inode, 0, &inode);
1734    put_le16(&mut inode, 0x7C, csum as u16);
1735    put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1736
1737    Ok(inode)
1738}
1739
1740fn write_inode_time(
1741    inode: &mut [u8],
1742    seconds_offset: usize,
1743    extra_offset: usize,
1744    metadata: &InodeMetadata,
1745) -> Result<(), Ext4Error> {
1746    if metadata.mtime_nsec >= 1_000_000_000 {
1747        return Err(Ext4Error::Layout(format!(
1748            "inode timestamp nanoseconds are out of range: {}",
1749            metadata.mtime_nsec
1750        )));
1751    }
1752    if metadata.mtime > 0x3_ffff_ffff {
1753        return Err(Ext4Error::Layout(format!(
1754            "inode timestamp seconds exceed ext4 epoch range: {}",
1755            metadata.mtime
1756        )));
1757    }
1758
1759    put_le32(inode, seconds_offset, metadata.mtime as u32);
1760    let epoch = ((metadata.mtime >> 32) & 0x3) as u32;
1761    put_le32(inode, extra_offset, (metadata.mtime_nsec << 2) | epoch);
1762    Ok(())
1763}
1764
1765fn write_device_number(
1766    inode: &mut [u8],
1767    major: u32,
1768    minor: u32,
1769    path: &str,
1770) -> Result<(), Ext4Error> {
1771    if major > 0x0fff || minor > 0x0f_ffff {
1772        return Err(Ext4Error::Layout(format!(
1773            "device number is out of Linux ext4 range for '{path}': {major}:{minor}"
1774        )));
1775    }
1776
1777    if major < 256 && minor < 256 {
1778        put_le32(inode, 0x28, (major << 8) | minor);
1779    } else {
1780        let encoded = (minor & 0xff) | (major << 8) | ((minor & !0xff) << 12);
1781        put_le32(inode, 0x2c, encoded);
1782    }
1783    Ok(())
1784}
1785
1786fn write_empty_extent_tree(buf: &mut [u8], offset: usize) {
1787    put_le16(buf, offset, EXT4_EH_MAGIC);
1788    put_le16(buf, offset + 2, 0);
1789    put_le16(buf, offset + 4, 4);
1790    put_le16(buf, offset + 6, 0);
1791    put_le32(buf, offset + 8, 0);
1792}
1793
1794fn build_superblock_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1795    build_superblock_with_stats_for_group(layout, stats, 0, true)
1796}
1797
1798fn build_backup_superblock_with_stats(
1799    layout: &Layout,
1800    stats: &FsStats,
1801    group: u32,
1802) -> Result<Vec<u8>, Ext4Error> {
1803    build_superblock_with_stats_for_group(layout, stats, group, false)
1804}
1805
1806fn build_superblock_with_stats_for_group(
1807    layout: &Layout,
1808    stats: &FsStats,
1809    group: u32,
1810    padded_primary: bool,
1811) -> Result<Vec<u8>, Ext4Error> {
1812    let mut block = build_superblock(layout, group, padded_primary)?;
1813    let sb_offset = if padded_primary { 1024 } else { 0 };
1814    let sb = &mut block[sb_offset..sb_offset + 1024];
1815    put_le32(sb, 0x0C, stats.total_free_blocks as u32);
1816    put_le32(sb, 0x10, stats.total_free_inodes as u32);
1817    put_le32(sb, 0x158, (stats.total_free_blocks >> 32) as u32);
1818    let (overhead_offset, overhead_blocks) = if layout.resize_inode_block.is_none() {
1819        layout.recorded_overhead_blocks(stats.total_free_blocks)
1820    } else {
1821        (EXT4_SB_OVERHEAD_BLOCKS_OFFSET, stats.overhead_blocks)
1822    };
1823    put_le32(sb, overhead_offset, overhead_blocks as u32);
1824    let checksum = superblock_checksum(sb);
1825    put_le32(sb, 0x3FC, checksum);
1826    Ok(block)
1827}
1828
1829fn build_gdt_with_stats(layout: &Layout, stats: &FsStats) -> Result<Vec<u8>, Ext4Error> {
1830    let desc_size = EXT4_DESC_SIZE as usize;
1831    let geometry = layout.geometry();
1832    let mut gdt = Vec::with_capacity(layout.num_groups as usize * desc_size);
1833
1834    for g in 0..layout.num_groups {
1835        let desc_stats = GroupDescStats {
1836            free_blocks: stats.group_free_blocks[g as usize],
1837            free_inodes: stats.group_free_inodes[g as usize],
1838            used_dirs: stats.group_used_dirs[g as usize],
1839            block_bitmap_csum: stats.block_bitmap_checksums[g as usize],
1840            inode_bitmap_csum: stats.inode_bitmap_checksums[g as usize],
1841        };
1842        gdt.extend_from_slice(&build_group_descriptor(
1843            &geometry,
1844            g,
1845            &desc_stats,
1846            layout.csum_seed,
1847        ));
1848    }
1849
1850    Ok(gdt)
1851}
1852#[cfg(test)]
1853fn build_block_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1854    (0..layout.num_groups)
1855        .map(|group| build_block_bitmap(layout, group))
1856        .collect()
1857}
1858
1859#[cfg(test)]
1860fn build_inode_bitmaps(layout: &Layout) -> Vec<Vec<u8>> {
1861    (0..layout.num_groups)
1862        .map(|group| build_inode_bitmap(layout, group))
1863        .collect()
1864}
1865
1866#[cfg(test)]
1867fn build_block_bitmap(layout: &Layout, group: u32) -> Vec<u8> {
1868    let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1869
1870    // Metadata, the root directory block, and the journal are permanently
1871    // allocated within the filesystem image.
1872    let used = layout.group_used_blocks(group);
1873    for bit in 0..used {
1874        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1875    }
1876
1877    // Bits beyond the final partial group are permanently unavailable.
1878    let blocks_in_group = layout.blocks_in_group(group);
1879    for bit in blocks_in_group..EXT4_BLOCKS_PER_GROUP {
1880        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1881    }
1882
1883    bitmap
1884}
1885
1886#[cfg(test)]
1887fn build_inode_bitmap(_layout: &Layout, group: u32) -> Vec<u8> {
1888    let mut bitmap = vec![0u8; EXT4_BLOCK_SIZE as usize];
1889
1890    if group == 0 {
1891        // Inode numbering is 1-based; bit 0 corresponds to inode 1.
1892        for bit in 0..(EXT4_FIRST_INO - 1) {
1893            bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1894        }
1895    }
1896
1897    // The inode bitmap consumes only the first inodes-per-group bits; the
1898    // remaining padding bits in the block must stay permanently set.
1899    for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
1900        bitmap[(bit / 8) as usize] |= 1 << (bit % 8);
1901    }
1902
1903    bitmap
1904}
1905
1906fn write_bitmaps(
1907    file: &mut (impl std::io::Write + std::io::Seek),
1908    layout: &Layout,
1909    plan: &BitmapPlan,
1910) -> Result<(), Ext4Error> {
1911    for group in 0..layout.num_groups {
1912        let block_bitmap = build_block_bitmap_for_plan(layout, plan, group);
1913        let inode_bitmap = build_inode_bitmap_for_plan(layout, plan, group);
1914        let block_offset = layout.group_block_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1915        file.seek(SeekFrom::Start(block_offset))?;
1916        file.write_all(&block_bitmap)?;
1917
1918        let inode_offset = layout.group_inode_bitmap_block(group) * EXT4_BLOCK_SIZE as u64;
1919        file.seek(SeekFrom::Start(inode_offset))?;
1920        file.write_all(&inode_bitmap)?;
1921    }
1922
1923    Ok(())
1924}
1925
1926/// Build the 256-byte journal inode (inode 8).
1927fn build_journal_inode(layout: &Layout) -> Result<Vec<u8>, Ext4Error> {
1928    let mut inode = vec![0u8; EXT4_INODE_SIZE as usize];
1929
1930    let mode = S_IFREG | 0o600;
1931    let size = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
1932
1933    // i_mode (offset 0, u16)
1934    put_le16(&mut inode, 0x00, mode);
1935    // i_size_lo (offset 4, u32)
1936    put_le32(&mut inode, 0x04, size as u32);
1937    // i_size_high (offset 108, u32)
1938    put_le32(&mut inode, 0x6C, (size >> 32) as u32);
1939    // i_links_count (offset 26, u16)
1940    put_le16(&mut inode, 0x1A, 1);
1941    // i_blocks_lo (offset 28, u32) -- in 512-byte sectors
1942    let sectors = (layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64) / 512;
1943    put_le32(&mut inode, 0x1C, sectors as u32);
1944    // i_flags (offset 32, u32)
1945    put_le32(&mut inode, 0x20, EXT4_EXTENTS_FL);
1946
1947    // i_block (offset 40, 60 bytes) -- extent tree pointing to journal blocks
1948    write_extent_tree(
1949        &mut inode,
1950        0x28,
1951        layout.journal_start_block,
1952        layout.journal_blocks,
1953        "journal",
1954    )?;
1955
1956    // i_generation (offset 100, u32)
1957    put_le32(&mut inode, 0x64, 0);
1958
1959    // -- Extended inode fields --
1960    // i_extra_isize (offset 128, u16)
1961    put_le16(&mut inode, 0x80, EXT4_MIN_EXTRA_ISIZE);
1962
1963    // Inode checksum
1964    let csum = inode_checksum(layout.csum_seed, EXT4_JOURNAL_INO, 0, &inode);
1965    // l_i_checksum_lo (offset 0x7C, u16)
1966    put_le16(&mut inode, 0x7C, csum as u16);
1967    // i_checksum_hi (offset 0x82, u16)
1968    put_le16(&mut inode, 0x82, (csum >> 16) as u16);
1969
1970    Ok(inode)
1971}
1972
1973/// Write an extent tree header + one extent entry into `buf` at `offset`.
1974///
1975/// The extent tree header is 12 bytes, each extent entry is also 12 bytes.
1976fn write_extent_tree(
1977    buf: &mut [u8],
1978    offset: usize,
1979    start_block: u64,
1980    block_count: u32,
1981    label: &str,
1982) -> Result<(), Ext4Error> {
1983    validate_extent_block_count(block_count, label)?;
1984    if start_block > u32::MAX as u64 {
1985        return Err(Ext4Error::TooLarge {
1986            requested_blocks: start_block,
1987            max_blocks: u32::MAX as u64,
1988        });
1989    }
1990
1991    // Extent header (12 bytes)
1992    put_le16(buf, offset, EXT4_EH_MAGIC); // eh_magic
1993    put_le16(buf, offset + 2, 1); // eh_entries
1994    put_le16(buf, offset + 4, 4); // eh_max (for inode: (60-12)/12 = 4)
1995    put_le16(buf, offset + 6, 0); // eh_depth (leaf)
1996    put_le32(buf, offset + 8, 0); // eh_generation
1997
1998    // Extent entry (12 bytes) at offset+12
1999    let ext_off = offset + 12;
2000    put_le32(buf, ext_off, 0); // ee_block (logical block 0)
2001    put_le16(buf, ext_off + 4, block_count as u16); // ee_len
2002    put_le16(buf, ext_off + 6, (start_block >> 32) as u16); // ee_start_hi
2003    put_le32(buf, ext_off + 8, start_block as u32); // ee_start_lo
2004
2005    Ok(())
2006}
2007
2008fn write_extent_plan(buf: &mut [u8], offset: usize, plan: &NodePlan) -> Result<(), Ext4Error> {
2009    if plan.data_extents.is_empty() {
2010        write_empty_extent_tree(buf, offset);
2011        return Ok(());
2012    }
2013
2014    if plan.data_extents.len() <= INLINE_EXTENT_CAPACITY {
2015        write_extent_header(
2016            buf,
2017            offset,
2018            plan.data_extents.len() as u16,
2019            INLINE_EXTENT_CAPACITY as u16,
2020            0,
2021        );
2022        for (index, extent) in plan.data_extents.iter().enumerate() {
2023            write_extent_entry(buf, offset + 12 + index * 12, extent, &plan.path)?;
2024        }
2025        return Ok(());
2026    }
2027
2028    let leaf = plan.extent_tree_block.ok_or_else(|| {
2029        Ext4Error::Layout(format!("missing extent-tree block for '{}'", plan.path))
2030    })?;
2031    write_extent_header(buf, offset, 1, INLINE_EXTENT_CAPACITY as u16, 1);
2032    let index_offset = offset + 12;
2033    put_le32(buf, index_offset, plan.data_extents[0].logical_block);
2034    put_le32(buf, index_offset + 4, leaf as u32);
2035    put_le16(buf, index_offset + 8, (leaf >> 32) as u16);
2036    put_le16(buf, index_offset + 10, 0);
2037    Ok(())
2038}
2039
2040fn build_extent_leaf_block(layout: &Layout, plan: &NodePlan) -> Result<Vec<u8>, Ext4Error> {
2041    if plan.data_extents.len() > EXTENT_BLOCK_CAPACITY {
2042        return Err(Ext4Error::Layout(format!(
2043            "'{}' exceeds the depth-one extent-tree capacity",
2044            plan.path
2045        )));
2046    }
2047
2048    let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
2049    write_extent_header(
2050        &mut block,
2051        0,
2052        plan.data_extents.len() as u16,
2053        EXTENT_BLOCK_CAPACITY as u16,
2054        0,
2055    );
2056    for (index, extent) in plan.data_extents.iter().enumerate() {
2057        write_extent_entry(&mut block, 12 + index * 12, extent, &plan.path)?;
2058    }
2059
2060    let tail = EXT4_BLOCK_SIZE as usize - std::mem::size_of::<u32>();
2061    let checksum = dir_block_checksum(layout.csum_seed, plan.inode, 0, &block[..tail]);
2062    put_le32(&mut block, tail, checksum);
2063    Ok(block)
2064}
2065
2066fn build_xattr_block(
2067    layout: &Layout,
2068    block_number: u64,
2069    xattrs: &[Xattr],
2070    path: &str,
2071) -> Result<Vec<u8>, Ext4Error> {
2072    let encoded = encode_xattrs(xattrs, path)?;
2073
2074    let mut block = vec![0u8; EXT4_BLOCK_SIZE as usize];
2075    put_le32(&mut block, 0, EXT4_XATTR_MAGIC);
2076    put_le32(&mut block, 4, 1);
2077    put_le32(&mut block, 8, 1);
2078
2079    let mut entry_offset = EXT4_XATTR_HEADER_SIZE;
2080    let mut value_offset = EXT4_BLOCK_SIZE as usize;
2081    let mut block_hash = 0u32;
2082    for xattr in encoded {
2083        let entry_len = align_four(EXT4_XATTR_ENTRY_SIZE + xattr.name.len());
2084        let padded_value_len = align_four(xattr.value.len());
2085        value_offset = value_offset.checked_sub(padded_value_len).ok_or_else(|| {
2086            Ext4Error::Layout(format!(
2087                "extended attributes on '{path}' exceed an ext4 block"
2088            ))
2089        })?;
2090        if entry_offset + entry_len + 4 > value_offset {
2091            return Err(Ext4Error::Layout(format!(
2092                "extended attributes on '{path}' do not fit in one ext4 xattr block"
2093            )));
2094        }
2095
2096        block[value_offset..value_offset + xattr.value.len()].copy_from_slice(xattr.value);
2097        let entry_hash = xattr_entry_hash(
2098            xattr.name,
2099            &block[value_offset..value_offset + padded_value_len],
2100        );
2101        block[entry_offset] = xattr.name.len() as u8;
2102        block[entry_offset + 1] = xattr.name_index;
2103        put_le16(&mut block, entry_offset + 2, value_offset as u16);
2104        put_le32(&mut block, entry_offset + 4, 0);
2105        put_le32(&mut block, entry_offset + 8, xattr.value.len() as u32);
2106        put_le32(&mut block, entry_offset + 12, entry_hash);
2107        block[entry_offset + 16..entry_offset + 16 + xattr.name.len()].copy_from_slice(xattr.name);
2108        entry_offset += entry_len;
2109        block_hash = block_hash.rotate_left(16) ^ entry_hash;
2110    }
2111
2112    put_le32(&mut block, 12, block_hash);
2113    let checksum = xattr_block_checksum(layout.csum_seed, block_number, &block);
2114    put_le32(&mut block, 16, checksum);
2115    Ok(block)
2116}
2117
2118fn build_inline_xattrs(xattrs: &[Xattr], path: &str) -> Result<Option<Vec<u8>>, Ext4Error> {
2119    if xattrs.is_empty() {
2120        return Ok(None);
2121    }
2122
2123    let encoded = encode_xattrs(xattrs, path)?;
2124    let mut bytes = vec![0u8; EXT4_INODE_SIZE as usize - 0xA0];
2125    put_le32(&mut bytes, 0, EXT4_XATTR_MAGIC);
2126    let first_entry = 4usize;
2127    let mut entry_offset = first_entry;
2128    let mut value_offset = bytes.len();
2129    for xattr in encoded {
2130        let entry_len = align_four(EXT4_XATTR_ENTRY_SIZE + xattr.name.len());
2131        let padded_value_len = align_four(xattr.value.len());
2132        let Some(next_value_offset) = value_offset.checked_sub(padded_value_len) else {
2133            return Ok(None);
2134        };
2135        if entry_offset + entry_len + 4 > next_value_offset {
2136            return Ok(None);
2137        }
2138        value_offset = next_value_offset;
2139        bytes[value_offset..value_offset + xattr.value.len()].copy_from_slice(xattr.value);
2140        bytes[entry_offset] = xattr.name.len() as u8;
2141        bytes[entry_offset + 1] = xattr.name_index;
2142        put_le16(
2143            &mut bytes,
2144            entry_offset + 2,
2145            (value_offset - first_entry) as u16,
2146        );
2147        put_le32(&mut bytes, entry_offset + 4, 0);
2148        put_le32(&mut bytes, entry_offset + 8, xattr.value.len() as u32);
2149        put_le32(&mut bytes, entry_offset + 12, 0);
2150        bytes[entry_offset + 16..entry_offset + 16 + xattr.name.len()].copy_from_slice(xattr.name);
2151        entry_offset += entry_len;
2152    }
2153
2154    Ok(Some(bytes))
2155}
2156
2157fn encode_xattrs<'a>(xattrs: &'a [Xattr], path: &str) -> Result<Vec<EncodedXattr<'a>>, Ext4Error> {
2158    let mut encoded = xattrs
2159        .iter()
2160        .map(|xattr| encode_xattr_name(xattr, path))
2161        .collect::<Result<Vec<_>, _>>()?;
2162    encoded.sort_by(|left, right| {
2163        (left.name_index, left.name.len(), left.name).cmp(&(
2164            right.name_index,
2165            right.name.len(),
2166            right.name,
2167        ))
2168    });
2169    if encoded
2170        .windows(2)
2171        .any(|pair| pair[0].name_index == pair[1].name_index && pair[0].name == pair[1].name)
2172    {
2173        return Err(Ext4Error::Layout(format!(
2174            "duplicate extended attribute on '{path}'"
2175        )));
2176    }
2177    Ok(encoded)
2178}
2179
2180fn encode_xattr_name<'a>(xattr: &'a Xattr, path: &str) -> Result<EncodedXattr<'a>, Ext4Error> {
2181    if xattr.name.is_empty() || xattr.name.len() > 255 || xattr.name.contains(&0) {
2182        return Err(Ext4Error::Layout(format!(
2183            "invalid extended-attribute name on '{path}'"
2184        )));
2185    }
2186
2187    let (name_index, name) = if xattr.name.as_slice() == b"system.posix_acl_access" {
2188        (2, &[][..])
2189    } else if xattr.name.as_slice() == b"system.posix_acl_default" {
2190        (3, &[][..])
2191    } else if let Some(name) = xattr.name.strip_prefix(b"user.") {
2192        (1, name)
2193    } else if let Some(name) = xattr.name.strip_prefix(b"trusted.") {
2194        (4, name)
2195    } else if let Some(name) = xattr.name.strip_prefix(b"security.") {
2196        (6, name)
2197    } else {
2198        return Err(Ext4Error::Layout(format!(
2199            "unsupported extended-attribute namespace on '{path}'"
2200        )));
2201    };
2202    if name.is_empty() && name_index != 2 && name_index != 3 {
2203        return Err(Ext4Error::Layout(format!(
2204            "extended-attribute namespace has an empty name on '{path}'"
2205        )));
2206    }
2207
2208    Ok(EncodedXattr {
2209        name_index,
2210        name,
2211        value: &xattr.value,
2212    })
2213}
2214
2215fn xattr_entry_hash(name: &[u8], padded_value: &[u8]) -> u32 {
2216    let mut hash = 0u32;
2217    for byte in name {
2218        hash = hash.rotate_left(5) ^ u32::from(*byte);
2219    }
2220    for word in padded_value.as_chunks::<4>().0 {
2221        hash = hash.rotate_left(16) ^ u32::from_le_bytes(*word);
2222    }
2223    hash
2224}
2225
2226fn xattr_block_checksum(csum_seed: u32, block_number: u64, block: &[u8]) -> u32 {
2227    let mut checksum = crc32c::crc32c_raw(csum_seed, &block_number.to_le_bytes());
2228    checksum = crc32c::crc32c_raw(checksum, &block[..16]);
2229    checksum = crc32c::crc32c_raw(checksum, &[0u8; 4]);
2230    crc32c::crc32c_raw(checksum, &block[20..])
2231}
2232
2233fn align_four(value: usize) -> usize {
2234    (value + 3) & !3
2235}
2236
2237fn write_extent_header(buf: &mut [u8], offset: usize, entries: u16, max: u16, depth: u16) {
2238    put_le16(buf, offset, EXT4_EH_MAGIC);
2239    put_le16(buf, offset + 2, entries);
2240    put_le16(buf, offset + 4, max);
2241    put_le16(buf, offset + 6, depth);
2242    put_le32(buf, offset + 8, 0);
2243}
2244
2245fn write_extent_entry(
2246    buf: &mut [u8],
2247    offset: usize,
2248    extent: &AllocatedExtent,
2249    label: &str,
2250) -> Result<(), Ext4Error> {
2251    validate_extent_block_count(extent.block_count, label)?;
2252    put_le32(buf, offset, extent.logical_block);
2253    put_le16(buf, offset + 4, extent.block_count as u16);
2254    put_le16(buf, offset + 6, (extent.physical_start >> 32) as u16);
2255    put_le32(buf, offset + 8, extent.physical_start as u32);
2256    Ok(())
2257}
2258
2259/// Write the journal superblock at the first journal block.
2260fn write_journal(
2261    file: &mut (impl std::io::Write + std::io::Seek),
2262    layout: &Layout,
2263) -> Result<(), Ext4Error> {
2264    let mut jsb = vec![0u8; EXT4_BLOCK_SIZE as usize];
2265
2266    // All jbd2 fields are BIG-ENDIAN.
2267    // Header (12 bytes)
2268    put_be32(&mut jsb, 0, JBD2_MAGIC); // h_magic
2269    put_be32(&mut jsb, 4, JBD2_SUPERBLOCK_V2); // h_blocktype
2270    put_be32(&mut jsb, 8, 0); // h_sequence (not used for sb)
2271
2272    // Journal superblock fields
2273    put_be32(&mut jsb, 12, EXT4_BLOCK_SIZE); // s_blocksize
2274    put_be32(&mut jsb, 16, layout.journal_blocks); // s_maxlen
2275    put_be32(&mut jsb, 20, 1); // s_first (first log block)
2276    put_be32(&mut jsb, 24, 1); // s_sequence (next expected sequence)
2277    put_be32(&mut jsb, 28, 0); // s_start (0 = clean/no recovery needed)
2278
2279    // s_errno (offset 32)
2280    put_be32(&mut jsb, 32, 0);
2281    // s_feature_compat (offset 36)
2282    put_be32(&mut jsb, 36, 0);
2283    // s_feature_incompat (offset 40) = CSUM_V3(0x10) | 64BIT(0x02) | REVOKE(0x01)
2284    put_be32(&mut jsb, 40, 0x13);
2285    // s_feature_ro_compat (offset 44)
2286    put_be32(&mut jsb, 44, 0);
2287
2288    // s_uuid (offset 48, 16 bytes) -- same as filesystem UUID
2289    jsb[48..64].copy_from_slice(&layout.uuid);
2290
2291    // s_nr_users (offset 64, u32)
2292    put_be32(&mut jsb, 64, 1);
2293
2294    // s_dynsuper (offset 68, u32) -- block of dynamic superblock copy
2295    put_be32(&mut jsb, 68, 0);
2296
2297    // s_max_transaction (offset 72), s_max_trans_data (offset 76)
2298    put_be32(&mut jsb, 72, 0);
2299    put_be32(&mut jsb, 76, 0);
2300
2301    // s_checksum_type (offset 80, u8) = 4 (CRC32C)
2302    // Actually offset for checksum_type is at offset 80+... Let me use correct
2303    // offsets from the jbd2 spec:
2304    //   offset 80: padding (u8)
2305    //   offset 81-83: padding
2306    //   offset 84-87: s_padding2
2307    //   offset 88-91: s_num_fc_blks
2308    //   offset 92-95: s_head
2309    //   offset 96-255: s_padding[44]
2310    //   offset 256-271: s_users[16*48] (first 16 bytes = first user UUID)
2311    //
2312    // Correct jbd2 superblock layout (from kernel headers):
2313    //   0x00: h_magic (u32be)
2314    //   0x04: h_blocktype (u32be)
2315    //   0x08: h_sequence (u32be)
2316    //   0x0C: s_blocksize (u32be)
2317    //   0x10: s_maxlen (u32be)
2318    //   0x14: s_first (u32be)
2319    //   0x18: s_sequence (u32be)
2320    //   0x1C: s_start (u32be)
2321    //   0x20: s_errno (u32be)
2322    //   0x24: s_feature_compat (u32be)
2323    //   0x28: s_feature_incompat (u32be)
2324    //   0x2C: s_feature_ro_compat (u32be)
2325    //   0x30: s_uuid[16]
2326    //   0x40: s_nr_users (u32be)
2327    //   0x44: s_dynsuper (u32be)
2328    //   0x48: s_max_transaction (u32be)
2329    //   0x4C: s_max_trans_data (u32be)
2330    //   0x50: s_checksum_type (u8)
2331    //   0x51: s_padding2[3]
2332    //   0x54: s_padding[42] (u32be array = 168 bytes)
2333    //   0xFC: s_checksum (u32be)
2334    //   0x100: s_users[16*48]
2335
2336    jsb[0x50] = 4; // s_checksum_type = CRC32C
2337
2338    // s_checksum (offset 0xFC, u32be), computed over the 1024-byte on-disk
2339    // jbd2 superblock with the checksum field zeroed.
2340    let jsb_csum = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb[..JBD2_SUPERBLOCK_SIZE]);
2341    put_be32(&mut jsb, 0xFC, jsb_csum);
2342
2343    let offset = layout.journal_start_block * EXT4_BLOCK_SIZE as u64;
2344    file.seek(SeekFrom::Start(offset))?;
2345    file.write_all(&jsb)?;
2346
2347    Ok(())
2348}
2349
2350/// Build an ext4 superblock image.
2351fn build_superblock(
2352    layout: &Layout,
2353    group: u32,
2354    padded_primary: bool,
2355) -> Result<Vec<u8>, Ext4Error> {
2356    let mut block = if padded_primary {
2357        vec![0u8; EXT4_BLOCK_SIZE as usize]
2358    } else {
2359        vec![0u8; 1024]
2360    };
2361    let sb_offset = if padded_primary { 1024 } else { 0 };
2362    let sb = &mut block[sb_offset..sb_offset + 1024];
2363
2364    let total_blocks = layout.num_blocks;
2365    let total_inodes = layout.num_groups as u64 * EXT4_INODES_PER_GROUP as u64;
2366
2367    let free_blocks = layout.total_free_blocks();
2368    let free_inodes = layout.total_free_inodes();
2369
2370    // s_inodes_count (0x00, u32)
2371    put_le32(sb, 0x00, total_inodes as u32);
2372    // s_blocks_count_lo (0x04, u32)
2373    put_le32(sb, 0x04, total_blocks as u32);
2374    // s_r_blocks_count_lo (0x08, u32) -- reserved blocks for superuser
2375    put_le32(sb, 0x08, 0);
2376    // s_free_blocks_count_lo (0x0C, u32)
2377    put_le32(sb, 0x0C, free_blocks as u32);
2378    // s_free_inodes_count (0x10, u32)
2379    put_le32(sb, 0x10, free_inodes as u32);
2380    // s_first_data_block (0x14, u32) -- 0 for 4k blocks
2381    put_le32(sb, 0x14, 0);
2382    // s_log_block_size (0x18, u32)
2383    put_le32(sb, 0x18, EXT4_LOG_BLOCK_SIZE);
2384    // s_log_cluster_size (0x1C, u32)
2385    put_le32(sb, 0x1C, EXT4_LOG_BLOCK_SIZE);
2386    // s_blocks_per_group (0x20, u32)
2387    put_le32(sb, 0x20, EXT4_BLOCKS_PER_GROUP);
2388    // s_clusters_per_group (0x24, u32)
2389    put_le32(sb, 0x24, EXT4_BLOCKS_PER_GROUP);
2390    // s_inodes_per_group (0x28, u32)
2391    put_le32(sb, 0x28, EXT4_INODES_PER_GROUP);
2392
2393    // s_mtime (0x2C, u32), s_wtime (0x30, u32)
2394    // Leave as 0.
2395
2396    // s_mnt_count (0x34, u16)
2397    put_le16(sb, 0x34, 0);
2398    // s_max_mnt_count (0x36, u16) -- -1 = no limit
2399    put_le16(sb, 0x36, 0xFFFF);
2400    // s_magic (0x38, u16)
2401    put_le16(sb, 0x38, EXT4_SUPER_MAGIC);
2402    // s_state (0x3A, u16) -- 1 = clean
2403    put_le16(sb, 0x3A, 1);
2404    // s_errors (0x3C, u16) -- 1 = continue
2405    put_le16(sb, 0x3C, 1);
2406    // s_minor_rev_level (0x3E, u16)
2407    put_le16(sb, 0x3E, 0);
2408
2409    // s_lastcheck (0x40, u32), s_checkinterval (0x44, u32)
2410    // Leave as 0.
2411
2412    // s_creator_os (0x48, u32) -- 0 = Linux
2413    put_le32(sb, 0x48, 0);
2414    // s_rev_level (0x4C, u32) -- 1 = dynamic rev
2415    put_le32(sb, 0x4C, 1);
2416
2417    // s_def_resuid (0x50, u16) -- 0
2418    put_le16(sb, 0x50, 0);
2419    // s_def_resgid (0x52, u16) -- 0
2420    put_le16(sb, 0x52, 0);
2421
2422    // --- EXT4_DYNAMIC_REV specific ---
2423    // s_first_ino (0x54, u32)
2424    put_le32(sb, 0x54, EXT4_FIRST_INO);
2425    // s_inode_size (0x58, u16)
2426    put_le16(sb, 0x58, EXT4_INODE_SIZE);
2427    // s_block_group_nr (0x5A, u16) -- block group hosting this superblock
2428    put_le16(sb, 0x5A, group as u16);
2429
2430    // s_feature_compat (0x5C, u32)
2431    put_le32(sb, 0x5C, layout.feature_compat);
2432    // s_feature_incompat (0x60, u32)
2433    put_le32(sb, 0x60, layout.feature_incompat);
2434    // s_feature_ro_compat (0x64, u32)
2435    put_le32(sb, 0x64, layout.feature_ro_compat);
2436
2437    // s_uuid (0x68, 16 bytes)
2438    sb[0x68..0x78].copy_from_slice(&layout.uuid);
2439
2440    // s_volume_name (0x78, 16 bytes) -- leave empty
2441
2442    // s_last_mounted (0x88, 64 bytes) -- leave empty
2443
2444    // s_algorithm_usage_bitmap (0xC8, u32) -- 0
2445    put_le32(sb, 0xC8, 0);
2446
2447    // s_prealloc_blocks (0xCC, u8), s_prealloc_dir_blocks (0xCD, u8)
2448    sb[0xCC] = 0;
2449    sb[0xCD] = 0;
2450
2451    // s_reserved_gdt_blocks (0xCE, u16)
2452    put_le16(sb, 0xCE, layout.reserved_gdt_blocks as u16);
2453
2454    // s_journal_uuid (0xD0, 16 bytes) -- leave zeroed (internal journal)
2455
2456    // s_journal_inum (0xE0, u32)
2457    put_le32(sb, 0xE0, EXT4_JOURNAL_INO);
2458    // s_journal_dev (0xE4, u32) -- 0 (internal)
2459    put_le32(sb, 0xE4, 0);
2460    // s_last_orphan (0xE8, u32)
2461    put_le32(sb, 0xE8, 0);
2462
2463    // s_hash_seed (0xEC, 4*u32 = 16 bytes) -- random
2464    sb[0xEC..0xFC].copy_from_slice(&layout.uuid); // reuse uuid bytes as hash seed
2465
2466    // s_def_hash_version (0xFC, u8) -- 1 = half MD4
2467    sb[0xFC] = 1;
2468    // s_jnl_backup_type (0xFD, u8) -- 1
2469    sb[0xFD] = 1;
2470
2471    // s_desc_size (0xFE, u16)
2472    put_le16(sb, 0xFE, EXT4_DESC_SIZE);
2473
2474    // s_default_mount_opts (0x100, u32) -- 0x000C (user_xattr, acl)
2475    put_le32(sb, 0x100, 0x000C);
2476
2477    // s_first_meta_bg (0x104, u32)
2478    put_le32(sb, 0x104, 0);
2479
2480    // s_mkfs_time (0x108, u32) -- leave 0
2481
2482    // s_jnl_blocks (0x10C, 17*u32 = 68 bytes) -- journal inode i_block backup
2483    // Copy the extent tree from the journal inode.
2484    {
2485        let mut extent_buf = [0u8; 60];
2486        write_extent_tree(
2487            &mut extent_buf,
2488            0,
2489            layout.journal_start_block,
2490            layout.journal_blocks,
2491            "journal",
2492        )?;
2493        // Copy 15 u32s (60 bytes) into s_jnl_blocks
2494        sb[0x10C..0x10C + 60].copy_from_slice(&extent_buf);
2495        // s_jnl_blocks[15] = i_size_lo
2496        let jsize = layout.journal_blocks as u64 * EXT4_BLOCK_SIZE as u64;
2497        put_le32(sb, 0x10C + 60, jsize as u32);
2498        // s_jnl_blocks[16] = i_size_hi
2499        put_le32(sb, 0x10C + 64, (jsize >> 32) as u32);
2500    }
2501
2502    // --- 64-bit fields ---
2503    // s_blocks_count_hi (0x150, u32)
2504    put_le32(sb, 0x150, (total_blocks >> 32) as u32);
2505    // s_r_blocks_count_hi (0x154, u32)
2506    put_le32(sb, 0x154, 0);
2507    // s_free_blocks_count_hi (0x158, u32)
2508    put_le32(sb, 0x158, (free_blocks >> 32) as u32);
2509
2510    // s_min_extra_isize (0x15C, u16)
2511    put_le16(sb, 0x15C, EXT4_MIN_EXTRA_ISIZE);
2512    // s_want_extra_isize (0x15E, u16)
2513    put_le16(sb, 0x15E, EXT4_MIN_EXTRA_ISIZE);
2514
2515    // s_flags (0x160, u32)
2516    put_le32(sb, 0x160, 0);
2517
2518    // s_log_groups_per_flex (0x174, u8) -- flex_bg disabled
2519    sb[0x174] = 0;
2520
2521    // s_checksum_type (0x175, u8) -- 1 = CRC32C
2522    sb[0x175] = 1;
2523
2524    // s_kbytes_written (0x178, u64) -- 0
2525    // s_snapshot_inum, etc. -- leave zeroed
2526
2527    // Modern images use s_overhead_clusters at 0x248. The test-only legacy formatter preserves
2528    // the pre-0.6.9 field placement at 0x194 so compatibility tests exercise released bytes.
2529    let (overhead_offset, overhead_blocks) =
2530        layout.recorded_overhead_blocks(layout.total_free_blocks());
2531    put_le32(sb, overhead_offset, overhead_blocks as u32);
2532
2533    // s_checksum_seed (0x270, u32) -- crc32c::crc32c_raw(~0, uuid)
2534    // Only used if INCOMPAT_CSUM_SEED is set. For METADATA_CSUM without
2535    // CSUM_SEED, the kernel computes from the UUID. We don't set
2536    // INCOMPAT_CSUM_SEED so leave this zero.
2537    put_le32(sb, 0x270, 0);
2538
2539    // s_encoding (0x27C, u16) -- 0 (no casefold)
2540    put_le16(sb, 0x27C, 0);
2541
2542    // s_checksum (0x3FC, u32) -- CRC32C of sb bytes 0..0x3FC
2543    let sb_csum = superblock_checksum(sb);
2544    put_le32(sb, 0x3FC, sb_csum);
2545
2546    Ok(block)
2547}
2548
2549/// Build the group descriptor table (GDT). Returns a byte vector containing
2550/// all group descriptors (64 bytes each).
2551#[cfg(test)]
2552fn build_gdt(
2553    layout: &Layout,
2554    block_bitmaps: &[Vec<u8>],
2555    inode_bitmaps: &[Vec<u8>],
2556) -> Result<Vec<u8>, Ext4Error> {
2557    let geometry = layout.geometry();
2558    let mut gdt = Vec::with_capacity(layout.num_groups as usize * EXT4_DESC_SIZE as usize);
2559
2560    for g in 0..layout.num_groups {
2561        let desc_stats = GroupDescStats {
2562            free_blocks: layout.group_free_blocks(g),
2563            free_inodes: layout.group_free_inodes(g),
2564            used_dirs: layout.group_used_dirs(g),
2565            block_bitmap_csum: bitmap_checksum(
2566                layout.csum_seed,
2567                &block_bitmaps[g as usize],
2568                EXT4_BLOCK_SIZE as usize,
2569            ),
2570            inode_bitmap_csum: bitmap_checksum(
2571                layout.csum_seed,
2572                &inode_bitmaps[g as usize],
2573                (EXT4_INODES_PER_GROUP / 8) as usize,
2574            ),
2575        };
2576        gdt.extend_from_slice(&build_group_descriptor(
2577            &geometry,
2578            g,
2579            &desc_stats,
2580            layout.csum_seed,
2581        ));
2582    }
2583
2584    Ok(gdt)
2585}
2586
2587/// Write the primary superblock block at the start of the image.
2588fn write_primary_superblock_at(
2589    file: &mut (impl std::io::Write + std::io::Seek),
2590    sb_block: &[u8],
2591) -> Result<(), Ext4Error> {
2592    file.seek(SeekFrom::Start(0))?;
2593    file.write_all(sb_block)?;
2594    Ok(())
2595}
2596
2597//--------------------------------------------------------------------------------------------------
2598// Re-Exports
2599//--------------------------------------------------------------------------------------------------
2600
2601pub use super::format::sparse_super_group;
2602
2603//--------------------------------------------------------------------------------------------------
2604// Tests
2605//--------------------------------------------------------------------------------------------------
2606
2607#[cfg(test)]
2608mod tests {
2609    use super::*;
2610    use std::io::{Read, Seek, SeekFrom};
2611    #[cfg(windows)]
2612    use std::os::windows::fs::MetadataExt;
2613
2614    #[cfg(windows)]
2615    use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SPARSE_FILE;
2616
2617    fn le_u16(bytes: &[u8], offset: usize) -> u16 {
2618        u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
2619    }
2620
2621    fn le_u32(bytes: &[u8], offset: usize) -> u32 {
2622        u32::from_le_bytes([
2623            bytes[offset],
2624            bytes[offset + 1],
2625            bytes[offset + 2],
2626            bytes[offset + 3],
2627        ])
2628    }
2629
2630    fn read_exact_at(path: &Path, offset: u64, len: usize) -> Vec<u8> {
2631        let mut file = std::fs::File::open(path).unwrap();
2632        file.seek(SeekFrom::Start(offset)).unwrap();
2633        let mut bytes = vec![0u8; len];
2634        file.read_exact(&mut bytes).unwrap();
2635        bytes
2636    }
2637
2638    fn assert_backup_superblock(path: &Path, layout: &Layout, group: u32) {
2639        assert!(sparse_super_group(group));
2640        let backup_offset = layout.group_start_block(group) * EXT4_BLOCK_SIZE as u64;
2641        let bytes = read_exact_at(path, backup_offset, 2048);
2642
2643        assert_eq!(le_u16(&bytes, 0x38), EXT4_SUPER_MAGIC);
2644        assert_eq!(le_u16(&bytes, 0x5A), group as u16);
2645        assert_ne!(le_u16(&bytes, 1024 + 0x38), EXT4_SUPER_MAGIC);
2646
2647        let mut sb = bytes[..1024].to_vec();
2648        let stored = le_u32(&sb, 0x3FC);
2649        put_le32(&mut sb, 0x3FC, 0);
2650        let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &sb[..0x3FC]);
2651
2652        assert_eq!(stored, expected);
2653    }
2654
2655    #[test]
2656    fn test_format_creates_file_of_correct_size() {
2657        let dir = tempfile::tempdir().unwrap();
2658        let path = dir.path().join("test.ext4");
2659
2660        let size: u64 = 256 * 1024 * 1024; // 256 MiB
2661        let opts = Ext4FormatOptions {
2662            size_bytes: size,
2663            journal_blocks: 4096, // 16 MiB journal
2664        };
2665
2666        format_ext4(&path, &opts).unwrap();
2667
2668        let meta = std::fs::metadata(&path).unwrap();
2669        assert_eq!(meta.len(), size);
2670    }
2671
2672    #[test]
2673    fn test_inode_allocation_crosses_block_group_boundary() {
2674        let opts = Ext4FormatOptions {
2675            size_bytes: 256 * 1024 * 1024,
2676            journal_blocks: 4096,
2677        };
2678        let layout = Layout::compute(&opts).unwrap();
2679        let plan = BitmapPlan {
2680            block_extents: Vec::new(),
2681            max_used_inode: EXT4_INODES_PER_GROUP + 1,
2682            group_used_dirs: vec![1, 0],
2683        };
2684
2685        let first_bitmap = build_inode_bitmap_for_plan(&layout, &plan, 0);
2686        let second_bitmap = build_inode_bitmap_for_plan(&layout, &plan, 1);
2687        assert!(
2688            first_bitmap[..(EXT4_INODES_PER_GROUP / 8) as usize]
2689                .iter()
2690                .all(|byte| *byte == 0xff)
2691        );
2692        assert_eq!(second_bitmap[0] & 0x01, 0x01);
2693        assert_eq!(second_bitmap[0] & 0xfe, 0);
2694
2695        assert_eq!(
2696            inode_offset(&layout, EXT4_INODES_PER_GROUP + 1),
2697            layout.group_inode_table_block(1) * EXT4_BLOCK_SIZE as u64
2698        );
2699    }
2700
2701    #[test]
2702    fn test_allocator_builds_depth_one_extent_tree_across_groups() {
2703        let opts = Ext4FormatOptions {
2704            size_bytes: 1024 * 1024 * 1024,
2705            journal_blocks: 4096,
2706        };
2707        let layout = Layout::compute(&opts).unwrap();
2708        let mut allocator = DataAllocator::new(&layout);
2709        let block_count = 150_000;
2710        let data_extents = allocator
2711            .allocate_extents(block_count, "/large-layer")
2712            .unwrap();
2713        assert!(data_extents.len() > INLINE_EXTENT_CAPACITY);
2714        assert_eq!(
2715            data_extents
2716                .iter()
2717                .map(|extent| extent.block_count)
2718                .sum::<u32>(),
2719            block_count
2720        );
2721        for pair in data_extents.windows(2) {
2722            assert_eq!(
2723                pair[1].logical_block,
2724                pair[0].logical_block + pair[0].block_count
2725            );
2726        }
2727
2728        let extent_tree_block = allocator.allocate_block("/large-layer").unwrap();
2729        let plan = NodePlan {
2730            inode: EXT4_FIRST_INO,
2731            path: "/large-layer".to_string(),
2732            metadata: InodeMetadata::default(),
2733            links_count: 1,
2734            kind: NodeKind::RegularFile {
2735                data: FileData::Memory(Vec::new()),
2736            },
2737            block_count,
2738            data_extents,
2739            extent_tree_block: Some(extent_tree_block),
2740            xattrs: Vec::new(),
2741            inline_xattrs: None,
2742            xattr_block: None,
2743        };
2744        let mut inode_block = [0u8; 60];
2745        write_extent_plan(&mut inode_block, 0, &plan).unwrap();
2746        assert_eq!(le_u16(&inode_block, 0), EXT4_EH_MAGIC);
2747        assert_eq!(le_u16(&inode_block, 2), 1);
2748        assert_eq!(le_u16(&inode_block, 6), 1);
2749
2750        let leaf = build_extent_leaf_block(&layout, &plan).unwrap();
2751        assert_eq!(le_u16(&leaf, 0), EXT4_EH_MAGIC);
2752        assert_eq!(le_u16(&leaf, 2) as usize, plan.data_extents.len());
2753        assert_eq!(le_u16(&leaf, 6), 0);
2754        let tail = EXT4_BLOCK_SIZE as usize - 4;
2755        assert_eq!(
2756            le_u32(&leaf, tail),
2757            dir_block_checksum(layout.csum_seed, plan.inode, 0, &leaf[..tail])
2758        );
2759    }
2760
2761    #[test]
2762    fn test_xattr_block_is_sorted_and_checksummed() {
2763        let opts = Ext4FormatOptions {
2764            size_bytes: 256 * 1024 * 1024,
2765            journal_blocks: 4096,
2766        };
2767        let layout = Layout::compute(&opts).unwrap();
2768        let block_number = layout.group_data_start_block(1);
2769        let xattrs = vec![
2770            Xattr {
2771                name: b"security.capability".to_vec(),
2772                value: (0u8..20).collect(),
2773            },
2774            Xattr {
2775                name: b"user.owner".to_vec(),
2776                value: b"microsandbox".to_vec(),
2777            },
2778        ];
2779
2780        let block = build_xattr_block(&layout, block_number, &xattrs, "/bin/tool").unwrap();
2781        assert_eq!(le_u32(&block, 0), EXT4_XATTR_MAGIC);
2782        assert_eq!(le_u32(&block, 4), 1);
2783        assert_eq!(le_u32(&block, 8), 1);
2784        assert_eq!(block[EXT4_XATTR_HEADER_SIZE + 1], 1);
2785        let second_entry = EXT4_XATTR_HEADER_SIZE + align_four(EXT4_XATTR_ENTRY_SIZE + 5);
2786        assert_eq!(block[second_entry + 1], 6);
2787
2788        let stored_checksum = le_u32(&block, 16);
2789        let mut checksum_input = block.clone();
2790        put_le32(&mut checksum_input, 16, 0);
2791        assert_eq!(
2792            stored_checksum,
2793            xattr_block_checksum(layout.csum_seed, block_number, &checksum_input)
2794        );
2795    }
2796
2797    #[test]
2798    fn test_small_xattr_is_encoded_in_inode_body() {
2799        let xattrs = vec![Xattr {
2800            name: b"security.capability".to_vec(),
2801            value: (0u8..20).collect(),
2802        }];
2803
2804        let bytes = build_inline_xattrs(&xattrs, "/bin/tool").unwrap().unwrap();
2805        assert_eq!(le_u32(&bytes, 0), EXT4_XATTR_MAGIC);
2806        assert_eq!(bytes[5], 6);
2807        assert_eq!(&bytes[20..30], b"capability");
2808        assert_eq!(le_u32(&bytes, 16), 0);
2809    }
2810
2811    #[test]
2812    fn test_format_too_small() {
2813        let dir = tempfile::tempdir().unwrap();
2814        let path = dir.path().join("tiny.ext4");
2815
2816        let opts = Ext4FormatOptions {
2817            size_bytes: 4096, // way too small
2818            journal_blocks: 16384,
2819        };
2820
2821        let result = format_ext4(&path, &opts);
2822        assert!(matches!(result, Err(Ext4Error::TooSmall)));
2823    }
2824
2825    #[test]
2826    fn test_layout_rejects_unaligned_size() {
2827        let opts = Ext4FormatOptions {
2828            size_bytes: DEFAULT_SIZE_BYTES + 1,
2829            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2830        };
2831
2832        let result = Layout::compute(&opts);
2833        assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
2834    }
2835
2836    #[test]
2837    fn test_layout_rejects_tiny_final_group() {
2838        let opts = Ext4FormatOptions {
2839            size_bytes: 128 * 1024 * 1024 + EXT4_BLOCK_SIZE as u64,
2840            journal_blocks: 4096,
2841        };
2842
2843        let result = Layout::compute(&opts);
2844        assert!(matches!(result, Err(Ext4Error::InvalidSize(_))));
2845    }
2846
2847    #[test]
2848    fn test_layout_rejects_size_beyond_32_bit_block_addresses() {
2849        let opts = Ext4FormatOptions {
2850            size_bytes: (MAX_BLOCKS + 1) * EXT4_BLOCK_SIZE as u64,
2851            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2852        };
2853
2854        let result = Layout::compute(&opts);
2855        assert!(matches!(result, Err(Ext4Error::TooLarge { .. })));
2856    }
2857
2858    #[test]
2859    fn test_layout_uses_actual_group_count_beyond_four_gib() {
2860        let opts = Ext4FormatOptions {
2861            size_bytes: 8 * 1024 * 1024 * 1024,
2862            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2863        };
2864
2865        let layout = Layout::compute(&opts).unwrap();
2866
2867        assert_eq!(layout.num_blocks, opts.size_bytes / EXT4_BLOCK_SIZE as u64);
2868        assert_eq!(layout.num_groups, 64);
2869        assert_eq!(layout.gdt_blocks, 1);
2870    }
2871
2872    #[test]
2873    fn test_format_default_options() {
2874        let dir = tempfile::tempdir().unwrap();
2875        let path = dir.path().join("default.ext4");
2876
2877        let opts = Ext4FormatOptions::default();
2878        format_ext4(&path, &opts).unwrap();
2879
2880        let meta = std::fs::metadata(&path).unwrap();
2881        assert_eq!(meta.len(), DEFAULT_SIZE_BYTES);
2882    }
2883
2884    #[test]
2885    fn test_superblock_magic() {
2886        let dir = tempfile::tempdir().unwrap();
2887        let path = dir.path().join("magic.ext4");
2888
2889        let opts = Ext4FormatOptions {
2890            size_bytes: 256 * 1024 * 1024,
2891            journal_blocks: 4096,
2892        };
2893        format_ext4(&path, &opts).unwrap();
2894
2895        // Read back and check magic number at offset 1024+0x38
2896        let data = std::fs::read(&path).unwrap();
2897        let magic = u16::from_le_bytes([data[1024 + 0x38], data[1024 + 0x39]]);
2898        assert_eq!(magic, EXT4_SUPER_MAGIC);
2899    }
2900
2901    #[test]
2902    fn test_journal_magic() {
2903        let dir = tempfile::tempdir().unwrap();
2904        let path = dir.path().join("journal.ext4");
2905
2906        let opts = Ext4FormatOptions {
2907            size_bytes: 256 * 1024 * 1024,
2908            journal_blocks: 4096,
2909        };
2910        format_ext4(&path, &opts).unwrap();
2911
2912        let layout = Layout::compute(&opts).unwrap();
2913        let data = std::fs::read(&path).unwrap();
2914
2915        // Journal superblock is at journal_start_block * 4096
2916        let jsb_offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
2917        let magic = u32::from_be_bytes([
2918            data[jsb_offset],
2919            data[jsb_offset + 1],
2920            data[jsb_offset + 2],
2921            data[jsb_offset + 3],
2922        ]);
2923        assert_eq!(magic, JBD2_MAGIC);
2924    }
2925
2926    #[test]
2927    fn test_root_dir_inode_exists() {
2928        let dir = tempfile::tempdir().unwrap();
2929        let path = dir.path().join("rootdir.ext4");
2930
2931        let opts = Ext4FormatOptions {
2932            size_bytes: 256 * 1024 * 1024,
2933            journal_blocks: 4096,
2934        };
2935        format_ext4(&path, &opts).unwrap();
2936
2937        let layout = Layout::compute(&opts).unwrap();
2938        let data = std::fs::read(&path).unwrap();
2939
2940        // Root inode at inode_table_block * 4096 + (2-1)*256
2941        let inode_offset = layout.group_inode_table_block(0) as usize * EXT4_BLOCK_SIZE as usize
2942            + (EXT4_INODE_SIZE as usize);
2943        let mode = u16::from_le_bytes([data[inode_offset], data[inode_offset + 1]]);
2944        assert_eq!(mode, S_IFDIR | 0o755);
2945    }
2946
2947    #[test]
2948    fn test_backup_group_bitmap_starts_after_backup_metadata() {
2949        let opts = Ext4FormatOptions {
2950            size_bytes: 256 * 1024 * 1024,
2951            journal_blocks: 4096,
2952        };
2953        let layout = Layout::compute(&opts).unwrap();
2954        let block_bitmaps = build_block_bitmaps(&layout);
2955        let inode_bitmaps = build_inode_bitmaps(&layout);
2956        let gdt = build_gdt(&layout, &block_bitmaps, &inode_bitmaps).unwrap();
2957
2958        let desc = &gdt[EXT4_DESC_SIZE as usize..(2 * EXT4_DESC_SIZE as usize)];
2959        let block_bitmap = u32::from_le_bytes([desc[0], desc[1], desc[2], desc[3]]) as u64;
2960        let group_start = layout.group_start_block(1);
2961
2962        assert_eq!(block_bitmap, layout.group_block_bitmap_block(1));
2963        assert!(block_bitmap > group_start + layout.gdt_blocks as u64 - 1);
2964    }
2965
2966    #[test]
2967    fn test_backup_superblock_is_written_at_backup_group_start() {
2968        let dir = tempfile::tempdir().unwrap();
2969        let path = dir.path().join("backup-super.ext4");
2970        let opts = Ext4FormatOptions {
2971            size_bytes: 256 * 1024 * 1024,
2972            journal_blocks: 4096,
2973        };
2974
2975        format_ext4(&path, &opts).unwrap();
2976
2977        let layout = Layout::compute(&opts).unwrap();
2978        assert_backup_superblock(&path, &layout, 1);
2979    }
2980
2981    #[test]
2982    fn test_backup_superblock_is_group_specific_in_sixteen_gib_image() {
2983        let dir = tempfile::tempdir().unwrap();
2984        let path = dir.path().join("backup-super-16g.ext4");
2985        let opts = Ext4FormatOptions {
2986            size_bytes: 16 * 1024 * 1024 * 1024,
2987            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
2988        };
2989
2990        format_ext4(&path, &opts).unwrap();
2991
2992        let layout = Layout::compute(&opts).unwrap();
2993        assert_eq!(layout.num_groups, 128);
2994        assert_backup_superblock(&path, &layout, 81);
2995    }
2996
2997    #[test]
2998    fn test_format_sparse_image_larger_than_four_gib() {
2999        let dir = tempfile::tempdir().unwrap();
3000        let path = dir.path().join("large.ext4");
3001        let opts = Ext4FormatOptions {
3002            size_bytes: 8 * 1024 * 1024 * 1024,
3003            journal_blocks: DEFAULT_JOURNAL_BLOCKS,
3004        };
3005
3006        format_ext4(&path, &opts).unwrap();
3007
3008        let meta = std::fs::metadata(&path).unwrap();
3009        assert_eq!(meta.len(), opts.size_bytes);
3010
3011        let layout = Layout::compute(&opts).unwrap();
3012        let sb = read_exact_at(&path, 1024, 1024);
3013        let blocks = le_u32(&sb, 0x04) as u64 | ((le_u32(&sb, 0x150) as u64) << 32);
3014        let inodes = le_u32(&sb, 0x00);
3015        assert_eq!(blocks, layout.num_blocks);
3016        assert_eq!(inodes, layout.num_groups * EXT4_INODES_PER_GROUP);
3017
3018        let desc_offset = EXT4_BLOCK_SIZE as u64 + 63 * EXT4_DESC_SIZE as u64;
3019        let desc = read_exact_at(&path, desc_offset, EXT4_DESC_SIZE as usize);
3020        let block_bitmap = le_u32(&desc, 0x00) as u64 | ((le_u32(&desc, 0x20) as u64) << 32);
3021        let inode_bitmap = le_u32(&desc, 0x04) as u64 | ((le_u32(&desc, 0x24) as u64) << 32);
3022
3023        assert_eq!(block_bitmap, layout.group_block_bitmap_block(63));
3024        assert_eq!(inode_bitmap, layout.group_inode_bitmap_block(63));
3025    }
3026
3027    #[cfg(windows)]
3028    #[test]
3029    fn test_format_marks_image_sparse_on_windows() {
3030        let dir = tempfile::tempdir().unwrap();
3031        let path = dir.path().join("sparse.ext4");
3032        let opts = Ext4FormatOptions {
3033            size_bytes: 256 * 1024 * 1024,
3034            journal_blocks: 4096,
3035        };
3036
3037        format_ext4(&path, &opts).unwrap();
3038
3039        let meta = std::fs::metadata(&path).unwrap();
3040        assert_eq!(meta.len(), opts.size_bytes);
3041        assert_ne!(meta.file_attributes() & FILE_ATTRIBUTE_SPARSE_FILE, 0);
3042    }
3043
3044    #[test]
3045    fn test_inode_bitmap_padding_is_marked_used() {
3046        let layout = Layout::compute(&Ext4FormatOptions {
3047            size_bytes: 256 * 1024 * 1024,
3048            journal_blocks: 4096,
3049        })
3050        .unwrap();
3051
3052        let bitmap = build_inode_bitmap(&layout, 0);
3053        for bit in EXT4_INODES_PER_GROUP..(EXT4_BLOCK_SIZE * 8) {
3054            assert_ne!(bitmap[(bit / 8) as usize] & (1 << (bit % 8)), 0);
3055        }
3056    }
3057
3058    #[test]
3059    fn test_journal_superblock_checksum_matches_contents() {
3060        let dir = tempfile::tempdir().unwrap();
3061        let path = dir.path().join("journal-csum.ext4");
3062        let opts = Ext4FormatOptions {
3063            size_bytes: 256 * 1024 * 1024,
3064            journal_blocks: 4096,
3065        };
3066
3067        format_ext4(&path, &opts).unwrap();
3068
3069        let layout = Layout::compute(&opts).unwrap();
3070        let data = std::fs::read(&path).unwrap();
3071        let offset = layout.journal_start_block as usize * EXT4_BLOCK_SIZE as usize;
3072        let mut jsb = data[offset..offset + JBD2_SUPERBLOCK_SIZE].to_vec();
3073        let stored = u32::from_be_bytes([jsb[0xFC], jsb[0xFD], jsb[0xFE], jsb[0xFF]]);
3074
3075        jsb[0xFC..0x100].fill(0);
3076        let expected = crc32c::crc32c_raw(0xFFFF_FFFF, &jsb);
3077
3078        assert_eq!(stored, expected);
3079    }
3080
3081    #[test]
3082    fn test_root_dir_checksum_matches_contents() {
3083        let dir = tempfile::tempdir().unwrap();
3084        let path = dir.path().join("rootdir-csum.ext4");
3085        let opts = Ext4FormatOptions {
3086            size_bytes: 256 * 1024 * 1024,
3087            journal_blocks: 4096,
3088        };
3089
3090        format_ext4(&path, &opts).unwrap();
3091
3092        let layout = Layout::compute(&opts).unwrap();
3093        let data = std::fs::read(&path).unwrap();
3094        let sb = &data[1024..2048];
3095        let uuid = &sb[0x68..0x78];
3096        let csum_seed = crc32c::crc32c_raw(0xFFFF_FFFF, uuid);
3097        let block_offset = layout.first_data_block as usize * EXT4_BLOCK_SIZE as usize;
3098        let tail_offset = block_offset + EXT4_BLOCK_SIZE as usize - 12;
3099        let stored = u32::from_le_bytes([
3100            data[tail_offset + 8],
3101            data[tail_offset + 9],
3102            data[tail_offset + 10],
3103            data[tail_offset + 11],
3104        ]);
3105        let expected = dir_block_checksum(
3106            csum_seed,
3107            EXT4_ROOT_INO,
3108            0,
3109            &data[block_offset..tail_offset],
3110        );
3111
3112        assert_eq!(stored, expected);
3113    }
3114}