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
18static ZEROS: [u8; 4096] = [0u8; 4096];
24
25#[derive(Debug)]
30pub enum ErofsError {
31 Io(io::Error),
32 NidOverflow,
33 UnsupportedXattrPrefix,
34}
35
36#[derive(Debug, Clone)]
41pub struct ErofsDataMap {
42 pub file_blocks: HashMap<PathBuf, (u32, u64)>,
44 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
79impl 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
104impl 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
145pub 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 plan_directory(&tree.root, 0, &mut state, true)?;
155 state.root_nid = state.plans[0].nid;
156
157 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 write_metadata(&mut file, &state, tree)?;
168
169 write_superblock(&mut file, &state)?;
171
172 file.flush()?;
175
176 let current_len = file.seek(SeekFrom::End(0))?;
179 let block_aligned = align_to_block(current_len);
180 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 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 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
225pub(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
233pub(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 let entry_count = 2 + dir.entries.len();
315
316 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 names.sort();
325
326 let blksiz = EROFS_BLKSIZ as usize;
329 let mut total_size = 0usize;
330 let mut idx = 0;
331
332 while idx < names.len() {
333 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 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 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
372pub(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 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 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 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 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 block[dirent_off..dirent_off + 8].copy_from_slice(&e.nid.to_le_bytes());
466 block[dirent_off + 8..dirent_off + 10]
468 .copy_from_slice(&(name_offset as u16).to_le_bytes());
469 block[dirent_off + 10] = e.file_type;
471 block[dirent_off + 11] = 0;
473
474 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 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
536fn 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 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 let xattr_ibody_size = compute_xattr_ibody_size(&dir.xattrs)?;
564
565 let dir_data_size = compute_dir_data_size(dir) as u64;
567
568 let inode_fixed_size = EROFS_INODE_EXTENDED_SIZE + xattr_ibody_size;
570
571 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 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, );
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 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 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 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 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 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 let meta_end = align_to_block(state.current_meta_offset);
751 let data_area_start = meta_end;
752
753 let data_start_block = (data_area_start / EROFS_BLKSIZ as u64) as u32;
758
759 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 ¤t_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 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 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 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 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 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 *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 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 let mut inode = [0u8; 64];
982
983 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 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 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 inode[6..8].copy_from_slice(&0u16.to_le_bytes());
999
1000 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 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 inode[20..24].copy_from_slice(&plan.nid.to_le_bytes());
1029
1030 inode[24..28].copy_from_slice(&meta.uid.to_le_bytes());
1032
1033 inode[28..32].copy_from_slice(&meta.gid.to_le_bytes());
1035
1036 inode[32..40].copy_from_slice(&meta.mtime.to_le_bytes());
1038
1039 inode[40..44].copy_from_slice(&meta.mtime_nsec.to_le_bytes());
1041
1042 let nlink = node_nlink(node, &state.regular_file_link_counts);
1044 inode[44..48].copy_from_slice(&nlink.to_le_bytes());
1045
1046 {
1049 let mut tracked = CursorTrackingWriter::new(file, meta_cursor);
1050 tracked.write_all(&inode)?;
1051
1052 let xattrs = node_xattrs(node);
1054 if plan.xattr_ibody_size > 0 {
1055 write_xattr_ibody(&mut tracked, xattrs)?;
1056 }
1057
1058 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 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 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 let mut entry = [0u8; 4];
1235 entry[0] = suffix.len() as u8; entry[1] = prefix_idx; entry[2..4].copy_from_slice(&(xattr.value.len() as u16).to_le_bytes()); file.write_all(&entry)?;
1239
1240 file.write_all(suffix)?;
1242
1243 file.write_all(&xattr.value)?;
1245
1246 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 file.seek(SeekFrom::Start(0))?;
1263 file.write_all(&[0u8; EROFS_SUPER_OFFSET as usize])?;
1264
1265 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 let mut sb = [0u8; EROFS_SUPERBLOCK_SIZE as usize];
1272
1273 sb[0x00..0x04].copy_from_slice(&EROFS_SUPER_MAGIC.to_le_bytes());
1275
1276 sb[0x04..0x08].copy_from_slice(&0u32.to_le_bytes());
1278
1279 sb[0x08..0x0C].copy_from_slice(&EROFS_FEATURE_COMPAT_SB_CHKSUM.to_le_bytes());
1281
1282 sb[0x0C] = EROFS_BLKSIZ_BITS;
1284
1285 sb[0x0D] = 0;
1287
1288 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 sb[0x10..0x18].copy_from_slice(&state.inode_count.to_le_bytes());
1296
1297 sb[0x18..0x20].copy_from_slice(&0u64.to_le_bytes());
1299
1300 sb[0x20..0x24].copy_from_slice(&0u32.to_le_bytes());
1302
1303 sb[0x24..0x28].copy_from_slice(&total_blocks.to_le_bytes());
1305
1306 sb[0x28..0x2C].copy_from_slice(&state.meta_blkaddr.to_le_bytes());
1308
1309 sb[0x2C..0x30].copy_from_slice(&0u32.to_le_bytes());
1311
1312 sb[0x50..0x54].copy_from_slice(&0u32.to_le_bytes());
1320
1321 sb[0x5A] = 0;
1323
1324 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 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 sb[0x04..0x08].copy_from_slice(&checksum.to_le_bytes());
1342
1343 file.seek(SeekFrom::Start(EROFS_SUPER_OFFSET))?;
1345 file.write_all(&sb)?;
1346
1347 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}