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