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//! A simple EROFS serializer for testing and benchmarks. Emits compact inodes using `FlatInline`
6//! when a file's tail fits within the metadata block and `FlatPlain` otherwise.
7
8use 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/// A node in the tree to be serialized.
17#[derive(Debug, Clone)]
18pub enum SerializerNode {
19    /// A directory with node children.
20    Directory { name: String, entries: Vec<SerializerNode> },
21    /// A file with byte contents.
22    File { name: String, data: Vec<u8> },
23    /// A symlink pointing to a target.
24    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        // (name, node_index, node_type)
57        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
175/// Serialize a directory tree into an EROFS image using compact inodes (`FlatInline` for files
176/// with tails that fit within a metadata block, and `FlatPlain` otherwise).
177pub fn serialize(root_entries: &[SerializerNode]) -> Vec<u8> {
178    let mut nodes = Vec::<FlatNode>::new();
179
180    // Create root directory at index 0 (which will receive NID 0 at offset 4096).
181    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    // 1. Assign metadata offsets and NIDs in the metadata zone (starting at block 1 = byte 4096).
207    // Files with a non-zero tail <= 4064 bytes use FlatInline so their tail sits immediately after
208    // their 32-byte InodeCompact within the same 4096-byte metadata block.
209    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    // 2. Build directory blocks first now that all NIDs are assigned, then allocate file and
235    // symlink data blocks so directory lookups do not sit adjacent to file data blocks.
236    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    // 3. Write Superblock
287    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, // 4096
292        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    // 4. Write Inodes (and inline tails) + Data Blocks
313    for i in 0..nodes.len() {
314        let mode = match nodes[i].node_type {
315            NodeType::Dir => 0o040000 | 0o755,     // S_IFDIR | rwxr-xr-x
316            NodeType::File => 0o100000 | 0o644,    // S_IFREG | rw-r--r--
317            NodeType::Symlink => 0o120000 | 0o777, // S_IFLNK | rwxrwxrwx
318        };
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        // Compact (0) | FlatPlain (0 << 1 = 0) or FlatInline (2 << 1 = 4)
323        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        // Verify '.' and '..' entries before filtering them out.
399        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        // Test boundary sizes across empty, 32-byte slot boundaries, max inline tail (4064),
517        // non-inlinable tail fallback to FlatPlain (4065..=4095), exact block (4096), and
518        // multi-block variants.
519        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        // 1. Empty root directory
558        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        // 2. Root directory with an empty subdirectory and an empty file
564        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        // Construct a directory where the first block's dirents (12B each) + names sum to
577        // exactly 4096 bytes (no trailing zero padding in block 0), plus one additional entry
578        // that spills into block 1:
579        //   "."  -> 12 + 1 = 13 bytes
580        //   ".." -> 12 + 2 = 14 bytes
581        //   15 entries with 250-byte names -> 15 * (12 + 250) = 3930 bytes
582        //   1 entry with 127-byte name -> 12 + 127 = 139 bytes
583        //   Total for first 18 entries = 13 + 14 + 3930 + 139 = 4096 bytes.
584        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        // 17th child entry spills into the second directory block.
597        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}