Skip to main content

erofs_serializer/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! An extremely simple erofs serializer. Doesn't do any efficient allocation, everything is
6//! compact inodes with FlatPlain data layouts. This is intended to allow creating on-the-fly erofs
7//! images for conformance testing the vfs implementation as opposed to making robust erofs images.
8
9use erofs::format;
10use zerocopy::IntoBytes;
11use zerocopy::byteorder::little_endian::{U16 as LEU16, U32 as LEU32, U64 as LEU64};
12
13/// A node in the tree to be serialized.
14#[derive(Debug, Clone)]
15pub enum SerializerNode {
16    /// A directory with node children.
17    Directory { name: String, entries: Vec<SerializerNode> },
18    /// A file with byte contents.
19    File { name: String, data: Vec<u8> },
20    /// A symlink pointing to a target.
21    Symlink { name: String, target: Vec<u8> },
22}
23
24impl SerializerNode {
25    pub fn name(&self) -> &str {
26        match self {
27            Self::Directory { name, .. } => name,
28            Self::File { name, .. } => name,
29            Self::Symlink { name, .. } => name,
30        }
31    }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35enum NodeType {
36    Dir,
37    File,
38    Symlink,
39}
40
41struct FlatNode {
42    nid: u64,
43    node_type: NodeType,
44    contents: FlatNodeContents,
45    data_block: u32,
46    size: u64,
47}
48
49enum FlatNodeContents {
50    Directory {
51        // (name, nid, node_type)
52        entries: Vec<(String, u64, NodeType)>,
53    },
54    File {
55        data: Vec<u8>,
56    },
57    Symlink {
58        target: Vec<u8>,
59    },
60}
61
62fn add_node(node: &SerializerNode, nodes: &mut Vec<FlatNode>, parent_nid: u64) -> u64 {
63    let nid = nodes.len() as u64;
64    match node {
65        SerializerNode::Directory { name: _, entries } => {
66            // Push placeholder
67            nodes.push(FlatNode {
68                nid,
69                node_type: NodeType::Dir,
70                contents: FlatNodeContents::Directory { entries: Vec::new() },
71                data_block: 0,
72                size: 0,
73            });
74
75            let mut child_entries = Vec::new();
76            child_entries.push((".".to_string(), nid, NodeType::Dir));
77            child_entries.push(("..".to_string(), parent_nid, NodeType::Dir));
78
79            for child in entries {
80                let child_name = child.name().to_string();
81                let child_nid = add_node(child, nodes, nid);
82                let child_type = nodes[child_nid as usize].node_type;
83                child_entries.push((child_name, child_nid, child_type));
84            }
85
86            child_entries.sort_by(|a, b| a.0.cmp(&b.0));
87            nodes[nid as usize].contents = FlatNodeContents::Directory { entries: child_entries };
88        }
89        SerializerNode::File { name: _, data } => {
90            nodes.push(FlatNode {
91                nid,
92                node_type: NodeType::File,
93                contents: FlatNodeContents::File { data: data.clone() },
94                data_block: 0,
95                size: 0,
96            });
97        }
98        SerializerNode::Symlink { name: _, target } => {
99            nodes.push(FlatNode {
100                nid,
101                node_type: NodeType::Symlink,
102                contents: FlatNodeContents::Symlink { target: target.clone() },
103                data_block: 0,
104                size: 0,
105            });
106        }
107    }
108    nid
109}
110
111/// Serialize a directory tree into a very simple erofs image. Intended for testing at the moment.
112/// As such, it may panic in various edge-cases and is not especially efficient at laying out the
113/// metadata.
114pub fn serialize(root_entries: &[SerializerNode]) -> Vec<u8> {
115    let mut nodes = Vec::<FlatNode>::new();
116
117    // Create root directory at NID 0
118    nodes.push(FlatNode {
119        nid: 0,
120        node_type: NodeType::Dir,
121        contents: FlatNodeContents::Directory { entries: Vec::new() },
122        data_block: 0,
123        size: 0,
124    });
125
126    let mut root_child_entries = Vec::new();
127    root_child_entries.push((".".to_string(), 0, NodeType::Dir));
128    root_child_entries.push(("..".to_string(), 0, NodeType::Dir));
129
130    for child in root_entries {
131        let child_name = child.name().to_string();
132        let child_nid = add_node(child, &mut nodes, 0);
133        let child_type = nodes[child_nid as usize].node_type;
134        root_child_entries.push((child_name, child_nid, child_type));
135    }
136
137    root_child_entries.sort_by(|a, b| a.0.cmp(&b.0));
138    nodes[0].contents = FlatNodeContents::Directory { entries: root_child_entries };
139
140    // Allocate blocks
141    let inode_blocks = (nodes.len() * 32).div_ceil(4096);
142    let mut next_free_block = 1 + inode_blocks as u32;
143
144    for i in 0..nodes.len() {
145        match &nodes[i].contents {
146            FlatNodeContents::Directory { .. } => {
147                nodes[i].data_block = next_free_block;
148                nodes[i].size = 4096;
149                next_free_block += 1;
150            }
151            FlatNodeContents::File { data } => {
152                let len = data.len() as u64;
153                nodes[i].size = len;
154                if len > 0 {
155                    nodes[i].data_block = next_free_block;
156                    let blocks_needed = len.div_ceil(4096);
157                    next_free_block += blocks_needed as u32;
158                } else {
159                    nodes[i].data_block = 0;
160                }
161            }
162            FlatNodeContents::Symlink { target } => {
163                let len = target.len() as u64;
164                nodes[i].size = len;
165                if len > 0 {
166                    nodes[i].data_block = next_free_block;
167                    let blocks_needed = len.div_ceil(4096);
168                    next_free_block += blocks_needed as u32;
169                } else {
170                    nodes[i].data_block = 0;
171                }
172            }
173        }
174    }
175
176    let total_blocks = next_free_block;
177    let mut image = vec![0u8; total_blocks as usize * 4096];
178
179    // 1. Write Superblock
180    let sb = format::SuperBlock {
181        magic: LEU32::new(format::EROFS_MAGIC),
182        checksum: LEU32::new(0),
183        feature_compat: LEU32::new(0),
184        block_size_bits: 12, // 4096
185        sb_ext_slots: 0,
186        root_nid: LEU16::new(0),
187        inode_count: LEU64::new(nodes.len() as u64),
188        epoch: LEU64::new(1700000000),
189        fixed_nsec: LEU32::new(0),
190        blocks: LEU32::new(total_blocks),
191        meta_block_addr: LEU32::new(1),
192        xattr_block_addr: LEU32::new(0),
193        uuid: [0; 16],
194        volume_name: [0; 16],
195        feature_incompat: LEU32::new(0),
196        available_compr_algs: LEU16::new(0),
197        extra_devices: LEU32::new(0),
198        dirblkbits: 0,
199        reserved: [0; 37],
200    };
201    image[1024..1024 + 128].copy_from_slice(sb.as_bytes());
202
203    // 2. Write Inodes
204    for i in 0..nodes.len() {
205        let mode = match nodes[i].node_type {
206            NodeType::Dir => 0o040000 | 0o755,     // S_IFDIR | rwxr-xr-x
207            NodeType::File => 0o100000 | 0o644,    // S_IFREG | rw-r--r--
208            NodeType::Symlink => 0o120000 | 0o777, // S_IFLNK | rwxrwxrwx
209        };
210
211        let link_count = if nodes[i].node_type == NodeType::Dir { 2 } else { 1 };
212        let i_u = nodes[i].data_block.to_le_bytes();
213
214        let inode = format::InodeCompact {
215            format: LEU16::new(0), // Compact + FlatPlain
216            xattr_icount: LEU16::new(0),
217            mode: LEU16::new(mode),
218            link_count: LEU16::new(link_count),
219            size: LEU32::new(nodes[i].size as u32),
220            reserved_1: [0; 4],
221            i_u,
222            ino: LEU32::new(nodes[i].nid as u32),
223            uid: LEU16::new(100),
224            gid: LEU16::new(200),
225            reserved_2: [0; 4],
226        };
227        let offset = 4096 + i * 32;
228        image[offset..offset + 32].copy_from_slice(inode.as_bytes());
229    }
230
231    // 3. Write Data Blocks
232    for i in 0..nodes.len() {
233        match &nodes[i].contents {
234            FlatNodeContents::Directory { entries } => {
235                let mut dir_block = vec![0u8; 4096];
236                let k = entries.len();
237                let mut current_nameoff = (k * 12) as u16;
238
239                let mut dirents = Vec::new();
240                let mut name_bytes = Vec::new();
241
242                for (name, nid, node_type) in entries {
243                    let file_type = match node_type {
244                        NodeType::Dir => 2,
245                        NodeType::File => 1,
246                        NodeType::Symlink => 7,
247                    };
248                    dirents.push(format::Dirent {
249                        nid: LEU64::new(*nid),
250                        nameoff: LEU16::new(current_nameoff),
251                        file_type,
252                        reserved: 0,
253                    });
254
255                    name_bytes.extend_from_slice(name.as_bytes());
256                    current_nameoff += name.as_bytes().len() as u16;
257                }
258
259                let dirents_bytes = dirents.as_slice().as_bytes();
260                dir_block[..dirents_bytes.len()].copy_from_slice(dirents_bytes);
261
262                let names_start = dirents_bytes.len();
263                dir_block[names_start..names_start + name_bytes.len()].copy_from_slice(&name_bytes);
264
265                let offset = nodes[i].data_block as usize * 4096;
266                image[offset..offset + 4096].copy_from_slice(&dir_block);
267            }
268            FlatNodeContents::File { data } => {
269                if !data.is_empty() {
270                    let offset = nodes[i].data_block as usize * 4096;
271                    image[offset..offset + data.len()].copy_from_slice(data);
272                }
273            }
274            FlatNodeContents::Symlink { target } => {
275                if !target.is_empty() {
276                    let offset = nodes[i].data_block as usize * 4096;
277                    image[offset..offset + target.len()].copy_from_slice(target);
278                }
279            }
280        }
281    }
282
283    image
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use erofs::readers::VecReader;
290    use erofs::{ErofsFilesystem, Node};
291    use std::sync::Arc;
292
293    fn assert_dir_recursive(
294        fs: &ErofsFilesystem,
295        expected_entries: &[SerializerNode],
296        actual_dir: &erofs::DirectoryNode,
297    ) {
298        let mut actual_entries_buf = vec![erofs::DirectoryEntry::default(); 100];
299        let filled = fs.read_directory(actual_dir, 0, &mut actual_entries_buf).unwrap();
300        let mut actual_entries = actual_entries_buf[..filled].to_vec();
301
302        // EROFS includes '.' and '..' so filter them out first
303        actual_entries.retain(|e| e.name != "." && e.name != "..");
304
305        assert_eq!(actual_entries.len(), expected_entries.len(), "Directory entry count mismatch");
306
307        let mut sorted_expected: Vec<&SerializerNode> = expected_entries.iter().collect();
308        sorted_expected.sort_by(|a, b| a.name().cmp(b.name()));
309
310        for (i, expected_node) in sorted_expected.iter().enumerate() {
311            let actual_entry = &actual_entries[i];
312            assert_eq!(actual_entry.name, expected_node.name());
313
314            let child_node = fs.node(actual_entry.nid).expect("failed to read child node");
315
316            match expected_node {
317                SerializerNode::Directory { entries, .. } => {
318                    let actual_child_dir = match child_node {
319                        Node::Directory(d) => d,
320                        _ => panic!("Expected directory node for {}", expected_node.name()),
321                    };
322                    assert_dir_recursive(fs, entries, &actual_child_dir);
323                }
324                SerializerNode::File { data, .. } => {
325                    let actual_child_file = match child_node {
326                        Node::File(f) => f,
327                        _ => panic!("Expected file node for {}", expected_node.name()),
328                    };
329                    assert_eq!(actual_child_file.size(), data.len() as u64);
330                    let mut file_buf = vec![0u8; data.len()];
331                    fs.read_file_range(&actual_child_file, 0, &mut file_buf).unwrap();
332                    assert_eq!(&file_buf, data);
333                }
334                SerializerNode::Symlink { target, .. } => {
335                    let actual_child_symlink = match child_node {
336                        Node::Symlink(s) => s,
337                        _ => panic!("Expected symlink node for {}", expected_node.name()),
338                    };
339                    assert_eq!(actual_child_symlink.size(), target.len() as u64);
340                    let actual_target = fs.read_symlink(&actual_child_symlink).unwrap();
341                    assert_eq!(&actual_target, target);
342                }
343            }
344        }
345    }
346
347    #[fuchsia::test]
348    fn test_serialize_and_parse() {
349        let tree = vec![
350            SerializerNode::File { name: "file1".to_string(), data: b"hello world".to_vec() },
351            SerializerNode::Symlink { name: "symlink1".to_string(), target: b"file1".to_vec() },
352            SerializerNode::Directory {
353                name: "dir1".to_string(),
354                entries: vec![
355                    SerializerNode::File {
356                        name: "file2".to_string(),
357                        data: b"another file!".to_vec(),
358                    },
359                    SerializerNode::Directory {
360                        name: "subdir".to_string(),
361                        entries: vec![
362                            SerializerNode::File {
363                                name: "file3".to_string(),
364                                data: b"nested file".to_vec(),
365                            },
366                            SerializerNode::Symlink {
367                                name: "nested_symlink".to_string(),
368                                target: b"../../file1".to_vec(),
369                            },
370                        ],
371                    },
372                ],
373            },
374        ];
375
376        let image = serialize(&tree);
377        let reader = Arc::new(VecReader::new(image));
378        let fs = ErofsFilesystem::new(reader).expect("Failed to parse serialized EROFS image");
379
380        let root = fs.root_node();
381        assert_eq!(root.ino(), 0);
382
383        assert_dir_recursive(&fs, &tree, &root);
384    }
385}