Skip to main content

microsandbox_image/ext4/
formatter.rs

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