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