Skip to main content

erofs/
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//! EROFS filesystem.
6
7use bitflags::bitflags;
8use crc::{CRC_32_ISCSI, Crc};
9use std::sync::Arc;
10use thiserror::Error;
11
12pub mod readers;
13use readers::{Reader, ReaderError, ReaderExt};
14
15pub mod format;
16
17bitflags! {
18    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
19    pub struct FeatureCompat: u32 {
20        /// If this feature is set, the checksum field in the superblock is valid and should be
21        /// used to verify the superblock integrity.
22        const SB_CHKSUM = 0x00000001;
23    }
24}
25
26/// Errors that can occur while interacting with an EROFS image.
27#[derive(Debug, Error, Clone, PartialEq)]
28pub enum ErofsError {
29    #[error("Unsupported compression algorithms: 0x{:X}", _0)]
30    UnsupportedCompressionAlgs(u16),
31    #[error("Unsupported feature incompat flags: 0x{:X}. Only 0x{:X} is supported", _0, _1)]
32    UnsupportedFeatureIncompat(u32, u32),
33
34    #[error("Parsing error: {}", _0)]
35    Parse(#[from] ParsingError),
36    #[error("Reader error: {}", _0)]
37    ReadError(#[from] ReaderError),
38}
39
40#[cfg(target_os = "fuchsia")]
41impl ErofsError {
42    pub fn to_status(self) -> zx::Status {
43        match self {
44            Self::UnsupportedCompressionAlgs(_) => zx::Status::NOT_SUPPORTED,
45            Self::UnsupportedFeatureIncompat(_, _) => zx::Status::NOT_SUPPORTED,
46            Self::Parse(_) => zx::Status::IO_DATA_INTEGRITY,
47            Self::ReadError(_) => zx::Status::IO,
48        }
49    }
50}
51
52/// Errors that can occur during parsing of an EROFS image.
53#[derive(Debug, Error, Clone, PartialEq)]
54pub enum ParsingError {
55    #[error("Invalid super block magic: 0x{:X}, should be 0x{:X}", _0, format::EROFS_MAGIC)]
56    InvalidSuperBlockMagic(u32),
57    #[error("Checksum mismatch: expected 0x{:X}, computed 0x{:X}", _0, _1)]
58    ChecksumMismatch(u32, u32),
59    #[error("Invalid block size bits: {}, must be between 9 and 12", _0)]
60    InvalidBlockSizeBits(u8),
61
62    #[error("Invalid inode data layout: 0x{:X}", _0)]
63    InvalidInodeDataLayout(u16),
64    #[error("Invalid directory entry")]
65    InvalidDirectoryEntry,
66    #[error("Invalid file type: {}", _0)]
67    InvalidFileType(u8),
68    #[error("Directory entry name was not valid utf8")]
69    InvalidDirectoryEntryName(#[source] std::str::Utf8Error),
70    #[error("Inline data layout missing inline data")]
71    InlineDataLayoutMissingInlineData,
72
73    #[error("Invalid root node")]
74    InvalidRootNode,
75    #[error("Node has an invalid U value for its data layout")]
76    InvalidUValue,
77    #[error("Invalid nid: {}", _0)]
78    InvalidNid(u64),
79    #[error("Integer overflow during calculation")]
80    Overflow,
81}
82
83#[derive(Debug, Clone, Copy)]
84enum InodeDataUnion {
85    DataBlkAddrPlain(u32),
86    DataBlkAddrInline(u32),
87}
88
89impl InodeDataUnion {
90    fn parse(data: [u8; 4], format: InodeFormat) -> Self {
91        match format.data_layout {
92            InodeDataLayout::FlatPlain => {
93                InodeDataUnion::DataBlkAddrPlain(u32::from_le_bytes(data))
94            }
95            // Technically this is only valid for inline data where the size is more than a block.
96            InodeDataLayout::FlatInline => {
97                InodeDataUnion::DataBlkAddrInline(u32::from_le_bytes(data))
98            }
99        }
100    }
101}
102
103#[derive(Debug, Clone)]
104struct NodeInner {
105    inode_offset: u64,
106    format: InodeFormat,
107    mode: u16,
108    size: u64,
109    data_union: InodeDataUnion,
110    ino: u32,
111}
112
113impl NodeInner {
114    fn is_dir(&self) -> bool {
115        self.mode & 0x4000 != 0
116    }
117
118    fn inode_offset(&self) -> u64 {
119        self.inode_offset
120    }
121
122    /// Interpret the u field as a block address. This is only a valid interpretation on FlatPlain,
123    /// or on FlatInline if the size is larger than a block. This debug_asserts that the size is
124    /// larger than a block for the inline case to catch programming errors.
125    fn blkaddr(&self, block_size: u64) -> u64 {
126        match self.data_union {
127            InodeDataUnion::DataBlkAddrPlain(addr) => addr.into(),
128            InodeDataUnion::DataBlkAddrInline(addr) => {
129                debug_assert!(self.size / block_size > 0);
130                addr.into()
131            }
132        }
133    }
134
135    /// Safely calculate the on-disk offset for a read in this nodes data. This doesn't check out
136    /// of bounds errors.
137    fn blkaddr_offset(&self, block_size: u64, offset: u64) -> Result<u64, ParsingError> {
138        self.blkaddr(block_size)
139            .checked_mul(block_size)
140            .ok_or(ParsingError::Overflow)?
141            .checked_add(offset)
142            .ok_or(ParsingError::Overflow)
143    }
144
145    fn metadata_size(&self) -> u64 {
146        match self.format.version {
147            InodeVersion::Compact => 32,
148            InodeVersion::Extended => 64,
149        }
150    }
151}
152
153/// A directory node in the EROFS image.
154#[derive(Debug, Clone)]
155pub struct DirectoryNode(NodeInner);
156
157impl DirectoryNode {
158    pub fn size(&self) -> u64 {
159        self.0.size
160    }
161    pub fn ino(&self) -> u32 {
162        self.0.ino
163    }
164}
165
166/// File type for a directory entry.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
168pub enum FileType {
169    #[default]
170    Unknown = 0,
171    RegFile = 1,
172    Dir = 2,
173    ChrDev = 3,
174    BlkDev = 4,
175    Fifo = 5,
176    Sock = 6,
177    Symlink = 7,
178}
179
180impl TryFrom<u8> for FileType {
181    type Error = ParsingError;
182
183    fn try_from(value: u8) -> Result<Self, Self::Error> {
184        match value {
185            0 => Ok(FileType::Unknown),
186            1 => Ok(FileType::RegFile),
187            2 => Ok(FileType::Dir),
188            3 => Ok(FileType::ChrDev),
189            4 => Ok(FileType::BlkDev),
190            5 => Ok(FileType::Fifo),
191            6 => Ok(FileType::Sock),
192            7 => Ok(FileType::Symlink),
193            _ => Err(ParsingError::InvalidFileType(value)),
194        }
195    }
196}
197
198/// A directory entry in the EROFS image.
199#[derive(Debug, Clone, Default)]
200pub struct DirectoryEntry {
201    pub nid: u64,
202    pub file_type: FileType,
203    pub name: String,
204}
205
206/// A file node in the EROFS image.
207#[derive(Debug, Clone)]
208pub struct FileNode(NodeInner);
209
210impl FileNode {
211    pub fn size(&self) -> u64 {
212        self.0.size
213    }
214    pub fn ino(&self) -> u32 {
215        self.0.ino
216    }
217}
218
219/// A node in the EROFS image.
220#[derive(Debug, Clone)]
221pub enum Node {
222    Directory(DirectoryNode),
223    File(FileNode),
224}
225
226impl Node {
227    fn new(inner: NodeInner) -> Self {
228        if inner.is_dir() {
229            Node::Directory(DirectoryNode(inner))
230        } else {
231            Node::File(FileNode(inner))
232        }
233    }
234
235    fn parse_compact(
236        inode_offset: u64,
237        format: InodeFormat,
238        inode: format::InodeCompact,
239    ) -> Result<Self, ParsingError> {
240        let data_union = InodeDataUnion::parse(inode.i_u, format);
241        Ok(Self::new(NodeInner {
242            inode_offset,
243            format,
244            mode: inode.mode.get(),
245            size: inode.size.get().into(),
246            data_union,
247            ino: inode.ino.get(),
248        }))
249    }
250
251    fn parse_extended(
252        inode_offset: u64,
253        format: InodeFormat,
254        inode: format::InodeExtended,
255    ) -> Result<Self, ParsingError> {
256        let data_union = InodeDataUnion::parse(inode.i_u, format);
257        Ok(Self::new(NodeInner {
258            inode_offset,
259            format,
260            mode: inode.mode.get(),
261            size: inode.size.get(),
262            data_union,
263            ino: inode.ino.get(),
264        }))
265    }
266
267    fn from_nid(nid: u64, meta_addr: u64, reader: &dyn Reader) -> Result<Self, ErofsError> {
268        let node_offset =
269            nid.checked_mul(format::INODE_SLOT_SIZE).ok_or(ParsingError::InvalidNid(nid))?;
270        let inode_offset =
271            meta_addr.checked_add(node_offset).ok_or(ParsingError::InvalidNid(nid))?;
272        // Read the first 2 bytes to determine the inode format.
273        let mut head = [0u8; 2];
274        reader.read(inode_offset, &mut head)?;
275        let format = InodeFormat::parse(u16::from_le_bytes(head))?;
276        let node = match format.version {
277            InodeVersion::Compact => {
278                Self::parse_compact(inode_offset, format, reader.read_object(inode_offset)?)?
279            }
280            InodeVersion::Extended => {
281                Self::parse_extended(inode_offset, format, reader.read_object(inode_offset)?)?
282            }
283        };
284        Ok(node)
285    }
286
287    pub fn size(&self) -> u64 {
288        match self {
289            Node::Directory(node) => node.size(),
290            Node::File(node) => node.size(),
291        }
292    }
293
294    pub fn ino(&self) -> u32 {
295        match self {
296            Node::Directory(node) => node.ino(),
297            Node::File(node) => node.ino(),
298        }
299    }
300}
301
302/// The filesystem implementation for an EROFS image.
303pub struct ErofsFilesystem {
304    reader: Arc<dyn Reader>,
305    block_size: u64,
306    meta_addr: u64,
307    root_node: DirectoryNode,
308}
309
310impl ErofsFilesystem {
311    /// Creates a new filesystem instance for an EROFS image from a reader.
312    pub fn new(reader: Arc<dyn Reader>) -> Result<Self, ErofsError> {
313        let super_block = Self::parse_superblock(&reader)?;
314        let block_size = 1u64 << super_block.block_size_bits;
315        let meta_block_addr = super_block.meta_block_addr.get().into();
316        let meta_addr = block_size.checked_mul(meta_block_addr).ok_or(ParsingError::Overflow)?;
317        let root_nid = super_block.root_nid.get().into();
318        let root_node = match Node::from_nid(root_nid, meta_addr, &reader)? {
319            Node::Directory(node) => node,
320            _ => return Err(ParsingError::InvalidRootNode.into()),
321        };
322        Ok(Self { reader, block_size, meta_addr, root_node })
323    }
324
325    fn parse_superblock(reader: &dyn Reader) -> Result<format::SuperBlock, ErofsError> {
326        let sb: format::SuperBlock = reader.read_object(format::SUPERBLOCK_OFFSET)?;
327        if sb.magic.get() != format::EROFS_MAGIC {
328            return Err(ParsingError::InvalidSuperBlockMagic(sb.magic.get()).into());
329        }
330        // The max block size that can be made by tooling is 4096 right now, and the specified
331        // minimum is 512, so make sure we are in that window.
332        if sb.block_size_bits < 9 || sb.block_size_bits > 12 {
333            return Err(ParsingError::InvalidBlockSizeBits(sb.block_size_bits).into());
334        }
335        // TODO(https://fxbug.dev/479841115): Handle more feature_compat flags.
336        let feature_compat = FeatureCompat::from_bits_truncate(sb.feature_compat.get());
337        if feature_compat.contains(FeatureCompat::SB_CHKSUM) {
338            Self::check_superblock_checksum(reader, &sb)?;
339        }
340        // TODO(https://fxbug.dev/479841115): Handle feature_incompat flags.
341        if sb.feature_incompat.get() != 0 {
342            return Err(ErofsError::UnsupportedFeatureIncompat(sb.feature_incompat.get(), 0));
343        }
344        // TODO(https://fxbug.dev/479841115): Support compression. Validate we support all the
345        // listed compression algorithms when we do.
346        if sb.available_compr_algs.get() != 0 {
347            return Err(ErofsError::UnsupportedCompressionAlgs(sb.available_compr_algs.get()));
348        }
349        Ok(sb)
350    }
351
352    fn check_superblock_checksum(
353        reader: &dyn Reader,
354        sb: &format::SuperBlock,
355    ) -> Result<(), ErofsError> {
356        let block_size = 1usize << sb.block_size_bits;
357        let len = block_size - (format::SUPERBLOCK_OFFSET as usize) % block_size;
358        let mut buf = vec![0u8; len];
359        reader.read(format::SUPERBLOCK_OFFSET, &mut buf)?;
360
361        // Zero out checksum field, which is at a well-known offset off the superblock offset.
362        buf[4..8].copy_from_slice(&[0u8; 4]);
363
364        let crc = Crc::<u32>::new(&CRC_32_ISCSI);
365        let checksum = crc.checksum(&buf);
366        // Undo final bitwise inversion applied by the crc crate, as suggested by the EROFS docs
367        // (https://erofs.docs.kernel.org/en/latest/ondisk/core_ondisk.html#superblock-checksum)
368        let checksum = !checksum;
369
370        if checksum != sb.checksum.get() {
371            Err(ParsingError::ChecksumMismatch(sb.checksum.get(), checksum).into())
372        } else {
373            Ok(())
374        }
375    }
376
377    /// Returns the block size of the EROFS image.
378    pub fn block_size(&self) -> u64 {
379        self.block_size
380    }
381
382    /// Returns the node with the given nid.
383    pub fn node(&self, nid: u64) -> Result<Node, ErofsError> {
384        Node::from_nid(nid, self.meta_addr, &self.reader)
385    }
386
387    /// Returns the root node of the EROFS image.
388    pub fn root_node(&self) -> DirectoryNode {
389        self.root_node.clone()
390    }
391
392    /// Reads the data of the given file node into a buffer.
393    pub fn read_file_range(
394        &self,
395        node: &FileNode,
396        offset: u64,
397        buf: &mut [u8],
398    ) -> Result<usize, ErofsError> {
399        self.read_node_range(&node.0, offset, buf)
400    }
401
402    /// Read bytes from the node's data at an offset. The length of the read is determined by the
403    /// length of the provided output buf. The data is written into that buf. Returns the number of
404    /// bytes read.
405    ///
406    /// TODO(https://fxbug.dev/479841115): This is a traditional unix-y way of handling reads -
407    /// potentially reading less data than asked for - but we should determine whether that fits
408    /// our apis and tweak it if needed.
409    fn read_node_range(
410        &self,
411        node: &NodeInner,
412        offset: u64,
413        buf: &mut [u8],
414    ) -> Result<usize, ErofsError> {
415        if offset >= node.size {
416            return Ok(0);
417        }
418        let read_len = std::cmp::min(buf.len() as u64, node.size - offset) as usize;
419        let buf = &mut buf[..read_len];
420        let block_size = self.block_size();
421
422        match node.format.data_layout {
423            InodeDataLayout::FlatPlain => {
424                let read_offset = node.blkaddr_offset(block_size, offset)?;
425                self.reader.read(read_offset, buf)?;
426                Ok(read_len)
427            }
428            InodeDataLayout::FlatInline => {
429                // A node will _only_ have the flat inline layout if it has a tail that that fits
430                // inline after the inode, so we can assume any tail data is there.
431                let full_blocks_len = (node.size / block_size) * block_size;
432                let mut bytes_read = 0;
433
434                if offset < full_blocks_len {
435                    // If there are no full blocks and the full file is in the tail section, this
436                    // check will never be true, so this is a valid use of the u value.
437                    let current_read_len =
438                        std::cmp::min(read_len as u64, full_blocks_len - offset) as usize;
439                    let read_offset = node.blkaddr_offset(block_size, offset)?;
440                    self.reader.read(read_offset, &mut buf[..current_read_len])?;
441                    bytes_read += current_read_len;
442                }
443
444                if bytes_read < read_len {
445                    let remaining_len = read_len - bytes_read;
446                    let current_offset = offset + bytes_read as u64;
447                    // TODO(https://fxbug.dev/479841115): figure out how xattrs fit into this.
448                    let inline_data_offset = node
449                        .inode_offset()
450                        .checked_add(node.metadata_size())
451                        .ok_or(ParsingError::Overflow)?;
452                    let tail_offset = current_offset - full_blocks_len;
453                    let tail_read_offset = inline_data_offset
454                        .checked_add(tail_offset)
455                        .ok_or(ParsingError::Overflow)?;
456                    self.reader.read(tail_read_offset, &mut buf[bytes_read..])?;
457                    bytes_read += remaining_len;
458                }
459
460                Ok(bytes_read)
461            }
462        }
463    }
464
465    /// Read a number of entries from a directory, starting at entry_offset. Will retrieve up to
466    /// the number of entries in the directory or the size of the provided buffer, returning the
467    /// number of entries filled in the buffer. If there are less filled entries then the number of
468    /// entry slots provided in the buffer, there are no more entries in this directory. Entries
469    /// are sorted lexicographically. Reads past the end of the number of entries will return zero
470    /// entries filled.
471    ///
472    /// TODO(https://fxbug.dev/479841115): It is possible for directories to omit their "." entries
473    /// in erofs, and in that case there is a flag marking it and we are expected to synthesize it.
474    /// Parse that flag and implement it.
475    /// TODO(https://fxbug.dev/479841115): This API is slightly awkward to hold. We should consider
476    /// making it an iterator interface.
477    pub fn read_directory(
478        &self,
479        node: &DirectoryNode,
480        mut entry_offset: usize,
481        entries: &mut [DirectoryEntry],
482    ) -> Result<usize, ErofsError> {
483        let block_size = self.block_size();
484        let block_size_usize: usize = block_size as usize;
485        let mut entries_filled = 0;
486        let mut current_entry_index = 0;
487        let mut block_data = vec![0u8; block_size_usize];
488
489        for block in 0.. {
490            let base_offset = block * block_size;
491            let bytes_read = self.read_node_range(&node.0, base_offset, &mut block_data)?;
492            if bytes_read < format::DIRENT_SIZE {
493                // We must be done if there wasn't enough data left for another dirent.
494                return Ok(entries_filled);
495            }
496            block_data[bytes_read..].fill(0);
497
498            // Get the first dirent in the block to calculate the number of entries.
499            let (dirent0, _) = zerocopy::Ref::<&[u8], format::Dirent>::from_prefix(&block_data)
500                .map_err(|_| ParsingError::InvalidDirectoryEntry)?;
501            let nameoff0 = dirent0.nameoff.get() as usize;
502            if nameoff0 < format::DIRENT_SIZE || nameoff0 >= block_size_usize {
503                return Err(ParsingError::InvalidDirectoryEntry.into());
504            }
505            let entry_count = nameoff0 / format::DIRENT_SIZE;
506
507            // Check if the offset we want is even in this block.
508            if current_entry_index + entry_count <= entry_offset {
509                current_entry_index += entry_count;
510                continue;
511            }
512
513            // Get all the dirents and make sure the nameoffs won't cause out of bounds errors.
514            let dirents_raw = block_data
515                .get(..entry_count * format::DIRENT_SIZE)
516                .ok_or(ParsingError::InvalidDirectoryEntry)?;
517            let dirents: &[format::Dirent] =
518                &*zerocopy::Ref::<&[u8], [format::Dirent]>::from_bytes(dirents_raw)
519                    .map_err(|_| ParsingError::InvalidDirectoryEntry)?;
520
521            let block_entry_offset = entry_offset - current_entry_index;
522            let space = entries.len() - entries_filled;
523            let block_entry_end = std::cmp::min(
524                entry_count,
525                block_entry_offset.checked_add(space).ok_or(ParsingError::Overflow)?,
526            );
527
528            for i in block_entry_offset..block_entry_end {
529                let last_entry = i + 1 == entry_count;
530                let nameoff = dirents[i].nameoff.get() as usize;
531
532                let name_bytes = if last_entry {
533                    // For the last entry, it ends at the end of the block or is null-terminated.
534                    // Since block_data is padded with nulls, we can just split by 0.
535                    let name_data =
536                        block_data.get(nameoff..).ok_or(ParsingError::InvalidDirectoryEntry)?;
537                    name_data.split(|&x| x == 0).next().unwrap()
538                } else {
539                    let nameoff_next = dirents[i + 1].nameoff.get() as usize;
540                    block_data
541                        .get(nameoff..nameoff_next)
542                        .ok_or(ParsingError::InvalidDirectoryEntry)?
543                };
544
545                let name = std::str::from_utf8(name_bytes)
546                    .map_err(|e| ParsingError::InvalidDirectoryEntryName(e))?
547                    .to_string();
548                entries[entries_filled] = DirectoryEntry {
549                    nid: dirents[i].nid.get(),
550                    file_type: dirents[i].file_type.try_into()?,
551                    name,
552                };
553                entries_filled += 1;
554                if entries_filled == entries.len() {
555                    return Ok(entries_filled);
556                }
557            }
558
559            current_entry_index =
560                current_entry_index.checked_add(entry_count).ok_or(ParsingError::Overflow)?;
561            entry_offset = current_entry_index;
562        }
563
564        Ok(entries_filled)
565    }
566
567    /// Looks up a node by name in a directory.
568    pub fn lookup(&self, dir: &DirectoryNode, name: &str) -> Result<Option<Node>, ErofsError> {
569        let mut entry_offset = 0;
570        let mut buffer = vec![DirectoryEntry::default(); 16];
571
572        loop {
573            let filled = self.read_directory(dir, entry_offset, &mut buffer)?;
574            for i in 0..filled {
575                if buffer[i].name == name {
576                    let node = self.node(buffer[i].nid)?;
577                    return Ok(Some(node));
578                }
579            }
580            if filled < buffer.len() {
581                break;
582            }
583            entry_offset += filled;
584        }
585
586        Ok(None)
587    }
588}
589
590/// The version of the on-disk format of the inode. Can be either 32-byte compact or 64-byte
591/// extended.
592#[derive(Debug, Clone, Copy, PartialEq, Eq)]
593pub enum InodeVersion {
594    Compact,
595    Extended,
596}
597
598/// The layout of the data portion of the inode.
599#[derive(Debug, Clone, Copy, PartialEq, Eq)]
600pub enum InodeDataLayout {
601    /// The data union is interpreted as a block address. The data for this inode is stored in
602    /// consecutive blocks starting from that block address.
603    FlatPlain,
604    /// The data union is interpreted as a block address. The data for this inode is stored in
605    /// consecutive blocks starting from that block address, except for the tail of the data which
606    /// is stored immediately following this metadata. If the whole tail is inlined, the data union
607    /// is unused and doesn't matter. For this to be used, the data _must_ have a tail section that
608    /// fits within the current metadata block.
609    FlatInline,
610}
611
612/// The format of the inode, containing the version and data layout.
613#[derive(Debug, Clone, Copy)]
614pub struct InodeFormat {
615    pub version: InodeVersion,
616    pub data_layout: InodeDataLayout,
617}
618
619impl InodeFormat {
620    /// Parse the inode format from the given format value.
621    pub fn parse(format: u16) -> Result<Self, ParsingError> {
622        let version =
623            if format & 0x1 == 0 { InodeVersion::Compact } else { InodeVersion::Extended };
624        let data_layout_raw = (format >> 1) & 0x7;
625        let data_layout = match data_layout_raw {
626            0 => InodeDataLayout::FlatPlain,
627            2 => InodeDataLayout::FlatInline,
628            _ => return Err(ParsingError::InvalidInodeDataLayout(data_layout_raw)),
629        };
630        Ok(Self { version, data_layout })
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637    use crate::readers::VecReader;
638    use std::fs;
639    use test_case::test_case;
640
641    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
642    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
643    #[fuchsia::test]
644    fn test_parse_superblock(path: &str) {
645        let runfiles = fs::read(path).expect("failed to read test file");
646        let reader = Arc::new(VecReader::new(runfiles.clone()));
647        // The fs validates the superblock during construction.
648        let _fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
649
650        // Now mutate a byte in the superblock. This ensures the checksumming is actually happening
651        // and getting evaluated correctly.
652        let mut mutated_runfiles = runfiles.clone();
653        mutated_runfiles[1088] ^= 0xFF;
654
655        let reader = Arc::new(VecReader::new(mutated_runfiles));
656        let fs = ErofsFilesystem::new(reader);
657        assert!(fs.is_err());
658        match fs.err().unwrap() {
659            ErofsError::Parse(ParsingError::ChecksumMismatch(_, _)) => {}
660            e => panic!("Expected ChecksumMismatch error, got {:?}", e),
661        }
662    }
663
664    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
665    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
666    #[fuchsia::test]
667    fn test_list_dir(path: &str) {
668        let runfiles = fs::read(path).expect("failed to read test file");
669        let reader = Arc::new(VecReader::new(runfiles));
670        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
671        let root_node = fs.root_node();
672
673        let mut buf = vec![DirectoryEntry::default(); 16];
674        let filled = fs.read_directory(&root_node, 0, &mut buf).expect("failed to read directory");
675
676        let names: Vec<String> = buf[..filled].iter().map(|e| e.name.clone()).collect();
677        assert_eq!(names, vec![".", "..", "file1", "large_dir", "photosynthesis", "quantum"]);
678    }
679
680    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
681    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
682    #[fuchsia::test]
683    fn test_overflow_nid(path: &str) {
684        let runfiles = fs::read(path).expect("failed to read test file");
685        let reader = Arc::new(VecReader::new(runfiles));
686        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
687        let result = fs.node(u64::MAX);
688        assert!(result.is_err());
689        assert_eq!(result.unwrap_err(), ErofsError::Parse(ParsingError::InvalidNid(u64::MAX)));
690    }
691
692    #[test_case("/pkg/data/simple.erofs", "file1" ; "4096 block size file1")]
693    #[test_case("/pkg/data/simple_512.erofs", "file1" ; "512 block size file1")]
694    #[test_case("/pkg/data/simple.erofs", "photosynthesis" ; "4096 block size photosynthesis")]
695    #[test_case("/pkg/data/simple_512.erofs", "photosynthesis" ; "512 block size photosynthesis")]
696    #[fuchsia::test]
697    fn test_read_file_range(path: &str, name: &str) {
698        let runfiles = fs::read(path).expect("failed to read test file");
699        let reader = Arc::new(VecReader::new(runfiles));
700        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
701        let root_node = fs.root_node();
702
703        let node = fs.lookup(&root_node, name).expect("failed to lookup").expect("file not found");
704        let file_node = match node {
705            Node::File(f) => f,
706            _ => panic!("Expected file node"),
707        };
708
709        let size = file_node.size() as usize;
710        let mut buf = vec![0u8; size];
711        let bytes_read = fs.read_file_range(&file_node, 0, &mut buf).expect("failed to read");
712        assert_eq!(bytes_read, size);
713        if name == "file1" {
714            assert_eq!(&buf[..14], b"this is a file");
715        }
716
717        // Test partial read within file
718        let mut buf = vec![0u8; 5];
719        let bytes_read = fs.read_file_range(&file_node, 5, &mut buf).expect("failed to read");
720        assert_eq!(bytes_read, 5);
721        if name == "file1" {
722            assert_eq!(&buf, b"is a ");
723        }
724
725        // Test read spanning across EOF (buffer larger than remaining data)
726        let mut buf = vec![0u8; 100];
727        let bytes_read =
728            fs.read_file_range(&file_node, (size - 5) as u64, &mut buf).expect("failed to read");
729        assert_eq!(bytes_read, 5);
730        if name == "file1" {
731            assert_eq!(&buf[..5], b"file\n");
732        }
733
734        // Test read at EOF
735        let mut buf = vec![0u8; 100];
736        let bytes_read =
737            fs.read_file_range(&file_node, size as u64, &mut buf).expect("failed to read");
738        assert_eq!(bytes_read, 0);
739    }
740
741    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
742    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
743    #[fuchsia::test]
744    fn test_read_directory_pagination(path: &str) {
745        let runfiles = fs::read(path).expect("failed to read test file");
746        let reader = Arc::new(VecReader::new(runfiles));
747        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
748        let root_node = fs.root_node();
749
750        let expected_names = vec![".", "..", "file1", "large_dir", "photosynthesis", "quantum"];
751
752        // Test reading with buffer size 2 (pagination)
753        let mut buf = vec![DirectoryEntry::default(); 2];
754
755        // Page 1 (offset 0)
756        let filled = fs.read_directory(&root_node, 0, &mut buf).expect("failed to read dir");
757        assert_eq!(filled, 2);
758        assert_eq!(buf[0].name, expected_names[0]);
759        assert_eq!(buf[1].name, expected_names[1]);
760
761        // Page 2 (offset 2)
762        let filled = fs.read_directory(&root_node, 2, &mut buf).expect("failed to read dir");
763        assert_eq!(filled, 2);
764        assert_eq!(buf[0].name, expected_names[2]);
765        assert_eq!(buf[1].name, expected_names[3]);
766
767        // Page 4 (offset 5)
768        let filled = fs.read_directory(&root_node, 5, &mut buf).expect("failed to read dir");
769        assert_eq!(filled, 1);
770        assert_eq!(buf[0].name, expected_names[5]);
771
772        // Page 5 (offset 6 - EOF)
773        let filled = fs.read_directory(&root_node, 6, &mut buf).expect("failed to read dir");
774        assert_eq!(filled, 0);
775
776        // Test reading with buffer size 1 (extreme pagination)
777        let mut buf1 = vec![DirectoryEntry::default(); 1];
778        for i in 0..expected_names.len() {
779            let filled = fs.read_directory(&root_node, i, &mut buf1).expect("failed to read dir");
780            assert_eq!(filled, 1);
781            assert_eq!(buf1[0].name, expected_names[i]);
782        }
783        let filled = fs
784            .read_directory(&root_node, expected_names.len(), &mut buf1)
785            .expect("failed to read dir");
786        assert_eq!(filled, 0);
787    }
788
789    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
790    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
791    #[fuchsia::test]
792    fn test_read_directory_large_dir(path: &str) {
793        // Note: the large directory in the golden image is only large enough to split the entries
794        // into multiple blocks on the 512 block size golden.
795        let runfiles = fs::read(path).expect("failed to read test file");
796        let reader = Arc::new(VecReader::new(runfiles));
797        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
798        let root_node = fs.root_node();
799
800        let large_dir_node = fs
801            .lookup(&root_node, "large_dir")
802            .expect("failed to look up large_dir")
803            .expect("large_dir not found");
804
805        let large_dir = match large_dir_node {
806            Node::Directory(d) => d,
807            _ => panic!("Expected directory node"),
808        };
809
810        // Skip the first two entries, . and ..
811        let mut entry_offset = 2;
812        let mut buffer = vec![DirectoryEntry::default(); 16];
813        loop {
814            let filled = fs.read_directory(&large_dir, entry_offset, &mut buffer).unwrap();
815            for i in 0..filled {
816                // check the prefix
817                assert_eq!(buffer[i].name[..12], format!("file_number_"));
818            }
819            if filled < buffer.len() {
820                break;
821            }
822            entry_offset += filled;
823        }
824    }
825}