1use erofs::format;
10use zerocopy::IntoBytes;
11use zerocopy::byteorder::little_endian::{U16 as LEU16, U32 as LEU32, U64 as LEU64};
12
13#[derive(Debug, Clone)]
15pub enum SerializerNode {
16 Directory { name: String, entries: Vec<SerializerNode> },
18 File { name: String, data: Vec<u8> },
20}
21
22impl SerializerNode {
23 pub fn name(&self) -> &str {
24 match self {
25 Self::Directory { name, .. } => name,
26 Self::File { name, .. } => name,
27 }
28 }
29}
30
31struct FlatNode {
32 nid: u64,
33 is_dir: bool,
34 contents: FlatNodeContents,
35 data_block: u32,
36 size: u64,
37}
38
39enum FlatNodeContents {
40 Directory {
41 entries: Vec<(String, u64, bool)>,
43 },
44 File {
45 data: Vec<u8>,
46 },
47}
48
49fn add_node(node: &SerializerNode, nodes: &mut Vec<FlatNode>, parent_nid: u64) -> u64 {
50 let nid = nodes.len() as u64;
51 match node {
52 SerializerNode::Directory { name: _, entries } => {
53 nodes.push(FlatNode {
55 nid,
56 is_dir: true,
57 contents: FlatNodeContents::Directory { entries: Vec::new() },
58 data_block: 0,
59 size: 0,
60 });
61
62 let mut child_entries = Vec::new();
63 child_entries.push((".".to_string(), nid, true));
64 child_entries.push(("..".to_string(), parent_nid, true));
65
66 for child in entries {
67 let child_name = child.name().to_string();
68 let child_nid = add_node(child, nodes, nid);
69 let child_is_dir = nodes[child_nid as usize].is_dir;
70 child_entries.push((child_name, child_nid, child_is_dir));
71 }
72
73 child_entries.sort_by(|a, b| a.0.cmp(&b.0));
74 nodes[nid as usize].contents = FlatNodeContents::Directory { entries: child_entries };
75 }
76 SerializerNode::File { name: _, data } => {
77 nodes.push(FlatNode {
78 nid,
79 is_dir: false,
80 contents: FlatNodeContents::File { data: data.clone() },
81 data_block: 0,
82 size: 0,
83 });
84 }
85 }
86 nid
87}
88
89pub fn serialize(root_entries: &[SerializerNode]) -> Vec<u8> {
93 let mut nodes = Vec::<FlatNode>::new();
94
95 nodes.push(FlatNode {
97 nid: 0,
98 is_dir: true,
99 contents: FlatNodeContents::Directory { entries: Vec::new() },
100 data_block: 0,
101 size: 0,
102 });
103
104 let mut root_child_entries = Vec::new();
105 root_child_entries.push((".".to_string(), 0, true));
106 root_child_entries.push(("..".to_string(), 0, true));
107
108 for child in root_entries {
109 let child_name = child.name().to_string();
110 let child_nid = add_node(child, &mut nodes, 0);
111 let child_is_dir = nodes[child_nid as usize].is_dir;
112 root_child_entries.push((child_name, child_nid, child_is_dir));
113 }
114
115 root_child_entries.sort_by(|a, b| a.0.cmp(&b.0));
116 nodes[0].contents = FlatNodeContents::Directory { entries: root_child_entries };
117
118 let inode_blocks = ((nodes.len() * 32) + 4095) / 4096;
120 let mut next_free_block = 1 + inode_blocks as u32;
121
122 for i in 0..nodes.len() {
123 match &nodes[i].contents {
124 FlatNodeContents::Directory { .. } => {
125 nodes[i].data_block = next_free_block;
126 nodes[i].size = 4096;
127 next_free_block += 1;
128 }
129 FlatNodeContents::File { data } => {
130 let len = data.len() as u64;
131 nodes[i].size = len;
132 if len > 0 {
133 nodes[i].data_block = next_free_block;
134 let blocks_needed = (len + 4095) / 4096;
135 next_free_block += blocks_needed as u32;
136 } else {
137 nodes[i].data_block = 0;
138 }
139 }
140 }
141 }
142
143 let total_blocks = next_free_block;
144 let mut image = vec![0u8; total_blocks as usize * 4096];
145
146 let sb = format::SuperBlock {
148 magic: LEU32::new(format::EROFS_MAGIC),
149 checksum: LEU32::new(0),
150 feature_compat: LEU32::new(0),
151 block_size_bits: 12, sb_ext_slots: 0,
153 root_nid: LEU16::new(0),
154 inode_count: LEU64::new(nodes.len() as u64),
155 epoch: LEU64::new(0),
156 fixed_nsec: LEU32::new(0),
157 blocks: LEU32::new(total_blocks),
158 meta_block_addr: LEU32::new(1),
159 xattr_block_addr: LEU32::new(0),
160 uuid: [0; 16],
161 volume_name: [0; 16],
162 feature_incompat: LEU32::new(0),
163 available_compr_algs: LEU16::new(0),
164 extra_devices: LEU32::new(0),
165 dirblkbits: 0,
166 reserved: [0; 37],
167 };
168 image[1024..1024 + 128].copy_from_slice(sb.as_bytes());
169
170 for i in 0..nodes.len() {
172 let mode = if nodes[i].is_dir {
173 0o040000 | 0o755 } else {
175 0o100000 | 0o644 };
177
178 let link_count = if nodes[i].is_dir { 2 } else { 1 };
179 let i_u = nodes[i].data_block.to_le_bytes();
180
181 let inode = format::InodeCompact {
182 format: LEU16::new(0), xattr_icount: LEU16::new(0),
184 mode: LEU16::new(mode),
185 link_count: LEU16::new(link_count),
186 size: LEU32::new(nodes[i].size as u32),
187 reserved_1: [0; 4],
188 i_u,
189 ino: LEU32::new(nodes[i].nid as u32),
190 uid: LEU16::new(0),
191 gid: LEU16::new(0),
192 reserved_2: [0; 4],
193 };
194 let offset = 4096 + i * 32;
195 image[offset..offset + 32].copy_from_slice(inode.as_bytes());
196 }
197
198 for i in 0..nodes.len() {
200 match &nodes[i].contents {
201 FlatNodeContents::Directory { entries } => {
202 let mut dir_block = vec![0u8; 4096];
203 let k = entries.len();
204 let mut current_nameoff = (k * 12) as u16;
205
206 let mut dirents = Vec::new();
207 let mut name_bytes = Vec::new();
208
209 for (name, nid, is_dir) in entries {
210 let file_type = if *is_dir { 2 } else { 1 };
211 dirents.push(format::Dirent {
212 nid: LEU64::new(*nid),
213 nameoff: LEU16::new(current_nameoff),
214 file_type,
215 reserved: 0,
216 });
217
218 name_bytes.extend_from_slice(name.as_bytes());
219 current_nameoff += name.as_bytes().len() as u16;
220 }
221
222 let dirents_bytes = dirents.as_slice().as_bytes();
223 dir_block[..dirents_bytes.len()].copy_from_slice(dirents_bytes);
224
225 let names_start = dirents_bytes.len();
226 dir_block[names_start..names_start + name_bytes.len()].copy_from_slice(&name_bytes);
227
228 let offset = nodes[i].data_block as usize * 4096;
229 image[offset..offset + 4096].copy_from_slice(&dir_block);
230 }
231 FlatNodeContents::File { data } => {
232 if !data.is_empty() {
233 let offset = nodes[i].data_block as usize * 4096;
234 image[offset..offset + data.len()].copy_from_slice(data);
235 }
236 }
237 }
238 }
239
240 image
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use erofs::readers::VecReader;
247 use erofs::{ErofsFilesystem, Node};
248 use std::sync::Arc;
249
250 fn assert_dir_recursive(
251 fs: &ErofsFilesystem,
252 expected_entries: &[SerializerNode],
253 actual_dir: &erofs::DirectoryNode,
254 ) {
255 let mut actual_entries_buf = vec![erofs::DirectoryEntry::default(); 100];
256 let filled = fs.read_directory(actual_dir, 0, &mut actual_entries_buf).unwrap();
257 let mut actual_entries = actual_entries_buf[..filled].to_vec();
258
259 actual_entries.retain(|e| e.name != "." && e.name != "..");
261
262 assert_eq!(actual_entries.len(), expected_entries.len(), "Directory entry count mismatch");
263
264 let mut sorted_expected: Vec<&SerializerNode> = expected_entries.iter().collect();
265 sorted_expected.sort_by(|a, b| a.name().cmp(b.name()));
266
267 for (i, expected_node) in sorted_expected.iter().enumerate() {
268 let actual_entry = &actual_entries[i];
269 assert_eq!(actual_entry.name, expected_node.name());
270
271 let child_node = fs.node(actual_entry.nid).expect("failed to read child node");
272
273 match expected_node {
274 SerializerNode::Directory { entries, .. } => {
275 let actual_child_dir = match child_node {
276 Node::Directory(d) => d,
277 _ => panic!("Expected directory node for {}", expected_node.name()),
278 };
279 assert_dir_recursive(fs, entries, &actual_child_dir);
280 }
281 SerializerNode::File { data, .. } => {
282 let actual_child_file = match child_node {
283 Node::File(f) => f,
284 _ => panic!("Expected file node for {}", expected_node.name()),
285 };
286 assert_eq!(actual_child_file.size(), data.len() as u64);
287 let mut file_buf = vec![0u8; data.len()];
288 fs.read_file_range(&actual_child_file, 0, &mut file_buf).unwrap();
289 assert_eq!(&file_buf, data);
290 }
291 }
292 }
293 }
294
295 #[fuchsia::test]
296 fn test_serialize_and_parse() {
297 let tree = vec![
298 SerializerNode::File { name: "file1".to_string(), data: b"hello world".to_vec() },
299 SerializerNode::Directory {
300 name: "dir1".to_string(),
301 entries: vec![
302 SerializerNode::File {
303 name: "file2".to_string(),
304 data: b"another file!".to_vec(),
305 },
306 SerializerNode::Directory {
307 name: "subdir".to_string(),
308 entries: vec![SerializerNode::File {
309 name: "file3".to_string(),
310 data: b"nested file".to_vec(),
311 }],
312 },
313 ],
314 },
315 ];
316
317 let image = serialize(&tree);
318 let reader = Arc::new(VecReader::new(image));
319 let fs = ErofsFilesystem::new(reader).expect("Failed to parse serialized EROFS image");
320
321 let root = fs.root_node();
322 assert_eq!(root.ino(), 0);
323
324 assert_dir_recursive(&fs, &tree, &root);
325 }
326}