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}
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        // (name, nid, is_dir)
42        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            // Push placeholder
54            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
89/// Serialize a directory tree into a very simple erofs image. Intended for testing at the moment.
90/// As such, it may panic in various edge-cases and is not especially efficient at laying out the
91/// metadata.
92pub fn serialize(root_entries: &[SerializerNode]) -> Vec<u8> {
93    let mut nodes = Vec::<FlatNode>::new();
94
95    // Create root directory at NID 0
96    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    // Allocate blocks
119    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    // 1. Write Superblock
147    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, // 4096
152        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    // 2. Write Inodes
171    for i in 0..nodes.len() {
172        let mode = if nodes[i].is_dir {
173            0o040000 | 0o755 // S_IFDIR | rwxr-xr-x
174        } else {
175            0o100000 | 0o644 // S_IFREG | rw-r--r--
176        };
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), // Compact + FlatPlain
183            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    // 3. Write Data Blocks
199    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        // EROFS includes '.' and '..' so filter them out first
260        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}