Skip to main content

microsandbox_image/erofs/
writer.rs

1use std::collections::{BTreeMap, HashMap};
2use std::ffi::OsString;
3use std::io::{self, BufWriter, Seek, SeekFrom, Write};
4use std::os::unix::ffi::OsStrExt;
5use std::path::{Path, PathBuf};
6
7use crate::crc32c;
8use crate::tree::{DirectoryNode, FileTree, InodeMetadata, RegularFileId, TreeNode, Xattr};
9
10use super::format::{
11    self, EROFS_BLKSIZ, EROFS_BLKSIZ_BITS, EROFS_DIRENT_SIZE, EROFS_FEATURE_COMPAT_SB_CHKSUM,
12    EROFS_INODE_EXTENDED_SIZE, EROFS_INODE_FLAT_INLINE, EROFS_INODE_FLAT_PLAIN, EROFS_ISLOT_SIZE,
13    EROFS_NULL_ADDR, EROFS_SUPER_MAGIC, EROFS_SUPER_OFFSET, EROFS_SUPERBLOCK_SIZE,
14    EROFS_XATTR_IBODY_HEADER_SIZE, dirent_file_type, erofs_xattr_align, mode_type_bits,
15    new_encode_dev, xattr_prefix_index,
16};
17
18//--------------------------------------------------------------------------------------------------
19// Constants
20//--------------------------------------------------------------------------------------------------
21
22/// Stack-allocated zero buffer for padding writes (avoids heap allocation per pad).
23static ZEROS: [u8; 4096] = [0u8; 4096];
24
25//--------------------------------------------------------------------------------------------------
26// Types
27//--------------------------------------------------------------------------------------------------
28
29#[derive(Debug)]
30pub enum ErofsError {
31    Io(io::Error),
32    NidOverflow,
33    UnsupportedXattrPrefix,
34}
35
36/// Data layout map returned by [`write_erofs()`].
37///
38/// Records where each file's data blocks were placed in the output image,
39/// consumed by the fsmeta writer to build chunk-based inodes.
40#[derive(Debug, Clone)]
41pub struct ErofsDataMap {
42    /// For each file path: `(start_block, size_bytes)` within the output image.
43    pub file_blocks: HashMap<PathBuf, (u32, u64)>,
44    /// Total block count of the output image.
45    pub total_blocks: u32,
46}
47
48#[allow(dead_code)]
49struct InodePlan {
50    nid: u32,
51    data_layout: u8,
52    data_block_start: u32,
53    data_block_count: u32,
54    inline_tail_size: u32,
55    xattr_ibody_size: u32,
56    total_inode_size: u32,
57    slots: u32,
58    dir_data: Option<Vec<u8>>,
59    parent_nid: u32,
60}
61
62#[allow(dead_code)]
63struct LayoutState {
64    plans: Vec<InodePlan>,
65    regular_file_plans: HashMap<RegularFileId, usize>,
66    regular_file_link_counts: HashMap<RegularFileId, u32>,
67    current_meta_offset: u64,
68    current_data_block: u32,
69    meta_blkaddr: u32,
70    root_nid: u32,
71    inode_count: u64,
72}
73
74pub(super) struct CursorTrackingWriter<'a, W> {
75    inner: &'a mut W,
76    cursor: &'a mut u64,
77}
78
79//--------------------------------------------------------------------------------------------------
80// Methods
81//--------------------------------------------------------------------------------------------------
82
83impl LayoutState {
84    fn new() -> Self {
85        Self {
86            plans: Vec::new(),
87            regular_file_plans: HashMap::new(),
88            regular_file_link_counts: HashMap::new(),
89            current_meta_offset: EROFS_BLKSIZ as u64,
90            current_data_block: 0,
91            meta_blkaddr: 1,
92            root_nid: 0,
93            inode_count: 0,
94        }
95    }
96}
97
98impl<'a, W> CursorTrackingWriter<'a, W> {
99    pub(super) fn new(inner: &'a mut W, cursor: &'a mut u64) -> Self {
100        Self { inner, cursor }
101    }
102}
103
104//--------------------------------------------------------------------------------------------------
105// Trait Implementations
106//--------------------------------------------------------------------------------------------------
107
108impl std::fmt::Display for ErofsError {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self {
111            ErofsError::Io(e) => write!(f, "I/O error: {e}"),
112            ErofsError::NidOverflow => write!(f, "root NID exceeds u16::MAX"),
113            ErofsError::UnsupportedXattrPrefix => write!(f, "unsupported xattr prefix"),
114        }
115    }
116}
117
118impl std::error::Error for ErofsError {
119    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
120        match self {
121            ErofsError::Io(e) => Some(e),
122            _ => None,
123        }
124    }
125}
126
127impl From<io::Error> for ErofsError {
128    fn from(e: io::Error) -> Self {
129        ErofsError::Io(e)
130    }
131}
132
133impl<W: Write> Write for CursorTrackingWriter<'_, W> {
134    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
135        let written = self.inner.write(buf)?;
136        *self.cursor += written as u64;
137        Ok(written)
138    }
139
140    fn flush(&mut self) -> io::Result<()> {
141        self.inner.flush()
142    }
143}
144
145//--------------------------------------------------------------------------------------------------
146// Functions
147//--------------------------------------------------------------------------------------------------
148
149pub fn write_erofs(tree: &FileTree, output: &Path) -> Result<ErofsDataMap, ErofsError> {
150    let mut file = BufWriter::new(std::fs::File::create(output)?);
151    let mut state = LayoutState::new();
152
153    // Phase 1+2: Plan layout and assign NIDs.
154    plan_directory(&tree.root, 0, &mut state, true)?;
155    state.root_nid = state.plans[0].nid;
156
157    // Phase 3: Write data blocks and collect data map.
158    let regular_file_paths = state
159        .regular_file_link_counts
160        .values()
161        .map(|count| *count as usize)
162        .sum();
163    let mut data_map = HashMap::with_capacity(regular_file_paths);
164    write_data_blocks(&mut file, &state, tree, &mut data_map)?;
165
166    // Phase 4: Write metadata.
167    write_metadata(&mut file, &state, tree)?;
168
169    // Phase 5: Write superblock.
170    write_superblock(&mut file, &state)?;
171
172    // Flush buffered writes, then pad the file to a whole number of 4096-byte
173    // EROFS blocks. Also ensure 512-byte sector alignment for virtio-blk.
174    file.flush()?;
175
176    // stream_position() may not reflect the true file length after seeks,
177    // so seek to end to get the actual length.
178    let current_len = file.seek(SeekFrom::End(0))?;
179    let block_aligned = align_to_block(current_len);
180    // Also align to 512-byte sectors for virtio-blk.
181    let sector_aligned = block_aligned.div_ceil(512) * 512;
182    let target_len = sector_aligned.max(block_aligned);
183
184    if target_len > current_len {
185        file.seek(SeekFrom::Start(target_len - 1))?;
186        file.write_all(&[0u8])?;
187        file.flush()?;
188    }
189
190    // Compute total blocks from the final file size.
191    let final_len = file.seek(SeekFrom::End(0))?;
192    let total_blocks = (final_len / EROFS_BLKSIZ as u64) as u32;
193
194    Ok(ErofsDataMap {
195        file_blocks: data_map,
196        total_blocks,
197    })
198}
199
200pub(super) fn compute_xattr_ibody_size(xattrs: &[Xattr]) -> Result<u32, ErofsError> {
201    if xattrs.is_empty() {
202        return Ok(0);
203    }
204
205    let mut size = EROFS_XATTR_IBODY_HEADER_SIZE as usize;
206    for xattr in xattrs {
207        let (_, suffix) =
208            xattr_prefix_index(&xattr.name).ok_or(ErofsError::UnsupportedXattrPrefix)?;
209        // erofs_xattr_entry (4 bytes) + suffix name + value, aligned to 4
210        let entry_size = 4 + suffix.len() + xattr.value.len();
211        size += erofs_xattr_align(entry_size);
212    }
213
214    Ok(size as u32)
215}
216
217pub(super) fn compute_xattr_icount(xattr_ibody_size: u32) -> u16 {
218    if xattr_ibody_size == 0 {
219        0
220    } else {
221        ((xattr_ibody_size - EROFS_XATTR_IBODY_HEADER_SIZE) / 4 + 1) as u16
222    }
223}
224
225/// Layout decision result for an inode's data storage strategy.
226pub(super) struct DataLayoutDecision {
227    pub(super) layout: u8,
228    pub(super) inline_tail_size: u32,
229    pub(super) block_count: u32,
230    pub(super) block_start: u32,
231}
232
233/// Decide between FLAT_PLAIN and FLAT_INLINE for an inode's data.
234///
235/// FLAT_INLINE stores the tail (< block_size remainder) immediately after
236/// the inode metadata, saving a data block. Falls back to FLAT_PLAIN
237/// (with a padded last block) if the tail doesn't fit in the current
238/// metadata block alongside the inode.
239///
240/// `allow_inline` must be false for regular files — the fsmeta references
241/// each full block via a chunk index by block address, so the tail must
242/// live in a real data block (padded) rather than being inlined in the
243/// per-layer EROFS metadata area, which fsmeta cannot reach.
244pub(super) fn decide_data_layout(
245    data_size: u64,
246    inode_fixed_size: u32,
247    meta_offset: u64,
248    current_data_block: &mut u32,
249    allow_inline: bool,
250) -> DataLayoutDecision {
251    let blksiz = EROFS_BLKSIZ as u64;
252    let tail_size = data_size % blksiz;
253    let full_blocks = data_size / blksiz;
254
255    if data_size == 0 {
256        DataLayoutDecision {
257            layout: EROFS_INODE_FLAT_PLAIN,
258            inline_tail_size: 0,
259            block_count: 0,
260            block_start: EROFS_NULL_ADDR,
261        }
262    } else if tail_size == 0 {
263        let start = *current_data_block;
264        *current_data_block += full_blocks as u32;
265        DataLayoutDecision {
266            layout: EROFS_INODE_FLAT_PLAIN,
267            inline_tail_size: 0,
268            block_count: full_blocks as u32,
269            block_start: start,
270        }
271    } else if allow_inline {
272        let inode_pos_in_block = meta_offset % blksiz;
273        let remaining_in_block = blksiz - inode_pos_in_block;
274        let needed = inode_fixed_size as u64 + tail_size;
275
276        if needed <= remaining_in_block {
277            let start = if full_blocks > 0 {
278                let s = *current_data_block;
279                *current_data_block += full_blocks as u32;
280                s
281            } else {
282                EROFS_NULL_ADDR
283            };
284            DataLayoutDecision {
285                layout: EROFS_INODE_FLAT_INLINE,
286                inline_tail_size: tail_size as u32,
287                block_count: full_blocks as u32,
288                block_start: start,
289            }
290        } else {
291            let start = *current_data_block;
292            *current_data_block += (full_blocks + 1) as u32;
293            DataLayoutDecision {
294                layout: EROFS_INODE_FLAT_PLAIN,
295                inline_tail_size: 0,
296                block_count: (full_blocks + 1) as u32,
297                block_start: start,
298            }
299        }
300    } else {
301        let start = *current_data_block;
302        *current_data_block += (full_blocks + 1) as u32;
303        DataLayoutDecision {
304            layout: EROFS_INODE_FLAT_PLAIN,
305            inline_tail_size: 0,
306            block_count: (full_blocks + 1) as u32,
307            block_start: start,
308        }
309    }
310}
311
312pub(super) fn compute_dir_data_size(dir: &DirectoryNode) -> u32 {
313    // Total entries = 2 (. and ..) + number of children
314    let entry_count = 2 + dir.entries.len();
315
316    // Collect all names to determine block packing
317    let mut names: Vec<&[u8]> = Vec::with_capacity(entry_count);
318    names.push(b".");
319    names.push(b"..");
320    for name in dir.entries.keys() {
321        names.push(name.as_bytes());
322    }
323    // EROFS requires dirents sorted by name in byte order.
324    names.sort();
325
326    // Pack entries into blocks. Each block is EROFS_BLKSIZ bytes.
327    // Block layout: dirents first, then names.
328    let blksiz = EROFS_BLKSIZ as usize;
329    let mut total_size = 0usize;
330    let mut idx = 0;
331
332    while idx < names.len() {
333        // Figure out how many entries fit in this block
334        let mut block_entries = 0;
335        let mut dirent_area = 0usize;
336        let mut name_area = 0usize;
337
338        for name in &names[idx..] {
339            let new_dirent_area = (block_entries + 1) * EROFS_DIRENT_SIZE as usize;
340            let new_name_area = name_area + name.len();
341            if new_dirent_area + new_name_area > blksiz {
342                break;
343            }
344            dirent_area = new_dirent_area;
345            name_area = new_name_area;
346            block_entries += 1;
347        }
348
349        if block_entries == 0 {
350            // Single entry that's too big shouldn't happen for reasonable names
351            block_entries = 1;
352            name_area = names[idx].len();
353            dirent_area = EROFS_DIRENT_SIZE as usize;
354        }
355
356        let used = dirent_area + name_area;
357        // Last block: size is the used portion (not padded to block boundary for inline)
358        // But for data blocks, we pad. We'll track the actual used size.
359        // For sizing purposes, non-last blocks are full blocks.
360        if idx + block_entries < names.len() {
361            total_size += blksiz;
362        } else {
363            total_size += used;
364        }
365
366        idx += block_entries;
367    }
368
369    total_size as u32
370}
371
372/// Serialize directory entries into EROFS directory data blocks.
373///
374/// EROFS directory blocks are self-contained: each block packs 12-byte
375/// dirent headers at the start followed by the concatenated name strings.
376/// `dirent[0].nameoff / 12` tells the kernel how many entries are in the
377/// block, so the first nameoff must equal the total dirent header area.
378///
379/// Entries are sorted alphabetically by name (the kernel binary-searches
380/// within each block). `.` and `..` are always the first two entries.
381///
382/// The last block may be shorter than 4096 bytes — it will be stored
383/// inline after the inode if the layout planner chose FLAT_INLINE.
384pub(super) fn serialize_dir_blocks(
385    dir: &DirectoryNode,
386    own_nid: u32,
387    parent_nid: u32,
388    child_nids: &BTreeMap<OsString, u32>,
389) -> Result<Vec<u8>, ErofsError> {
390    struct DirEntryInfo {
391        name: Vec<u8>,
392        nid: u64,
393        file_type: u8,
394    }
395
396    let mut entries: Vec<DirEntryInfo> = Vec::new();
397
398    entries.push(DirEntryInfo {
399        name: b".".to_vec(),
400        nid: own_nid as u64,
401        file_type: format::EROFS_FT_DIR,
402    });
403    entries.push(DirEntryInfo {
404        name: b"..".to_vec(),
405        nid: parent_nid as u64,
406        file_type: format::EROFS_FT_DIR,
407    });
408
409    for (name, child) in &dir.entries {
410        let nid = *child_nids.get(name).expect("child NID not found") as u64;
411        entries.push(DirEntryInfo {
412            name: name.as_bytes().to_vec(),
413            nid,
414            file_type: dirent_file_type(child),
415        });
416    }
417
418    // EROFS requires dirents sorted by name in byte order (memcmp).
419    entries.sort_by(|a, b| a.name.cmp(&b.name));
420
421    let blksiz = EROFS_BLKSIZ as usize;
422    let mut result = Vec::new();
423    let mut idx = 0;
424
425    while idx < entries.len() {
426        // Determine how many entries fit in this block
427        let mut block_entries = 0usize;
428        let mut name_total = 0usize;
429
430        for entry in &entries[idx..] {
431            let new_dirent_area = (block_entries + 1) * EROFS_DIRENT_SIZE as usize;
432            let new_name_total = name_total + entry.name.len();
433            if new_dirent_area + new_name_total > blksiz {
434                break;
435            }
436            name_total += entry.name.len();
437            block_entries += 1;
438        }
439
440        if block_entries == 0 {
441            block_entries = 1;
442            name_total = entries[idx].name.len();
443        }
444
445        let dirent_area_size = block_entries * EROFS_DIRENT_SIZE as usize;
446        let is_last_block = idx + block_entries >= entries.len();
447
448        // Build this block
449        let mut block = vec![
450            0u8;
451            if is_last_block {
452                dirent_area_size + name_total
453            } else {
454                blksiz
455            }
456        ];
457
458        // Write dirents
459        let mut name_offset = dirent_area_size;
460        for i in 0..block_entries {
461            let e = &entries[idx + i];
462            let dirent_off = i * EROFS_DIRENT_SIZE as usize;
463
464            // nid: u64 at offset 0
465            block[dirent_off..dirent_off + 8].copy_from_slice(&e.nid.to_le_bytes());
466            // nameoff: u16 at offset 8
467            block[dirent_off + 8..dirent_off + 10]
468                .copy_from_slice(&(name_offset as u16).to_le_bytes());
469            // file_type: u8 at offset 10
470            block[dirent_off + 10] = e.file_type;
471            // reserved: u8 at offset 11
472            block[dirent_off + 11] = 0;
473
474            // Write name
475            block[name_offset..name_offset + e.name.len()].copy_from_slice(&e.name);
476            name_offset += e.name.len();
477        }
478
479        result.extend_from_slice(&block);
480        idx += block_entries;
481    }
482
483    Ok(result)
484}
485
486pub(super) fn node_data_size(node: &TreeNode) -> u64 {
487    match node {
488        TreeNode::RegularFile(f) => f.data.len() as u64,
489        TreeNode::Symlink(s) => s.target.len() as u64,
490        _ => 0,
491    }
492}
493
494pub(super) fn node_xattrs(node: &TreeNode) -> &[Xattr] {
495    match node {
496        TreeNode::RegularFile(f) => &f.xattrs,
497        TreeNode::Directory(d) => &d.xattrs,
498        _ => &[],
499    }
500}
501
502pub(super) fn node_metadata(node: &TreeNode) -> &InodeMetadata {
503    match node {
504        TreeNode::RegularFile(f) => &f.metadata,
505        TreeNode::Directory(d) => &d.metadata,
506        TreeNode::Symlink(s) => &s.metadata,
507        TreeNode::CharDevice(d) => &d.metadata,
508        TreeNode::BlockDevice(d) => &d.metadata,
509        TreeNode::Fifo(m) => m,
510        TreeNode::Socket(m) => m,
511    }
512}
513
514pub(super) fn node_nlink(
515    node: &TreeNode,
516    regular_file_link_counts: &HashMap<RegularFileId, u32>,
517) -> u32 {
518    match node {
519        TreeNode::RegularFile(f) => regular_file_link_counts
520            .get(&f.id)
521            .copied()
522            .unwrap_or(f.nlink.max(1)),
523        TreeNode::Directory(d) => {
524            // nlink for directory = 2 + number of child directories
525            let child_dirs = d
526                .entries
527                .values()
528                .filter(|c| matches!(c, TreeNode::Directory(_)))
529                .count();
530            2 + child_dirs as u32
531        }
532        _ => 1,
533    }
534}
535
536/// Recursive function that plans the layout for a directory and all its descendants.
537/// Assigns NIDs, computes data layouts, and tracks data block assignments.
538fn plan_directory(
539    dir: &DirectoryNode,
540    parent_nid: u32,
541    state: &mut LayoutState,
542    is_root: bool,
543) -> Result<u32, ErofsError> {
544    let blksiz = EROFS_BLKSIZ as u64;
545
546    // Reserve an index for this directory's plan
547    let dir_plan_idx = state.plans.len();
548    state.plans.push(InodePlan {
549        nid: 0,
550        data_layout: 0,
551        data_block_start: 0,
552        data_block_count: 0,
553        inline_tail_size: 0,
554        xattr_ibody_size: 0,
555        total_inode_size: 0,
556        slots: 0,
557        dir_data: None,
558        parent_nid,
559    });
560    state.inode_count += 1;
561
562    // Compute xattr ibody size for the directory
563    let xattr_ibody_size = compute_xattr_ibody_size(&dir.xattrs)?;
564
565    // Compute directory data size
566    let dir_data_size = compute_dir_data_size(dir) as u64;
567
568    // Determine data layout for this directory
569    let inode_fixed_size = EROFS_INODE_EXTENDED_SIZE + xattr_ibody_size;
570
571    // Assign NID for this directory
572    let meta_base = state.meta_blkaddr as u64 * blksiz;
573    let nid_offset = state.current_meta_offset - meta_base;
574    if !nid_offset.is_multiple_of(EROFS_ISLOT_SIZE as u64) {
575        // Align to slot boundary
576        let aligned = nid_offset.div_ceil(EROFS_ISLOT_SIZE as u64) * EROFS_ISLOT_SIZE as u64;
577        state.current_meta_offset = meta_base + aligned;
578    }
579
580    let nid_offset = state.current_meta_offset - meta_base;
581    let nid = (nid_offset / EROFS_ISLOT_SIZE as u64) as u32;
582
583    let d = decide_data_layout(
584        dir_data_size,
585        inode_fixed_size,
586        state.current_meta_offset,
587        &mut state.current_data_block,
588        true, // directories: fsmeta builds its own, layer dir data is unreferenced
589    );
590    let (data_layout, inline_tail_size, data_block_count, data_block_start) =
591        (d.layout, d.inline_tail_size, d.block_count, d.block_start);
592
593    let total_inode_size = inode_fixed_size + inline_tail_size;
594    let slots = total_inode_size.div_ceil(EROFS_ISLOT_SIZE);
595
596    state.current_meta_offset += (slots * EROFS_ISLOT_SIZE) as u64;
597
598    // Update the plan
599    state.plans[dir_plan_idx] = InodePlan {
600        nid,
601        data_layout,
602        data_block_start,
603        data_block_count,
604        inline_tail_size,
605        xattr_ibody_size,
606        total_inode_size,
607        slots,
608        dir_data: None,
609        parent_nid: if is_root { nid } else { parent_nid },
610    };
611
612    let dir_nid = nid;
613
614    // Now plan children (depth-first)
615    let mut child_nids: BTreeMap<OsString, u32> = BTreeMap::new();
616
617    for (name, child) in &dir.entries {
618        let child_nid = match child {
619            TreeNode::Directory(child_dir) => plan_directory(child_dir, dir_nid, state, false)?,
620            TreeNode::RegularFile(file) => plan_regular_file(child, file.id, state)?,
621            _ => plan_leaf_node(child, state)?,
622        };
623        child_nids.insert(name.clone(), child_nid);
624    }
625
626    // Now serialize directory data with real NIDs
627    let dir_data = serialize_dir_blocks(
628        dir,
629        dir_nid,
630        state.plans[dir_plan_idx].parent_nid,
631        &child_nids,
632    )?;
633    state.plans[dir_plan_idx].dir_data = Some(dir_data);
634
635    Ok(dir_nid)
636}
637
638fn plan_regular_file(
639    node: &TreeNode,
640    file_id: RegularFileId,
641    state: &mut LayoutState,
642) -> Result<u32, ErofsError> {
643    *state.regular_file_link_counts.entry(file_id).or_insert(0) += 1;
644
645    if let Some(plan_idx) = state.regular_file_plans.get(&file_id) {
646        return Ok(state.plans[*plan_idx].nid);
647    }
648
649    let plan_idx = state.plans.len();
650    let nid = plan_leaf_node(node, state)?;
651    state.regular_file_plans.insert(file_id, plan_idx);
652    Ok(nid)
653}
654
655fn plan_leaf_node(node: &TreeNode, state: &mut LayoutState) -> Result<u32, ErofsError> {
656    let blksiz = EROFS_BLKSIZ as u64;
657
658    let plan_idx = state.plans.len();
659    state.plans.push(InodePlan {
660        nid: 0,
661        data_layout: 0,
662        data_block_start: 0,
663        data_block_count: 0,
664        inline_tail_size: 0,
665        xattr_ibody_size: 0,
666        total_inode_size: 0,
667        slots: 0,
668        dir_data: None,
669        parent_nid: 0,
670    });
671    state.inode_count += 1;
672
673    let xattrs = node_xattrs(node);
674    let xattr_ibody_size = compute_xattr_ibody_size(xattrs)?;
675    let data_size = node_data_size(node);
676    let inode_fixed_size = EROFS_INODE_EXTENDED_SIZE + xattr_ibody_size;
677
678    // Assign NID
679    let meta_base = state.meta_blkaddr as u64 * blksiz;
680    let nid_offset = state.current_meta_offset - meta_base;
681    let aligned_offset = nid_offset.div_ceil(EROFS_ISLOT_SIZE as u64) * EROFS_ISLOT_SIZE as u64;
682    state.current_meta_offset = meta_base + aligned_offset;
683
684    let nid = (aligned_offset / EROFS_ISLOT_SIZE as u64) as u32;
685
686    // Regular files must use FLAT_PLAIN so every block is addressable by
687    // fsmeta chunk indexes. Symlinks/devices/fifos/sockets can still inline
688    // since fsmeta reads their content from the tree directly.
689    let allow_inline = !matches!(node, TreeNode::RegularFile(_));
690    let d = decide_data_layout(
691        data_size,
692        inode_fixed_size,
693        state.current_meta_offset,
694        &mut state.current_data_block,
695        allow_inline,
696    );
697    let (data_layout, inline_tail_size, data_block_count, data_block_start) =
698        (d.layout, d.inline_tail_size, d.block_count, d.block_start);
699
700    let total_inode_size = inode_fixed_size + inline_tail_size;
701    let slots = total_inode_size.div_ceil(EROFS_ISLOT_SIZE);
702
703    state.current_meta_offset += (slots * EROFS_ISLOT_SIZE) as u64;
704
705    state.plans[plan_idx] = InodePlan {
706        nid,
707        data_layout,
708        data_block_start,
709        data_block_count,
710        inline_tail_size,
711        xattr_ibody_size,
712        total_inode_size,
713        slots,
714        dir_data: None,
715        parent_nid: 0,
716    };
717
718    Ok(nid)
719}
720
721pub(super) fn write_zero_padding_to(
722    file: &mut impl Write,
723    cursor: &mut u64,
724    target: u64,
725) -> Result<(), ErofsError> {
726    if target < *cursor {
727        return Err(ErofsError::Io(io::Error::other(
728            "EROFS layout cursor moved backwards",
729        )));
730    }
731
732    while *cursor < target {
733        let remaining = (target - *cursor) as usize;
734        let to_write = remaining.min(ZEROS.len());
735        file.write_all(&ZEROS[..to_write])?;
736        *cursor += to_write as u64;
737    }
738
739    Ok(())
740}
741
742fn write_data_blocks(
743    file: &mut (impl Write + Seek),
744    state: &LayoutState,
745    tree: &FileTree,
746    data_map: &mut HashMap<PathBuf, (u32, u64)>,
747) -> Result<(), ErofsError> {
748    // Compute where data area starts: after the metadata area
749    // The metadata area ends at current_meta_offset, rounded up to block boundary
750    let meta_end = align_to_block(state.current_meta_offset);
751    let data_area_start = meta_end;
752
753    // We need to figure out the data block base. The data blocks are numbered
754    // starting from the block after metadata.
755    // Actually, data_block_start in each plan is an absolute block number.
756    // We need to determine: what block number does the data area start at?
757    let data_start_block = (data_area_start / EROFS_BLKSIZ as u64) as u32;
758
759    // Wait - we assigned current_data_block starting from 0 during planning,
760    // but we need them to be absolute block numbers. Let's fix this.
761    // Actually, looking at the plan again: data_block_start should be absolute.
762    // During planning, we started current_data_block at 0, which means they're
763    // relative to the data area. We need to add the data_start_block offset.
764
765    file.seek(SeekFrom::Start(data_area_start))?;
766    let mut data_cursor = data_area_start;
767
768    let current_path = PathBuf::new();
769    write_data_for_tree(
770        file,
771        state,
772        &tree.root,
773        data_start_block,
774        &mut 0,
775        &current_path,
776        data_map,
777        &mut data_cursor,
778    )?;
779
780    Ok(())
781}
782
783#[allow(clippy::too_many_arguments)]
784fn write_data_for_tree(
785    file: &mut (impl Write + Seek),
786    state: &LayoutState,
787    dir: &DirectoryNode,
788    data_start_block: u32,
789    plan_idx: &mut usize,
790    current_path: &Path,
791    data_map: &mut HashMap<PathBuf, (u32, u64)>,
792    data_cursor: &mut u64,
793) -> Result<(), ErofsError> {
794    let blksiz = EROFS_BLKSIZ as u64;
795    let plan = &state.plans[*plan_idx];
796    *plan_idx += 1;
797
798    // Write directory data blocks (non-inline portion)
799    if let Some(ref dir_data) = plan.dir_data
800        && plan.data_block_count > 0
801    {
802        let abs_block = data_start_block + plan.data_block_start;
803        let offset = abs_block as u64 * blksiz;
804        write_zero_padding_to(file, data_cursor, offset)?;
805        let mut tracked = CursorTrackingWriter::new(file, data_cursor);
806
807        let full_block_bytes = plan.data_block_count as usize * EROFS_BLKSIZ as usize;
808        let data_to_write = &dir_data[..std::cmp::min(full_block_bytes, dir_data.len())];
809        tracked.write_all(data_to_write)?;
810
811        // Pad remaining space in last full block if needed
812        if data_to_write.len() < full_block_bytes {
813            let pad = full_block_bytes - data_to_write.len();
814            tracked.write_all(&ZEROS[..pad])?;
815        }
816    }
817
818    // Recurse into children in BTreeMap order
819    for (name, child) in &dir.entries {
820        let child_path = current_path.join(name);
821        match child {
822            TreeNode::Directory(child_dir) => {
823                write_data_for_tree(
824                    file,
825                    state,
826                    child_dir,
827                    data_start_block,
828                    plan_idx,
829                    &child_path,
830                    data_map,
831                    data_cursor,
832                )?;
833            }
834            TreeNode::RegularFile(f) => {
835                let child_plan_idx = *state
836                    .regular_file_plans
837                    .get(&f.id)
838                    .expect("regular file plan missing");
839                let child_plan = &state.plans[child_plan_idx];
840                let first_visit = child_plan_idx == *plan_idx;
841                if first_visit {
842                    *plan_idx += 1;
843                }
844
845                // Record block address for this file in the data map.
846                if child_plan.data_block_start != EROFS_NULL_ADDR {
847                    let abs_block = data_start_block + child_plan.data_block_start;
848                    data_map.insert(child_path, (abs_block, f.data.len() as u64));
849                } else {
850                    // Empty file — record with NULL_ADDR.
851                    data_map.insert(child_path, (EROFS_NULL_ADDR, 0));
852                }
853
854                if first_visit && child_plan.data_block_count > 0 {
855                    let abs_block = data_start_block + child_plan.data_block_start;
856                    let offset = abs_block as u64 * blksiz;
857                    write_zero_padding_to(file, data_cursor, offset)?;
858                    let mut tracked = CursorTrackingWriter::new(file, data_cursor);
859
860                    let full_block_bytes =
861                        child_plan.data_block_count as usize * EROFS_BLKSIZ as usize;
862                    let data_end = if child_plan.data_layout == EROFS_INODE_FLAT_INLINE {
863                        full_block_bytes
864                    } else {
865                        std::cmp::min(f.data.len(), full_block_bytes)
866                    };
867
868                    f.data.write_range(0, data_end, &mut tracked)?;
869
870                    if child_plan.data_layout == EROFS_INODE_FLAT_PLAIN
871                        && data_end < full_block_bytes
872                    {
873                        let pad = full_block_bytes - data_end;
874                        tracked.write_all(&ZEROS[..pad])?;
875                    }
876                }
877            }
878            TreeNode::Symlink(s) => {
879                let child_plan = &state.plans[*plan_idx];
880                *plan_idx += 1;
881
882                if child_plan.data_block_count > 0 {
883                    let abs_block = data_start_block + child_plan.data_block_start;
884                    let offset = abs_block as u64 * blksiz;
885                    write_zero_padding_to(file, data_cursor, offset)?;
886                    let mut tracked = CursorTrackingWriter::new(file, data_cursor);
887
888                    let full_block_bytes =
889                        child_plan.data_block_count as usize * EROFS_BLKSIZ as usize;
890                    let data_end = if child_plan.data_layout == EROFS_INODE_FLAT_INLINE {
891                        full_block_bytes
892                    } else {
893                        std::cmp::min(s.target.len(), full_block_bytes)
894                    };
895
896                    tracked.write_all(&s.target[..data_end])?;
897
898                    if child_plan.data_layout == EROFS_INODE_FLAT_PLAIN
899                        && data_end < full_block_bytes
900                    {
901                        let pad = full_block_bytes - data_end;
902                        tracked.write_all(&ZEROS[..pad])?;
903                    }
904                }
905            }
906            _ => {
907                // CharDevice, BlockDevice, Fifo, Socket have no data
908                *plan_idx += 1;
909            }
910        }
911    }
912
913    Ok(())
914}
915
916fn write_metadata(
917    file: &mut (impl Write + Seek),
918    state: &LayoutState,
919    tree: &FileTree,
920) -> Result<(), ErofsError> {
921    let meta_end = align_to_block(state.current_meta_offset);
922    let data_start_block = (meta_end / EROFS_BLKSIZ as u64) as u32;
923    let mut meta_cursor = state.meta_blkaddr as u64 * EROFS_BLKSIZ as u64;
924
925    file.seek(SeekFrom::Start(meta_cursor))?;
926
927    write_metadata_for_tree(
928        file,
929        state,
930        &TreeNode::Directory(clone_dir_shell(&tree.root)),
931        &tree.root,
932        data_start_block,
933        &mut 0,
934        &mut meta_cursor,
935    )?;
936
937    Ok(())
938}
939
940pub(super) fn clone_dir_shell(dir: &DirectoryNode) -> DirectoryNode {
941    DirectoryNode {
942        metadata: InodeMetadata {
943            uid: dir.metadata.uid,
944            gid: dir.metadata.gid,
945            mode: dir.metadata.mode,
946            mtime: dir.metadata.mtime,
947            mtime_nsec: dir.metadata.mtime_nsec,
948        },
949        xattrs: dir
950            .xattrs
951            .iter()
952            .map(|x| Xattr {
953                name: x.name.clone(),
954                value: x.value.clone(),
955            })
956            .collect(),
957        entries: BTreeMap::new(),
958    }
959}
960
961fn write_metadata_for_tree(
962    file: &mut (impl Write + Seek),
963    state: &LayoutState,
964    node: &TreeNode,
965    real_dir: &DirectoryNode,
966    data_start_block: u32,
967    plan_idx: &mut usize,
968    meta_cursor: &mut u64,
969) -> Result<(), ErofsError> {
970    let blksiz = EROFS_BLKSIZ as u64;
971    let meta_base = state.meta_blkaddr as u64 * blksiz;
972    let plan = &state.plans[*plan_idx];
973    *plan_idx += 1;
974
975    // Compute the byte offset of this inode
976    let inode_offset = meta_base + plan.nid as u64 * EROFS_ISLOT_SIZE as u64;
977
978    write_zero_padding_to(file, meta_cursor, inode_offset)?;
979
980    // Build the 64-byte extended inode
981    let mut inode = [0u8; 64];
982
983    // i_format: bit 0 = 1 (extended), datalayout in bits 1..
984    let i_format: u16 = 1 | ((plan.data_layout as u16) << 1);
985    inode[0..2].copy_from_slice(&i_format.to_le_bytes());
986
987    // i_xattr_icount
988    let i_xattr_icount = compute_xattr_icount(plan.xattr_ibody_size);
989    inode[2..4].copy_from_slice(&i_xattr_icount.to_le_bytes());
990
991    // i_mode
992    let mode_bits = mode_type_bits(node);
993    let meta = node_metadata(node);
994    let i_mode = mode_bits | meta.mode;
995    inode[4..6].copy_from_slice(&i_mode.to_le_bytes());
996
997    // i_nb = 0
998    inode[6..8].copy_from_slice(&0u16.to_le_bytes());
999
1000    // i_size
1001    let i_size: u64 = match node {
1002        TreeNode::Directory(_) => {
1003            if let Some(ref dd) = plan.dir_data {
1004                dd.len() as u64
1005            } else {
1006                0
1007            }
1008        }
1009        _ => node_data_size(node),
1010    };
1011    inode[8..16].copy_from_slice(&i_size.to_le_bytes());
1012
1013    // i_u (startblk_lo or rdev)
1014    let i_u: u32 = match node {
1015        TreeNode::CharDevice(d) | TreeNode::BlockDevice(d) => new_encode_dev(d.major, d.minor),
1016        TreeNode::Fifo(_) | TreeNode::Socket(_) => 0,
1017        _ => {
1018            if plan.data_block_start == EROFS_NULL_ADDR {
1019                EROFS_NULL_ADDR
1020            } else {
1021                data_start_block + plan.data_block_start
1022            }
1023        }
1024    };
1025    inode[16..20].copy_from_slice(&i_u.to_le_bytes());
1026
1027    // i_ino (use NID as inode number)
1028    inode[20..24].copy_from_slice(&plan.nid.to_le_bytes());
1029
1030    // i_uid
1031    inode[24..28].copy_from_slice(&meta.uid.to_le_bytes());
1032
1033    // i_gid
1034    inode[28..32].copy_from_slice(&meta.gid.to_le_bytes());
1035
1036    // i_mtime
1037    inode[32..40].copy_from_slice(&meta.mtime.to_le_bytes());
1038
1039    // i_mtime_nsec
1040    inode[40..44].copy_from_slice(&meta.mtime_nsec.to_le_bytes());
1041
1042    // i_nlink
1043    let nlink = node_nlink(node, &state.regular_file_link_counts);
1044    inode[44..48].copy_from_slice(&nlink.to_le_bytes());
1045
1046    // reserved[16] already zeroed
1047
1048    {
1049        let mut tracked = CursorTrackingWriter::new(file, meta_cursor);
1050        tracked.write_all(&inode)?;
1051
1052        // Write xattr ibody if present
1053        let xattrs = node_xattrs(node);
1054        if plan.xattr_ibody_size > 0 {
1055            write_xattr_ibody(&mut tracked, xattrs)?;
1056        }
1057
1058        // Write inline tail data
1059        if plan.inline_tail_size > 0 {
1060            match node {
1061                TreeNode::Directory(_) => {
1062                    if let Some(ref dir_data) = plan.dir_data {
1063                        let full_block_bytes =
1064                            plan.data_block_count as usize * EROFS_BLKSIZ as usize;
1065                        let tail = &dir_data[full_block_bytes..];
1066                        tracked.write_all(tail)?;
1067                    }
1068                }
1069                TreeNode::RegularFile(f) => {
1070                    let full_block_bytes = plan.data_block_count as usize * EROFS_BLKSIZ as usize;
1071                    let tail_len = f.data.len() - full_block_bytes;
1072                    f.data
1073                        .write_range(full_block_bytes, tail_len, &mut tracked)?;
1074                }
1075                TreeNode::Symlink(s) => {
1076                    let full_block_bytes = plan.data_block_count as usize * EROFS_BLKSIZ as usize;
1077                    let tail = &s.target[full_block_bytes..];
1078                    tracked.write_all(tail)?;
1079                }
1080                _ => {}
1081            }
1082        }
1083    }
1084
1085    // Recurse into children for directories
1086    if let TreeNode::Directory(_) = node {
1087        for child in real_dir.entries.values() {
1088            match child {
1089                TreeNode::Directory(child_dir) => {
1090                    write_metadata_for_tree(
1091                        file,
1092                        state,
1093                        child,
1094                        child_dir,
1095                        data_start_block,
1096                        plan_idx,
1097                        meta_cursor,
1098                    )?;
1099                }
1100                _ => {
1101                    write_metadata_for_leaf(
1102                        file,
1103                        state,
1104                        child,
1105                        data_start_block,
1106                        plan_idx,
1107                        meta_cursor,
1108                    )?;
1109                }
1110            }
1111        }
1112    }
1113
1114    Ok(())
1115}
1116
1117fn write_metadata_for_leaf(
1118    file: &mut (impl Write + Seek),
1119    state: &LayoutState,
1120    node: &TreeNode,
1121    data_start_block: u32,
1122    plan_idx: &mut usize,
1123    meta_cursor: &mut u64,
1124) -> Result<(), ErofsError> {
1125    let blksiz = EROFS_BLKSIZ as u64;
1126    let meta_base = state.meta_blkaddr as u64 * blksiz;
1127    let (plan, first_regular_visit) = match node {
1128        TreeNode::RegularFile(file) => {
1129            let child_plan_idx = *state
1130                .regular_file_plans
1131                .get(&file.id)
1132                .expect("regular file plan missing");
1133            let first_visit = child_plan_idx == *plan_idx;
1134            if first_visit {
1135                *plan_idx += 1;
1136            }
1137            (&state.plans[child_plan_idx], first_visit)
1138        }
1139        _ => {
1140            let plan = &state.plans[*plan_idx];
1141            *plan_idx += 1;
1142            (plan, true)
1143        }
1144    };
1145
1146    if !first_regular_visit {
1147        return Ok(());
1148    }
1149
1150    let inode_offset = meta_base + plan.nid as u64 * EROFS_ISLOT_SIZE as u64;
1151    write_zero_padding_to(file, meta_cursor, inode_offset)?;
1152
1153    let mut inode = [0u8; 64];
1154
1155    let i_format: u16 = 1 | ((plan.data_layout as u16) << 1);
1156    inode[0..2].copy_from_slice(&i_format.to_le_bytes());
1157
1158    let i_xattr_icount = compute_xattr_icount(plan.xattr_ibody_size);
1159    inode[2..4].copy_from_slice(&i_xattr_icount.to_le_bytes());
1160
1161    let mode_bits = mode_type_bits(node);
1162    let meta = node_metadata(node);
1163    let i_mode = mode_bits | meta.mode;
1164    inode[4..6].copy_from_slice(&i_mode.to_le_bytes());
1165
1166    inode[6..8].copy_from_slice(&0u16.to_le_bytes());
1167
1168    let i_size = node_data_size(node);
1169    inode[8..16].copy_from_slice(&i_size.to_le_bytes());
1170
1171    let i_u: u32 = match node {
1172        TreeNode::CharDevice(d) | TreeNode::BlockDevice(d) => new_encode_dev(d.major, d.minor),
1173        TreeNode::Fifo(_) | TreeNode::Socket(_) => 0,
1174        _ => {
1175            if plan.data_block_start == EROFS_NULL_ADDR {
1176                EROFS_NULL_ADDR
1177            } else {
1178                data_start_block + plan.data_block_start
1179            }
1180        }
1181    };
1182    inode[16..20].copy_from_slice(&i_u.to_le_bytes());
1183
1184    inode[20..24].copy_from_slice(&plan.nid.to_le_bytes());
1185    inode[24..28].copy_from_slice(&meta.uid.to_le_bytes());
1186    inode[28..32].copy_from_slice(&meta.gid.to_le_bytes());
1187    inode[32..40].copy_from_slice(&meta.mtime.to_le_bytes());
1188    inode[40..44].copy_from_slice(&meta.mtime_nsec.to_le_bytes());
1189
1190    let nlink = node_nlink(node, &state.regular_file_link_counts);
1191    inode[44..48].copy_from_slice(&nlink.to_le_bytes());
1192
1193    {
1194        let mut tracked = CursorTrackingWriter::new(file, meta_cursor);
1195        tracked.write_all(&inode)?;
1196
1197        let xattrs = node_xattrs(node);
1198        if plan.xattr_ibody_size > 0 {
1199            write_xattr_ibody(&mut tracked, xattrs)?;
1200        }
1201
1202        if plan.inline_tail_size > 0 {
1203            match node {
1204                TreeNode::RegularFile(f) => {
1205                    let full_block_bytes = plan.data_block_count as usize * EROFS_BLKSIZ as usize;
1206                    let tail_len = f.data.len() - full_block_bytes;
1207                    f.data
1208                        .write_range(full_block_bytes, tail_len, &mut tracked)?;
1209                }
1210                TreeNode::Symlink(s) => {
1211                    let full_block_bytes = plan.data_block_count as usize * EROFS_BLKSIZ as usize;
1212                    let tail = &s.target[full_block_bytes..];
1213                    tracked.write_all(tail)?;
1214                }
1215                _ => {}
1216            }
1217        }
1218    }
1219
1220    Ok(())
1221}
1222
1223pub(super) fn write_xattr_ibody(file: &mut impl Write, xattrs: &[Xattr]) -> Result<(), ErofsError> {
1224    // Write the ibody header (12 bytes)
1225    // h_name_filter: u32 = 0, h_shared_count: u8 = 0, reserved: [u8; 7] = 0
1226    let header = [0u8; EROFS_XATTR_IBODY_HEADER_SIZE as usize];
1227    file.write_all(&header)?;
1228
1229    for xattr in xattrs {
1230        let (prefix_idx, suffix) =
1231            xattr_prefix_index(&xattr.name).ok_or(ErofsError::UnsupportedXattrPrefix)?;
1232
1233        // erofs_xattr_entry: 4 bytes
1234        let mut entry = [0u8; 4];
1235        entry[0] = suffix.len() as u8; // e_name_len
1236        entry[1] = prefix_idx; // e_name_index
1237        entry[2..4].copy_from_slice(&(xattr.value.len() as u16).to_le_bytes()); // e_value_size
1238        file.write_all(&entry)?;
1239
1240        // Write suffix name
1241        file.write_all(suffix)?;
1242
1243        // Write value
1244        file.write_all(&xattr.value)?;
1245
1246        // Pad to 4-byte alignment
1247        let entry_size = 4 + suffix.len() + xattr.value.len();
1248        let aligned = erofs_xattr_align(entry_size);
1249        let pad = aligned - entry_size;
1250        if pad > 0 {
1251            file.write_all(&ZEROS[..pad])?;
1252        }
1253    }
1254
1255    Ok(())
1256}
1257
1258fn write_superblock(file: &mut (impl Write + Seek), state: &LayoutState) -> Result<(), ErofsError> {
1259    let blksiz = EROFS_BLKSIZ as u64;
1260
1261    // First, ensure the file has at least the first block zeroed (boot sector area)
1262    file.seek(SeekFrom::Start(0))?;
1263    file.write_all(&[0u8; EROFS_SUPER_OFFSET as usize])?;
1264
1265    // Compute total blocks
1266    let meta_end = align_to_block(state.current_meta_offset);
1267    let data_start_block = (meta_end / blksiz) as u32;
1268    let total_blocks = data_start_block + state.current_data_block;
1269
1270    // Build superblock (128 bytes)
1271    let mut sb = [0u8; EROFS_SUPERBLOCK_SIZE as usize];
1272
1273    // magic
1274    sb[0x00..0x04].copy_from_slice(&EROFS_SUPER_MAGIC.to_le_bytes());
1275
1276    // checksum (zeroed for now, computed below)
1277    sb[0x04..0x08].copy_from_slice(&0u32.to_le_bytes());
1278
1279    // feature_compat (SB_CHKSUM)
1280    sb[0x08..0x0C].copy_from_slice(&EROFS_FEATURE_COMPAT_SB_CHKSUM.to_le_bytes());
1281
1282    // blkszbits
1283    sb[0x0C] = EROFS_BLKSIZ_BITS;
1284
1285    // sb_extslots
1286    sb[0x0D] = 0;
1287
1288    // rootnid_2b (16-bit field — image is unmountable if NID exceeds u16::MAX).
1289    if state.root_nid > u16::MAX as u32 {
1290        return Err(ErofsError::NidOverflow);
1291    }
1292    sb[0x0E..0x10].copy_from_slice(&(state.root_nid as u16).to_le_bytes());
1293
1294    // inos
1295    sb[0x10..0x18].copy_from_slice(&state.inode_count.to_le_bytes());
1296
1297    // epoch (base timestamp = 0)
1298    sb[0x18..0x20].copy_from_slice(&0u64.to_le_bytes());
1299
1300    // fixed_nsec
1301    sb[0x20..0x24].copy_from_slice(&0u32.to_le_bytes());
1302
1303    // blocks_lo
1304    sb[0x24..0x28].copy_from_slice(&total_blocks.to_le_bytes());
1305
1306    // meta_blkaddr
1307    sb[0x28..0x2C].copy_from_slice(&state.meta_blkaddr.to_le_bytes());
1308
1309    // xattr_blkaddr
1310    sb[0x2C..0x30].copy_from_slice(&0u32.to_le_bytes());
1311
1312    // uuid (16 bytes) - use zeros for now (deterministic)
1313    // sb[0x30..0x40] already zeroed
1314
1315    // volume_name (16 bytes) - zeros
1316    // sb[0x40..0x50] already zeroed
1317
1318    // feature_incompat
1319    sb[0x50..0x54].copy_from_slice(&0u32.to_le_bytes());
1320
1321    // dirblkbits at offset 0x5A
1322    sb[0x5A] = 0;
1323
1324    // Now compute CRC32C over the entire superblock block with checksum field zeroed
1325    // The superblock block starts at offset 0 and is EROFS_BLKSIZ bytes.
1326    // But the superblock starts at EROFS_SUPER_OFFSET within that block.
1327    // CRC is over bytes from EROFS_SUPER_OFFSET to end of the superblock block.
1328
1329    // Build the full block
1330    let mut block = vec![0u8; EROFS_BLKSIZ as usize];
1331    block
1332        [EROFS_SUPER_OFFSET as usize..EROFS_SUPER_OFFSET as usize + EROFS_SUPERBLOCK_SIZE as usize]
1333        .copy_from_slice(&sb);
1334
1335    // CRC32C of the range [EROFS_SUPER_OFFSET .. EROFS_BLKSIZ].
1336    // EROFS uses raw CRC32C (seed ~0, no final XOR) — call crc32c_raw directly.
1337    let crc_data = &block[EROFS_SUPER_OFFSET as usize..EROFS_BLKSIZ as usize];
1338    let checksum = crc32c::crc32c_raw(0xFFFF_FFFF, crc_data);
1339
1340    // Set checksum in the superblock
1341    sb[0x04..0x08].copy_from_slice(&checksum.to_le_bytes());
1342
1343    // Write the superblock at EROFS_SUPER_OFFSET
1344    file.seek(SeekFrom::Start(EROFS_SUPER_OFFSET))?;
1345    file.write_all(&sb)?;
1346
1347    // Pad the rest of block 0 with zeros (up to EROFS_BLKSIZ)
1348    let remaining = EROFS_BLKSIZ as u64 - EROFS_SUPER_OFFSET - EROFS_SUPERBLOCK_SIZE as u64;
1349    file.write_all(&vec![0u8; remaining as usize])?;
1350
1351    Ok(())
1352}
1353
1354fn align_to_block(offset: u64) -> u64 {
1355    let blksiz = EROFS_BLKSIZ as u64;
1356    offset.div_ceil(blksiz) * blksiz
1357}