1use erofs::{FileType, format};
9use zerocopy::IntoBytes;
10use zerocopy::byteorder::little_endian::{U16 as LEU16, U32 as LEU32, U64 as LEU64};
11
12const BLOCK_SIZE: usize = 4096;
13const INODE_SIZE: usize = format::INODE_SLOT_SIZE as usize;
14const MAX_INLINE_TAIL_LEN: usize = BLOCK_SIZE - INODE_SIZE;
15
16#[derive(Debug, Clone)]
18pub enum SerializerNode {
19 Directory { name: String, entries: Vec<SerializerNode> },
21 File { name: String, data: Vec<u8> },
23 Symlink { name: String, target: Vec<u8> },
25}
26
27impl SerializerNode {
28 pub fn name(&self) -> &str {
29 match self {
30 Self::Directory { name, .. } => name,
31 Self::File { name, .. } => name,
32 Self::Symlink { name, .. } => name,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38enum NodeType {
39 Dir,
40 File,
41 Symlink,
42}
43
44struct FlatNode {
45 nid: u64,
46 inode_offset: usize,
47 is_inline: bool,
48 node_type: NodeType,
49 contents: FlatNodeContents,
50 data_block: u32,
51 size: u64,
52}
53
54enum FlatNodeContents {
55 Directory {
56 entries: Vec<(String, usize, NodeType)>,
58 blocks: Vec<[u8; BLOCK_SIZE]>,
59 },
60 File {
61 data: Vec<u8>,
62 },
63 Symlink {
64 target: Vec<u8>,
65 },
66}
67
68fn add_node(node: &SerializerNode, nodes: &mut Vec<FlatNode>, parent_idx: usize) -> usize {
69 let idx = nodes.len();
70 match node {
71 SerializerNode::Directory { name: _, entries } => {
72 nodes.push(FlatNode {
73 nid: 0,
74 inode_offset: 0,
75 is_inline: false,
76 node_type: NodeType::Dir,
77 contents: FlatNodeContents::Directory { entries: Vec::new(), blocks: Vec::new() },
78 data_block: 0,
79 size: 0,
80 });
81
82 let mut child_entries = Vec::new();
83 child_entries.push((".".to_string(), idx, NodeType::Dir));
84 child_entries.push(("..".to_string(), parent_idx, NodeType::Dir));
85
86 for child in entries {
87 let child_name = child.name().to_string();
88 let child_idx = add_node(child, nodes, idx);
89 let child_type = nodes[child_idx].node_type;
90 child_entries.push((child_name, child_idx, child_type));
91 }
92
93 child_entries.sort_by(|a, b| a.0.cmp(&b.0));
94 nodes[idx].contents =
95 FlatNodeContents::Directory { entries: child_entries, blocks: Vec::new() };
96 }
97 SerializerNode::File { name: _, data } => {
98 nodes.push(FlatNode {
99 nid: 0,
100 inode_offset: 0,
101 is_inline: false,
102 node_type: NodeType::File,
103 contents: FlatNodeContents::File { data: data.clone() },
104 data_block: 0,
105 size: 0,
106 });
107 }
108 SerializerNode::Symlink { name: _, target } => {
109 nodes.push(FlatNode {
110 nid: 0,
111 inode_offset: 0,
112 is_inline: false,
113 node_type: NodeType::Symlink,
114 contents: FlatNodeContents::Symlink { target: target.clone() },
115 data_block: 0,
116 size: 0,
117 });
118 }
119 }
120 idx
121}
122
123fn pack_directory_blocks(
124 entries: &[(String, usize, NodeType)],
125 nodes: &[FlatNode],
126) -> Vec<[u8; BLOCK_SIZE]> {
127 let mut blocks = Vec::new();
128 let mut start = 0;
129 while start < entries.len() {
130 let mut end = start;
131 let mut names_len = 0usize;
132 while end < entries.len() {
133 let next_count = end - start + 1;
134 let next_names_len = names_len + entries[end].0.len();
135 if next_count * format::DIRENT_SIZE + next_names_len > BLOCK_SIZE {
136 break;
137 }
138 names_len = next_names_len;
139 end += 1;
140 }
141 assert!(end > start, "Single directory entry exceeds 4096-byte block size");
142
143 let slice = &entries[start..end];
144 let mut dir_block = [0u8; BLOCK_SIZE];
145 let mut current_nameoff = (slice.len() * format::DIRENT_SIZE) as u16;
146 let mut dirents = Vec::with_capacity(slice.len());
147 let mut name_bytes = Vec::with_capacity(names_len);
148
149 for (name, target_idx, node_type) in slice {
150 let file_type = match node_type {
151 NodeType::Dir => FileType::Dir as u8,
152 NodeType::File => FileType::RegFile as u8,
153 NodeType::Symlink => FileType::Symlink as u8,
154 };
155 dirents.push(format::Dirent {
156 nid: LEU64::new(nodes[*target_idx].nid),
157 nameoff: LEU16::new(current_nameoff),
158 file_type,
159 reserved: 0,
160 });
161 name_bytes.extend_from_slice(name.as_bytes());
162 current_nameoff += name.len() as u16;
163 }
164
165 let dirents_bytes = dirents.as_slice().as_bytes();
166 dir_block[..dirents_bytes.len()].copy_from_slice(dirents_bytes);
167 let names_start = dirents_bytes.len();
168 dir_block[names_start..names_start + name_bytes.len()].copy_from_slice(&name_bytes);
169 blocks.push(dir_block);
170 start = end;
171 }
172 blocks
173}
174
175pub fn serialize(root_entries: &[SerializerNode]) -> Vec<u8> {
178 let mut nodes = Vec::<FlatNode>::new();
179
180 nodes.push(FlatNode {
182 nid: 0,
183 inode_offset: 0,
184 is_inline: false,
185 node_type: NodeType::Dir,
186 contents: FlatNodeContents::Directory { entries: Vec::new(), blocks: Vec::new() },
187 data_block: 0,
188 size: 0,
189 });
190
191 let mut root_child_entries = Vec::new();
192 root_child_entries.push((".".to_string(), 0, NodeType::Dir));
193 root_child_entries.push(("..".to_string(), 0, NodeType::Dir));
194
195 for child in root_entries {
196 let child_name = child.name().to_string();
197 let child_idx = add_node(child, &mut nodes, 0);
198 let child_type = nodes[child_idx].node_type;
199 root_child_entries.push((child_name, child_idx, child_type));
200 }
201
202 root_child_entries.sort_by(|a, b| a.0.cmp(&b.0));
203 nodes[0].contents =
204 FlatNodeContents::Directory { entries: root_child_entries, blocks: Vec::new() };
205
206 let meta_base = BLOCK_SIZE;
210 let mut current_meta_offset = meta_base;
211 for node in &mut nodes {
212 let inline_tail_len = match &node.contents {
213 FlatNodeContents::File { data } => {
214 let tail_len = data.len() % BLOCK_SIZE;
215 if tail_len > 0 && tail_len <= MAX_INLINE_TAIL_LEN {
216 node.is_inline = true;
217 tail_len
218 } else {
219 0
220 }
221 }
222 _ => 0,
223 };
224 let required_meta_bytes = INODE_SIZE + inline_tail_len;
225 if (current_meta_offset % BLOCK_SIZE) + required_meta_bytes > BLOCK_SIZE {
226 current_meta_offset = current_meta_offset.next_multiple_of(BLOCK_SIZE);
227 }
228 node.inode_offset = current_meta_offset;
229 node.nid = ((current_meta_offset - meta_base) / INODE_SIZE) as u64;
230 current_meta_offset =
231 (current_meta_offset + required_meta_bytes).next_multiple_of(INODE_SIZE);
232 }
233
234 let data_start_block = current_meta_offset.div_ceil(BLOCK_SIZE) as u32;
237 let mut next_free_block = std::cmp::max(2, data_start_block);
238
239 for i in 0..nodes.len() {
240 if let FlatNodeContents::Directory { entries, .. } = &nodes[i].contents {
241 let blocks = pack_directory_blocks(entries, &nodes);
242 nodes[i].data_block = next_free_block;
243 nodes[i].size = (blocks.len() * BLOCK_SIZE) as u64;
244 next_free_block += blocks.len() as u32;
245 if let FlatNodeContents::Directory { blocks: dst_blocks, .. } = &mut nodes[i].contents {
246 *dst_blocks = blocks;
247 }
248 }
249 }
250
251 for node in &mut nodes {
252 match &node.contents {
253 FlatNodeContents::Directory { .. } => {}
254 FlatNodeContents::File { data } => {
255 let len = data.len() as u64;
256 node.size = len;
257 let data_blocks = if node.is_inline {
258 (len / BLOCK_SIZE as u64) as u32
259 } else {
260 len.div_ceil(BLOCK_SIZE as u64) as u32
261 };
262 if data_blocks > 0 {
263 node.data_block = next_free_block;
264 next_free_block += data_blocks;
265 } else {
266 node.data_block = 0;
267 }
268 }
269 FlatNodeContents::Symlink { target } => {
270 let len = target.len() as u64;
271 node.size = len;
272 if len > 0 {
273 node.data_block = next_free_block;
274 let blocks_needed = len.div_ceil(BLOCK_SIZE as u64);
275 next_free_block += blocks_needed as u32;
276 } else {
277 node.data_block = 0;
278 }
279 }
280 }
281 }
282
283 let total_blocks = next_free_block;
284 let mut image = vec![0u8; total_blocks as usize * BLOCK_SIZE];
285
286 let sb = format::SuperBlock {
288 magic: LEU32::new(format::EROFS_MAGIC),
289 checksum: LEU32::new(0),
290 feature_compat: LEU32::new(0),
291 block_size_bits: 12, sb_ext_slots: 0,
293 root_nid: LEU16::new(0),
294 inode_count: LEU64::new(nodes.len() as u64),
295 epoch: LEU64::new(1700000000),
296 fixed_nsec: LEU32::new(0),
297 blocks: LEU32::new(total_blocks),
298 meta_block_addr: LEU32::new(1),
299 xattr_block_addr: LEU32::new(0),
300 uuid: [0; 16],
301 volume_name: [0; 16],
302 feature_incompat: LEU32::new(0),
303 available_compr_algs: LEU16::new(0),
304 extra_devices: LEU32::new(0),
305 dirblkbits: 0,
306 reserved: [0; 37],
307 };
308 let sb_offset = format::SUPERBLOCK_OFFSET as usize;
309 image[sb_offset..sb_offset + std::mem::size_of::<format::SuperBlock>()]
310 .copy_from_slice(sb.as_bytes());
311
312 for i in 0..nodes.len() {
314 let mode = match nodes[i].node_type {
315 NodeType::Dir => 0o040000 | 0o755, NodeType::File => 0o100000 | 0o644, NodeType::Symlink => 0o120000 | 0o777, };
319
320 let link_count = if nodes[i].node_type == NodeType::Dir { 2 } else { 1 };
321 let i_u = nodes[i].data_block.to_le_bytes();
322 let inode_format = if nodes[i].is_inline { 4u16 } else { 0u16 };
324
325 let inode = format::InodeCompact {
326 format: LEU16::new(inode_format),
327 xattr_icount: LEU16::new(0),
328 mode: LEU16::new(mode),
329 link_count: LEU16::new(link_count),
330 size: LEU32::new(nodes[i].size as u32),
331 reserved_1: [0; 4],
332 i_u,
333 ino: LEU32::new(nodes[i].nid as u32),
334 uid: LEU16::new(100),
335 gid: LEU16::new(200),
336 reserved_2: [0; 4],
337 };
338 let inode_offset = nodes[i].inode_offset;
339 image[inode_offset..inode_offset + INODE_SIZE].copy_from_slice(inode.as_bytes());
340
341 match &nodes[i].contents {
342 FlatNodeContents::Directory { blocks, .. } => {
343 let offset = nodes[i].data_block as usize * BLOCK_SIZE;
344 for (b_idx, block) in blocks.iter().enumerate() {
345 let block_offset = offset + b_idx * BLOCK_SIZE;
346 image[block_offset..block_offset + BLOCK_SIZE].copy_from_slice(block);
347 }
348 }
349 FlatNodeContents::File { data } => {
350 if nodes[i].is_inline {
351 let full_blocks_len = (data.len() / BLOCK_SIZE) * BLOCK_SIZE;
352 if full_blocks_len > 0 {
353 let offset = nodes[i].data_block as usize * BLOCK_SIZE;
354 image[offset..offset + full_blocks_len]
355 .copy_from_slice(&data[..full_blocks_len]);
356 }
357 let tail = &data[full_blocks_len..];
358 image[inode_offset + INODE_SIZE..inode_offset + INODE_SIZE + tail.len()]
359 .copy_from_slice(tail);
360 } else if !data.is_empty() {
361 let offset = nodes[i].data_block as usize * BLOCK_SIZE;
362 image[offset..offset + data.len()].copy_from_slice(data);
363 }
364 }
365 FlatNodeContents::Symlink { target } => {
366 if !target.is_empty() {
367 let offset = nodes[i].data_block as usize * BLOCK_SIZE;
368 image[offset..offset + target.len()].copy_from_slice(target);
369 }
370 }
371 }
372 }
373
374 image
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use erofs::readers::VecReader;
381 use erofs::{ErofsFilesystem, InodeDataLayout, Node};
382 use std::sync::Arc;
383
384 fn patterned_bytes(seed: usize, len: usize) -> Vec<u8> {
385 (0..len).map(|i| (i.wrapping_mul(31).wrapping_add(seed * 17)) as u8).collect()
386 }
387
388 fn assert_dir_recursive(
389 fs: &ErofsFilesystem,
390 expected_entries: &[SerializerNode],
391 actual_dir: &erofs::DirectoryNode,
392 expected_parent_nid: u64,
393 ) {
394 let mut actual_entries_buf = vec![erofs::DirectoryEntry::default(); 512];
395 let filled = fs.read_directory(actual_dir, 0, &mut actual_entries_buf).unwrap();
396 let mut actual_entries = actual_entries_buf[..filled].to_vec();
397
398 let dot_entry =
400 actual_entries.iter().find(|e| e.name == ".").expect("missing '.' directory entry");
401 assert_eq!(dot_entry.nid, actual_dir.nid());
402 assert_eq!(dot_entry.file_type, FileType::Dir);
403
404 let dotdot_entry =
405 actual_entries.iter().find(|e| e.name == "..").expect("missing '..' directory entry");
406 assert_eq!(dotdot_entry.nid, expected_parent_nid);
407 assert_eq!(dotdot_entry.file_type, FileType::Dir);
408
409 actual_entries.retain(|e| e.name != "." && e.name != "..");
410
411 assert_eq!(actual_entries.len(), expected_entries.len(), "Directory entry count mismatch");
412
413 let mut sorted_expected: Vec<&SerializerNode> = expected_entries.iter().collect();
414 sorted_expected.sort_by(|a, b| a.name().cmp(b.name()));
415
416 for (i, expected_node) in sorted_expected.iter().enumerate() {
417 let actual_entry = &actual_entries[i];
418 assert_eq!(actual_entry.name, expected_node.name());
419
420 let child_node = fs.node(actual_entry.nid).expect("failed to read child node");
421
422 match expected_node {
423 SerializerNode::Directory { entries, .. } => {
424 assert_eq!(actual_entry.file_type, FileType::Dir);
425 let actual_child_dir = match child_node {
426 Node::Directory(d) => d,
427 _ => panic!("Expected directory node for {}", expected_node.name()),
428 };
429 assert_dir_recursive(fs, entries, &actual_child_dir, actual_dir.nid());
430 }
431 SerializerNode::File { data, .. } => {
432 assert_eq!(actual_entry.file_type, FileType::RegFile);
433 let actual_child_file = match child_node {
434 Node::File(f) => f,
435 _ => panic!("Expected file node for {}", expected_node.name()),
436 };
437 assert_eq!(actual_child_file.size(), data.len() as u64);
438 let tail_len = data.len() % BLOCK_SIZE;
439 if tail_len > 0 && tail_len <= MAX_INLINE_TAIL_LEN {
440 assert_eq!(actual_child_file.data_layout(), InodeDataLayout::FlatInline);
441 } else {
442 assert_eq!(actual_child_file.data_layout(), InodeDataLayout::FlatPlain);
443 }
444 let mut file_buf = vec![0u8; data.len()];
445 fs.read_file_range(&actual_child_file, 0, &mut file_buf).unwrap();
446 assert_eq!(&file_buf, data);
447 }
448 SerializerNode::Symlink { target, .. } => {
449 assert_eq!(actual_entry.file_type, FileType::Symlink);
450 let actual_child_symlink = match child_node {
451 Node::Symlink(s) => s,
452 _ => panic!("Expected symlink node for {}", expected_node.name()),
453 };
454 assert_eq!(actual_child_symlink.size(), target.len() as u64);
455 let actual_target = fs.read_symlink(&actual_child_symlink).unwrap();
456 assert_eq!(&actual_target, target);
457 }
458 }
459 }
460 }
461
462 #[fuchsia::test]
463 fn test_serialize_and_parse() {
464 let mut many_files = Vec::new();
465 for i in 0..200 {
466 let size = match i % 3 {
467 0 => 512,
468 1 => 4096,
469 _ => 4608,
470 };
471 many_files.push(SerializerNode::File {
472 name: format!("small_file_{i:03}"),
473 data: patterned_bytes(i, size),
474 });
475 }
476 many_files.push(SerializerNode::Symlink {
477 name: "nested_symlink".to_string(),
478 target: b"../../file1".to_vec(),
479 });
480
481 let tree = vec![
482 SerializerNode::File { name: "file1".to_string(), data: b"hello world".to_vec() },
483 SerializerNode::File {
484 name: "exact_block".to_string(),
485 data: patterned_bytes(301, 4096),
486 },
487 SerializerNode::File {
488 name: "multi_block_inline".to_string(),
489 data: patterned_bytes(302, 4608),
490 },
491 SerializerNode::Symlink { name: "symlink1".to_string(), target: b"file1".to_vec() },
492 SerializerNode::Directory {
493 name: "dir1".to_string(),
494 entries: vec![
495 SerializerNode::File {
496 name: "file2".to_string(),
497 data: b"another file!".to_vec(),
498 },
499 SerializerNode::Directory { name: "subdir".to_string(), entries: many_files },
500 ],
501 },
502 ];
503
504 let image = serialize(&tree);
505 let reader = Arc::new(VecReader::new(image));
506 let fs = ErofsFilesystem::new(reader).expect("Failed to parse serialized EROFS image");
507
508 let root = fs.root_node();
509 assert_eq!(root.ino(), 0);
510
511 assert_dir_recursive(&fs, &tree, &root, root.nid());
512 }
513
514 #[fuchsia::test]
515 fn test_file_tail_boundaries() {
516 let sizes = [
520 0,
521 1,
522 31,
523 32,
524 33,
525 MAX_INLINE_TAIL_LEN - 1,
526 MAX_INLINE_TAIL_LEN,
527 MAX_INLINE_TAIL_LEN + 1,
528 BLOCK_SIZE - 1,
529 BLOCK_SIZE,
530 BLOCK_SIZE + 1,
531 BLOCK_SIZE + MAX_INLINE_TAIL_LEN,
532 BLOCK_SIZE + MAX_INLINE_TAIL_LEN + 1,
533 BLOCK_SIZE * 2 - 1,
534 BLOCK_SIZE * 2,
535 BLOCK_SIZE * 2 + 123,
536 ];
537
538 let tree: Vec<SerializerNode> = sizes
539 .iter()
540 .enumerate()
541 .map(|(i, &size)| SerializerNode::File {
542 name: format!("file_{size:05}"),
543 data: patterned_bytes(i + 1, size),
544 })
545 .collect();
546
547 let image = serialize(&tree);
548 let reader = Arc::new(VecReader::new(image));
549 let fs = ErofsFilesystem::new(reader).expect("Failed to parse serialized EROFS image");
550
551 let root = fs.root_node();
552 assert_dir_recursive(&fs, &tree, &root, root.nid());
553 }
554
555 #[fuchsia::test]
556 fn test_empty_root_and_empty_subdir() {
557 let empty_image = serialize(&[]);
559 let empty_fs = ErofsFilesystem::new(Arc::new(VecReader::new(empty_image))).unwrap();
560 let empty_root = empty_fs.root_node();
561 assert_dir_recursive(&empty_fs, &[], &empty_root, empty_root.nid());
562
563 let tree = vec![
565 SerializerNode::Directory { name: "empty_dir".to_string(), entries: Vec::new() },
566 SerializerNode::File { name: "empty_file".to_string(), data: Vec::new() },
567 ];
568 let image = serialize(&tree);
569 let fs = ErofsFilesystem::new(Arc::new(VecReader::new(image))).unwrap();
570 let root = fs.root_node();
571 assert_dir_recursive(&fs, &tree, &root, root.nid());
572 }
573
574 #[fuchsia::test]
575 fn test_directory_block_exact_boundary() {
576 let mut entries = Vec::new();
585 for i in 0..15 {
586 let prefix = format!("entry_{i:02}_");
587 let name = format!("{prefix}{}", "a".repeat(250 - prefix.len()));
588 assert_eq!(name.len(), 250);
589 entries.push(SerializerNode::File { name, data: patterned_bytes(i, 64) });
590 }
591 let prefix_15 = "entry_15_";
592 let name_15 = format!("{prefix_15}{}", "b".repeat(127 - prefix_15.len()));
593 assert_eq!(name_15.len(), 127);
594 entries.push(SerializerNode::File { name: name_15, data: patterned_bytes(15, 64) });
595
596 entries.push(SerializerNode::File {
598 name: "entry_16_spill".to_string(),
599 data: patterned_bytes(16, 64),
600 });
601
602 let image = serialize(&entries);
603 let fs = ErofsFilesystem::new(Arc::new(VecReader::new(image))).unwrap();
604 let root = fs.root_node();
605 assert_eq!(root.size(), (2 * BLOCK_SIZE) as u64);
606 assert_dir_recursive(&fs, &entries, &root, root.nid());
607 }
608
609 #[fuchsia::test]
610 fn test_directory_blocks_precede_file_data_blocks() {
611 let tree = vec![
612 SerializerNode::Directory {
613 name: "a_dir".to_string(),
614 entries: vec![SerializerNode::File {
615 name: "first_file".to_string(),
616 data: patterned_bytes(1, BLOCK_SIZE),
617 }],
618 },
619 SerializerNode::Directory {
620 name: "z_dir".to_string(),
621 entries: vec![SerializerNode::File {
622 name: "second_file".to_string(),
623 data: patterned_bytes(2, BLOCK_SIZE),
624 }],
625 },
626 ];
627
628 let image = serialize(&tree);
629 let fs = ErofsFilesystem::new(Arc::new(VecReader::new(image))).unwrap();
630 let root = fs.root_node();
631
632 let mut root_entries = vec![erofs::DirectoryEntry::default(); 16];
633 let count = fs.read_directory(&root, 0, &mut root_entries).unwrap();
634 let find_child = |entries: &[erofs::DirectoryEntry], name: &str| {
635 entries.iter().find(|e| e.name == name).unwrap().nid
636 };
637
638 let a_dir = match fs.node(find_child(&root_entries[..count], "a_dir")).unwrap() {
639 Node::Directory(d) => d,
640 _ => panic!("expected directory"),
641 };
642 let z_dir = match fs.node(find_child(&root_entries[..count], "z_dir")).unwrap() {
643 Node::Directory(d) => d,
644 _ => panic!("expected directory"),
645 };
646
647 let mut a_entries = vec![erofs::DirectoryEntry::default(); 16];
648 let a_count = fs.read_directory(&a_dir, 0, &mut a_entries).unwrap();
649 let first_file = match fs.node(find_child(&a_entries[..a_count], "first_file")).unwrap() {
650 Node::File(f) => f,
651 _ => panic!("expected file"),
652 };
653
654 assert!(
655 z_dir.blkaddr_offset(BLOCK_SIZE as u64, 0).unwrap()
656 < first_file.blkaddr_offset(BLOCK_SIZE as u64, 0).unwrap(),
657 "all directory blocks must be placed before file data blocks"
658 );
659 }
660}