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;
11use zerocopy::IntoBytes;
12use zerocopy::byteorder::little_endian::U32 as LEU32;
13
14pub mod readers;
15use readers::{Reader, ReaderError, ReaderExt};
16
17pub mod format;
18
19bitflags! {
20    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
21    pub struct FeatureCompat: u32 {
22        /// If this feature is set, the checksum field in the superblock is valid and should be
23        /// used to verify the superblock integrity.
24        const SB_CHKSUM = 0x00000001;
25    }
26}
27
28/// Errors that can occur while interacting with an EROFS image.
29#[derive(Debug, Error, Clone, PartialEq)]
30pub enum ErofsError {
31    #[error("Unsupported compression algorithms: 0x{:X}", _0)]
32    UnsupportedCompressionAlgs(u16),
33    #[error("Unsupported feature incompat flags: 0x{:X}. Only 0x{:X} is supported", _0, _1)]
34    UnsupportedFeatureIncompat(u32, u32),
35
36    #[error("Parsing error: {}", _0)]
37    Parse(#[from] ParsingError),
38    #[error("Reader error: {}", _0)]
39    ReadError(#[from] ReaderError),
40}
41
42#[cfg(target_os = "fuchsia")]
43impl ErofsError {
44    pub fn to_status(self) -> zx::Status {
45        match self {
46            Self::UnsupportedCompressionAlgs(_) => zx::Status::NOT_SUPPORTED,
47            Self::UnsupportedFeatureIncompat(_, _) => zx::Status::NOT_SUPPORTED,
48            Self::Parse(_) => zx::Status::IO_DATA_INTEGRITY,
49            Self::ReadError(_) => zx::Status::IO,
50        }
51    }
52}
53
54/// Errors that can occur during parsing of an EROFS image.
55#[derive(Debug, Error, Clone, PartialEq)]
56pub enum ParsingError {
57    #[error("Invalid super block magic: 0x{:X}, should be 0x{:X}", _0, format::EROFS_MAGIC)]
58    InvalidSuperBlockMagic(u32),
59    #[error("Checksum mismatch: expected 0x{:X}, computed 0x{:X}", _0, _1)]
60    ChecksumMismatch(u32, u32),
61    #[error("Invalid block size bits: {}, must be between 9 and 12", _0)]
62    InvalidBlockSizeBits(u8),
63
64    #[error("Invalid inode data layout: 0x{:X}", _0)]
65    InvalidInodeDataLayout(u16),
66    #[error("Invalid directory entry")]
67    InvalidDirectoryEntry,
68    #[error("Invalid file type: {}", _0)]
69    InvalidFileType(u8),
70    #[error("Directory entry name was not valid utf8")]
71    InvalidDirectoryEntryName(#[source] std::str::Utf8Error),
72    #[error("Inline data layout missing inline data")]
73    InlineDataLayoutMissingInlineData,
74
75    #[error("Invalid root node")]
76    InvalidRootNode,
77    #[error("Node has an invalid U value for its data layout")]
78    InvalidUValue,
79    #[error("Invalid nid: {}", _0)]
80    InvalidNid(u64),
81    #[error("Integer overflow during calculation")]
82    Overflow,
83    #[error("Missing shared xattr area but inode has shared xattrs")]
84    MissingSharedXattrArea,
85    #[error("Xattr entry extends past the end of the inline xattr region")]
86    XattrEntryOutOfBounds,
87    #[error("Invalid xattr namespace index: {}", _0)]
88    InvalidXattrNamespace(u8),
89}
90
91#[derive(Debug, Clone, Copy)]
92enum InodeDataUnion {
93    DataBlkAddrPlain(u32),
94    DataBlkAddrInline(u32),
95}
96
97impl InodeDataUnion {
98    fn parse(data: [u8; 4], format: InodeFormat) -> Self {
99        match format.data_layout {
100            InodeDataLayout::FlatPlain => {
101                InodeDataUnion::DataBlkAddrPlain(u32::from_le_bytes(data))
102            }
103            // Technically this is only valid for inline data where the size is more than a block.
104            InodeDataLayout::FlatInline => {
105                InodeDataUnion::DataBlkAddrInline(u32::from_le_bytes(data))
106            }
107        }
108    }
109}
110
111#[derive(Debug, Clone)]
112pub struct NodeInner {
113    inode_offset: u64,
114    format: InodeFormat,
115    mode: u16,
116    size: u64,
117    data_union: InodeDataUnion,
118    ino: u32,
119    nid: u64,
120    link_count: u32,
121    uid: u32,
122    gid: u32,
123    mtime_ns: u64,
124    xattr_icount: u16,
125}
126
127impl NodeInner {
128    fn is_dir(&self) -> bool {
129        self.mode & 0x4000 != 0
130    }
131
132    fn inode_offset(&self) -> u64 {
133        self.inode_offset
134    }
135
136    /// Interpret the u field as a block address. This is only a valid interpretation on FlatPlain,
137    /// or on FlatInline if the size is larger than a block. This debug_asserts that the size is
138    /// larger than a block for the inline case to catch programming errors.
139    fn blkaddr(&self, block_size: u64) -> u64 {
140        match self.data_union {
141            InodeDataUnion::DataBlkAddrPlain(addr) => addr.into(),
142            InodeDataUnion::DataBlkAddrInline(addr) => {
143                debug_assert!(self.size / block_size > 0);
144                addr.into()
145            }
146        }
147    }
148
149    /// Safely calculate the on-disk offset for a read in this nodes data. This doesn't check out
150    /// of bounds errors.
151    fn blkaddr_offset(&self, block_size: u64, offset: u64) -> Result<u64, ParsingError> {
152        self.blkaddr(block_size)
153            .checked_mul(block_size)
154            .ok_or(ParsingError::Overflow)?
155            .checked_add(offset)
156            .ok_or(ParsingError::Overflow)
157    }
158
159    fn metadata_size(&self) -> u64 {
160        match self.format.version {
161            InodeVersion::Compact => 32,
162            InodeVersion::Extended => 64,
163        }
164    }
165
166    fn inline_xattr_size(&self) -> u64 {
167        if self.xattr_icount == 0 { 0 } else { ((self.xattr_icount as u64 - 1) * 4) + 12 }
168    }
169
170    pub fn size(&self) -> u64 {
171        self.size
172    }
173    pub fn ino(&self) -> u32 {
174        self.ino
175    }
176    pub fn nid(&self) -> u64 {
177        self.nid
178    }
179    pub fn link_count(&self) -> u32 {
180        self.link_count
181    }
182    pub fn uid(&self) -> u32 {
183        self.uid
184    }
185    pub fn gid(&self) -> u32 {
186        self.gid
187    }
188    pub fn mtime_ns(&self) -> u64 {
189        self.mtime_ns
190    }
191    pub fn mode(&self) -> u16 {
192        self.mode
193    }
194}
195
196/// A directory node in the EROFS image.
197#[derive(Debug, Clone)]
198pub struct DirectoryNode(NodeInner);
199
200impl std::ops::Deref for DirectoryNode {
201    type Target = NodeInner;
202    fn deref(&self) -> &Self::Target {
203        &self.0
204    }
205}
206
207/// File type for a directory entry.
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
209pub enum FileType {
210    #[default]
211    Unknown = 0,
212    RegFile = 1,
213    Dir = 2,
214    ChrDev = 3,
215    BlkDev = 4,
216    Fifo = 5,
217    Sock = 6,
218    Symlink = 7,
219}
220
221impl TryFrom<u8> for FileType {
222    type Error = ParsingError;
223
224    fn try_from(value: u8) -> Result<Self, Self::Error> {
225        match value {
226            0 => Ok(FileType::Unknown),
227            1 => Ok(FileType::RegFile),
228            2 => Ok(FileType::Dir),
229            3 => Ok(FileType::ChrDev),
230            4 => Ok(FileType::BlkDev),
231            5 => Ok(FileType::Fifo),
232            6 => Ok(FileType::Sock),
233            7 => Ok(FileType::Symlink),
234            _ => Err(ParsingError::InvalidFileType(value)),
235        }
236    }
237}
238
239/// A directory entry in the EROFS image.
240#[derive(Debug, Clone, Default)]
241pub struct DirectoryEntry {
242    pub nid: u64,
243    pub file_type: FileType,
244    pub name: String,
245}
246
247/// A file node in the EROFS image.
248#[derive(Debug, Clone)]
249pub struct FileNode(NodeInner);
250
251impl std::ops::Deref for FileNode {
252    type Target = NodeInner;
253    fn deref(&self) -> &Self::Target {
254        &self.0
255    }
256}
257
258/// A node in the EROFS image.
259#[derive(Debug, Clone)]
260pub enum Node {
261    Directory(DirectoryNode),
262    File(FileNode),
263}
264
265impl Node {
266    fn new(inner: NodeInner) -> Self {
267        if inner.is_dir() {
268            Node::Directory(DirectoryNode(inner))
269        } else {
270            Node::File(FileNode(inner))
271        }
272    }
273
274    fn parse_compact(
275        nid: u64,
276        inode_offset: u64,
277        format: InodeFormat,
278        inode: format::InodeCompact,
279        build_time_ns: u64,
280    ) -> Result<Self, ParsingError> {
281        let data_union = InodeDataUnion::parse(inode.i_u, format);
282        Ok(Self::new(NodeInner {
283            inode_offset,
284            format,
285            mode: inode.mode.get(),
286            size: inode.size.get().into(),
287            data_union,
288            ino: inode.ino.get(),
289            nid,
290            link_count: inode.link_count.get().into(),
291            uid: inode.uid.get().into(),
292            gid: inode.gid.get().into(),
293            mtime_ns: build_time_ns,
294            xattr_icount: inode.xattr_icount.get(),
295        }))
296    }
297
298    fn parse_extended(
299        nid: u64,
300        inode_offset: u64,
301        format: InodeFormat,
302        inode: format::InodeExtended,
303    ) -> Result<Self, ParsingError> {
304        let data_union = InodeDataUnion::parse(inode.i_u, format);
305        let mtime_ns = inode
306            .mtime
307            .get()
308            .checked_mul(1_000_000_000)
309            .and_then(|t| t.checked_add(inode.mtime_ns.get().into()))
310            .ok_or(ParsingError::Overflow)?;
311        Ok(Self::new(NodeInner {
312            inode_offset,
313            format,
314            mode: inode.mode.get(),
315            size: inode.size.get(),
316            data_union,
317            ino: inode.ino.get(),
318            nid,
319            link_count: inode.link_count.get(),
320            uid: inode.uid.get(),
321            gid: inode.gid.get(),
322            mtime_ns,
323            xattr_icount: inode.xattr_icount.get(),
324        }))
325    }
326
327    fn from_nid(
328        nid: u64,
329        meta_addr: u64,
330        build_time_ns: u64,
331        reader: &dyn Reader,
332    ) -> Result<Self, ErofsError> {
333        let node_offset =
334            nid.checked_mul(format::INODE_SLOT_SIZE).ok_or(ParsingError::InvalidNid(nid))?;
335        let inode_offset =
336            meta_addr.checked_add(node_offset).ok_or(ParsingError::InvalidNid(nid))?;
337        // Read the first 2 bytes to determine the inode format.
338        let mut head = [0u8; 2];
339        reader.read(inode_offset, &mut head)?;
340        let format = InodeFormat::parse(u16::from_le_bytes(head))?;
341        let node = match format.version {
342            InodeVersion::Compact => Self::parse_compact(
343                nid,
344                inode_offset,
345                format,
346                reader.read_object(inode_offset)?,
347                build_time_ns,
348            )?,
349            InodeVersion::Extended => {
350                Self::parse_extended(nid, inode_offset, format, reader.read_object(inode_offset)?)?
351            }
352        };
353        Ok(node)
354    }
355}
356
357impl std::ops::Deref for Node {
358    type Target = NodeInner;
359    fn deref(&self) -> &Self::Target {
360        match self {
361            Node::Directory(d) => d,
362            Node::File(f) => f,
363        }
364    }
365}
366
367/// The filesystem implementation for an EROFS image.
368pub struct ErofsFilesystem {
369    reader: Arc<dyn Reader>,
370    block_size: u64,
371    meta_addr: u64,
372    xattr_addr: u64,
373    root_node: DirectoryNode,
374    total_bytes: u64,
375    total_inodes: u64,
376    build_time_ns: u64,
377}
378
379impl ErofsFilesystem {
380    /// Creates a new filesystem instance for an EROFS image from a reader.
381    pub fn new(reader: Arc<dyn Reader>) -> Result<Self, ErofsError> {
382        let super_block = Self::parse_superblock(&reader)?;
383        let block_size = 1u64 << super_block.block_size_bits;
384        let meta_block_addr = super_block.meta_block_addr.get().into();
385        let meta_addr = block_size.checked_mul(meta_block_addr).ok_or(ParsingError::Overflow)?;
386        let total_inodes = super_block.inode_count.get();
387        let build_time_ns = super_block
388            .epoch
389            .get()
390            .checked_mul(1_000_000_000)
391            .and_then(|t| t.checked_add(super_block.fixed_nsec.get().into()))
392            .ok_or(ParsingError::Overflow)?;
393        let total_bytes = (super_block.blocks.get() as u64) * block_size;
394        let xattr_block_addr = super_block.xattr_block_addr.get().into();
395        let xattr_addr = block_size.checked_mul(xattr_block_addr).ok_or(ParsingError::Overflow)?;
396        let root_nid = super_block.root_nid.get().into();
397        let root_node = match Node::from_nid(root_nid, meta_addr, build_time_ns, &reader)? {
398            Node::Directory(node) => node,
399            _ => return Err(ParsingError::InvalidRootNode.into()),
400        };
401        Ok(Self {
402            reader,
403            block_size,
404            meta_addr,
405            xattr_addr,
406            root_node,
407            total_bytes,
408            total_inodes,
409            build_time_ns,
410        })
411    }
412
413    fn parse_superblock(reader: &dyn Reader) -> Result<format::SuperBlock, ErofsError> {
414        let sb: format::SuperBlock = reader.read_object(format::SUPERBLOCK_OFFSET)?;
415        if sb.magic.get() != format::EROFS_MAGIC {
416            return Err(ParsingError::InvalidSuperBlockMagic(sb.magic.get()).into());
417        }
418        // The max block size that can be made by tooling is 4096 right now, and the specified
419        // minimum is 512, so make sure we are in that window.
420        if sb.block_size_bits < 9 || sb.block_size_bits > 12 {
421            return Err(ParsingError::InvalidBlockSizeBits(sb.block_size_bits).into());
422        }
423        // TODO(https://fxbug.dev/479841115): Handle more feature_compat flags.
424        let feature_compat = FeatureCompat::from_bits_truncate(sb.feature_compat.get());
425        if feature_compat.contains(FeatureCompat::SB_CHKSUM) {
426            Self::check_superblock_checksum(reader, &sb)?;
427        }
428        // TODO(https://fxbug.dev/479841115): Handle feature_incompat flags.
429        if sb.feature_incompat.get() != 0 {
430            return Err(ErofsError::UnsupportedFeatureIncompat(sb.feature_incompat.get(), 0));
431        }
432        // TODO(https://fxbug.dev/479841115): Support compression. Validate we support all the
433        // listed compression algorithms when we do.
434        if sb.available_compr_algs.get() != 0 {
435            return Err(ErofsError::UnsupportedCompressionAlgs(sb.available_compr_algs.get()));
436        }
437        Ok(sb)
438    }
439
440    fn check_superblock_checksum(
441        reader: &dyn Reader,
442        sb: &format::SuperBlock,
443    ) -> Result<(), ErofsError> {
444        let block_size = 1usize << sb.block_size_bits;
445        let len = block_size - (format::SUPERBLOCK_OFFSET as usize) % block_size;
446        let mut buf = vec![0u8; len];
447        reader.read(format::SUPERBLOCK_OFFSET, &mut buf)?;
448
449        // Zero out checksum field, which is at a well-known offset off the superblock offset.
450        buf[4..8].copy_from_slice(&[0u8; 4]);
451
452        let crc = Crc::<u32>::new(&CRC_32_ISCSI);
453        let checksum = crc.checksum(&buf);
454        // Undo final bitwise inversion applied by the crc crate, as suggested by the EROFS docs
455        // (https://erofs.docs.kernel.org/en/latest/ondisk/core_ondisk.html#superblock-checksum)
456        let checksum = !checksum;
457
458        if checksum != sb.checksum.get() {
459            Err(ParsingError::ChecksumMismatch(sb.checksum.get(), checksum).into())
460        } else {
461            Ok(())
462        }
463    }
464
465    /// Returns the block size of the EROFS image.
466    pub fn block_size(&self) -> u64 {
467        self.block_size
468    }
469
470    /// Returns the node with the given nid.
471    pub fn node(&self, nid: u64) -> Result<Node, ErofsError> {
472        Node::from_nid(nid, self.meta_addr, self.build_time_ns, &self.reader)
473    }
474
475    /// Returns the root node of the EROFS image.
476    pub fn root_node(&self) -> DirectoryNode {
477        self.root_node.clone()
478    }
479
480    pub fn total_bytes(&self) -> u64 {
481        self.total_bytes
482    }
483
484    pub fn total_inodes(&self) -> u64 {
485        self.total_inodes
486    }
487
488    /// Reads the data of the given file node into a buffer.
489    pub fn read_file_range(
490        &self,
491        node: &FileNode,
492        offset: u64,
493        buf: &mut [u8],
494    ) -> Result<usize, ErofsError> {
495        self.read_node_range(&node.0, offset, buf)
496    }
497
498    /// Read bytes from the node's data at an offset. The length of the read is determined by the
499    /// length of the provided output buf. The data is written into that buf. Returns the number of
500    /// bytes read.
501    ///
502    /// TODO(https://fxbug.dev/479841115): This is a traditional unix-y way of handling reads -
503    /// potentially reading less data than asked for - but we should determine whether that fits
504    /// our apis and tweak it if needed.
505    fn read_node_range(
506        &self,
507        node: &NodeInner,
508        offset: u64,
509        buf: &mut [u8],
510    ) -> Result<usize, ErofsError> {
511        if offset >= node.size {
512            return Ok(0);
513        }
514        let read_len = std::cmp::min(buf.len() as u64, node.size - offset) as usize;
515        let buf = &mut buf[..read_len];
516        let block_size = self.block_size();
517
518        match node.format.data_layout {
519            InodeDataLayout::FlatPlain => {
520                let read_offset = node.blkaddr_offset(block_size, offset)?;
521                self.reader.read(read_offset, buf)?;
522                Ok(read_len)
523            }
524            InodeDataLayout::FlatInline => {
525                // A node will _only_ have the flat inline layout if it has a tail that that fits
526                // inline after the inode, so we can assume any tail data is there.
527                let full_blocks_len = (node.size / block_size) * block_size;
528                let mut bytes_read = 0;
529
530                if offset < full_blocks_len {
531                    // If there are no full blocks and the full file is in the tail section, this
532                    // check will never be true, so this is a valid use of the u value.
533                    let current_read_len =
534                        std::cmp::min(read_len as u64, full_blocks_len - offset) as usize;
535                    let read_offset = node.blkaddr_offset(block_size, offset)?;
536                    self.reader.read(read_offset, &mut buf[..current_read_len])?;
537                    bytes_read += current_read_len;
538                }
539
540                if bytes_read < read_len {
541                    let remaining_len = read_len - bytes_read;
542                    let current_offset = offset + bytes_read as u64;
543                    let inline_xattr_size = node.inline_xattr_size();
544                    let inline_data_offset = node
545                        .inode_offset()
546                        .checked_add(node.metadata_size())
547                        .ok_or(ParsingError::Overflow)?
548                        .checked_add(inline_xattr_size)
549                        .ok_or(ParsingError::Overflow)?;
550                    let tail_offset = current_offset - full_blocks_len;
551                    let tail_read_offset = inline_data_offset
552                        .checked_add(tail_offset)
553                        .ok_or(ParsingError::Overflow)?;
554                    self.reader.read(tail_read_offset, &mut buf[bytes_read..])?;
555                    bytes_read += remaining_len;
556                }
557
558                Ok(bytes_read)
559            }
560        }
561    }
562
563    /// Read a number of entries from a directory, starting at entry_offset. Will retrieve up to
564    /// the number of entries in the directory or the size of the provided buffer, returning the
565    /// number of entries filled in the buffer. If there are less filled entries then the number of
566    /// entry slots provided in the buffer, there are no more entries in this directory. Entries
567    /// are sorted lexicographically. Reads past the end of the number of entries will return zero
568    /// entries filled.
569    ///
570    /// TODO(https://fxbug.dev/479841115): It is possible for directories to omit their "." entries
571    /// in erofs, and in that case there is a flag marking it and we are expected to synthesize it.
572    /// Parse that flag and implement it.
573    /// TODO(https://fxbug.dev/479841115): This API is slightly awkward to hold. We should consider
574    /// making it an iterator interface.
575    pub fn read_directory(
576        &self,
577        node: &DirectoryNode,
578        mut entry_offset: usize,
579        entries: &mut [DirectoryEntry],
580    ) -> Result<usize, ErofsError> {
581        let block_size = self.block_size();
582        let block_size_usize: usize = block_size as usize;
583        let mut entries_filled = 0;
584        let mut current_entry_index = 0;
585        let mut block_data = vec![0u8; block_size_usize];
586
587        for block in 0.. {
588            let base_offset = block * block_size;
589            let bytes_read = self.read_node_range(&node.0, base_offset, &mut block_data)?;
590            if bytes_read < format::DIRENT_SIZE {
591                // We must be done if there wasn't enough data left for another dirent.
592                return Ok(entries_filled);
593            }
594            block_data[bytes_read..].fill(0);
595
596            // Get the first dirent in the block to calculate the number of entries.
597            let (dirent0, _) = zerocopy::Ref::<&[u8], format::Dirent>::from_prefix(&block_data)
598                .map_err(|_| ParsingError::InvalidDirectoryEntry)?;
599            let nameoff0 = dirent0.nameoff.get() as usize;
600            if nameoff0 < format::DIRENT_SIZE || nameoff0 >= block_size_usize {
601                return Err(ParsingError::InvalidDirectoryEntry.into());
602            }
603            let entry_count = nameoff0 / format::DIRENT_SIZE;
604
605            // Check if the offset we want is even in this block.
606            if current_entry_index + entry_count <= entry_offset {
607                current_entry_index += entry_count;
608                continue;
609            }
610
611            // Get all the dirents and make sure the nameoffs won't cause out of bounds errors.
612            let dirents_raw = block_data
613                .get(..entry_count * format::DIRENT_SIZE)
614                .ok_or(ParsingError::InvalidDirectoryEntry)?;
615            let dirents: &[format::Dirent] =
616                &*zerocopy::Ref::<&[u8], [format::Dirent]>::from_bytes(dirents_raw)
617                    .map_err(|_| ParsingError::InvalidDirectoryEntry)?;
618
619            let block_entry_offset = entry_offset - current_entry_index;
620            let space = entries.len() - entries_filled;
621            let block_entry_end = std::cmp::min(
622                entry_count,
623                block_entry_offset.checked_add(space).ok_or(ParsingError::Overflow)?,
624            );
625
626            for i in block_entry_offset..block_entry_end {
627                let last_entry = i + 1 == entry_count;
628                let nameoff = dirents[i].nameoff.get() as usize;
629
630                let name_bytes = if last_entry {
631                    // For the last entry, it ends at the end of the block or is null-terminated.
632                    // Since block_data is padded with nulls, we can just split by 0.
633                    let name_data =
634                        block_data.get(nameoff..).ok_or(ParsingError::InvalidDirectoryEntry)?;
635                    name_data.split(|&x| x == 0).next().unwrap()
636                } else {
637                    let nameoff_next = dirents[i + 1].nameoff.get() as usize;
638                    block_data
639                        .get(nameoff..nameoff_next)
640                        .ok_or(ParsingError::InvalidDirectoryEntry)?
641                };
642
643                let name = std::str::from_utf8(name_bytes)
644                    .map_err(|e| ParsingError::InvalidDirectoryEntryName(e))?
645                    .to_string();
646                entries[entries_filled] = DirectoryEntry {
647                    nid: dirents[i].nid.get(),
648                    file_type: dirents[i].file_type.try_into()?,
649                    name,
650                };
651                entries_filled += 1;
652                if entries_filled == entries.len() {
653                    return Ok(entries_filled);
654                }
655            }
656
657            current_entry_index =
658                current_entry_index.checked_add(entry_count).ok_or(ParsingError::Overflow)?;
659            entry_offset = current_entry_index;
660        }
661
662        Ok(entries_filled)
663    }
664
665    /// Looks up a node by name in a directory.
666    pub fn lookup(&self, dir: &DirectoryNode, name: &str) -> Result<Option<Node>, ErofsError> {
667        let mut entry_offset = 0;
668        let mut buffer = vec![DirectoryEntry::default(); 16];
669
670        loop {
671            let filled = self.read_directory(dir, entry_offset, &mut buffer)?;
672            for i in 0..filled {
673                if buffer[i].name == name {
674                    let node = self.node(buffer[i].nid)?;
675                    return Ok(Some(node));
676                }
677            }
678            if filled < buffer.len() {
679                break;
680            }
681            entry_offset += filled;
682        }
683
684        Ok(None)
685    }
686
687    /// Returns an iterator over the xattr entry headers for a node.
688    pub fn iter_xattrs<'a>(&'a self, node: &NodeInner) -> Result<XattrIterator<'a>, ErofsError> {
689        if node.xattr_icount == 0 {
690            return Ok(XattrIterator {
691                reader: self.reader.as_ref(),
692                xattr_addr: self.xattr_addr,
693                shared_ids: Vec::new(),
694                inline_offset: 0,
695                inline_end: 0,
696            });
697        }
698        let xattr_metadata_size = node.inline_xattr_size();
699        let xattr_metadata_start =
700            node.inode_offset().checked_add(node.metadata_size()).ok_or(ParsingError::Overflow)?;
701
702        // Read the inline xattr header to get the details on the extended attributes for this node
703        let header: format::XattrInlineBodyHeader =
704            self.reader.read_object(xattr_metadata_start)?;
705        let shared_count = header.shared_count as usize;
706
707        let shared_ids_size = shared_count as u64 * 4;
708        let inline_entries_start = xattr_metadata_start + 12 + shared_ids_size;
709        let inline_end = xattr_metadata_start + xattr_metadata_size;
710
711        if inline_entries_start > inline_end {
712            return Err(ParsingError::XattrEntryOutOfBounds.into());
713        }
714
715        let shared_ids = if shared_count > 0 {
716            let mut ids = vec![LEU32::ZERO; shared_count];
717            self.reader.read(xattr_metadata_start + 12, ids.as_mut_bytes())?;
718            ids
719        } else {
720            Vec::new()
721        };
722
723        Ok(XattrIterator {
724            reader: self.reader.as_ref(),
725            xattr_addr: self.xattr_addr,
726            shared_ids,
727            inline_offset: inline_entries_start,
728            inline_end,
729        })
730    }
731
732    /// List all xattr names for a given node.
733    pub fn list_xattrs(&self, node: &NodeInner) -> Result<Vec<Vec<u8>>, ErofsError> {
734        let mut names = Vec::new();
735        for entry in self.iter_xattrs(node)? {
736            let entry = entry?;
737            names.push(entry.read_name(self.reader.as_ref())?);
738        }
739        Ok(names)
740    }
741
742    /// Get the value of a specific xattr for a given node.
743    pub fn get_xattr(&self, node: &NodeInner, name: &[u8]) -> Result<Option<Vec<u8>>, ErofsError> {
744        for entry in self.iter_xattrs(node)? {
745            let entry = entry?;
746            if entry.matches_name(self.reader.as_ref(), name)? {
747                return Ok(Some(entry.read_value(self.reader.as_ref())?));
748            }
749        }
750        Ok(None)
751    }
752}
753
754/// An iterator over xattr entry headers for an inode.
755pub struct XattrIterator<'a> {
756    reader: &'a dyn Reader,
757    xattr_addr: u64,
758    shared_ids: Vec<LEU32>,
759    inline_offset: u64,
760    inline_end: u64,
761}
762
763impl XattrIterator<'_> {
764    fn next_inner(&mut self) -> Result<Option<XattrEntryHeader>, ErofsError> {
765        if let Some(shared_id) = self.shared_ids.pop() {
766            let shared_entry_offset = self.xattr_addr + (shared_id.get() as u64 * 4);
767            if self.shared_ids.is_empty() {
768                self.shared_ids = Vec::new();
769            }
770            return Ok(Some(XattrEntryHeader::parse(self.reader, shared_entry_offset)?));
771        }
772
773        if self.inline_offset < self.inline_end {
774            if self.inline_offset + 4 > self.inline_end {
775                return Err(ParsingError::XattrEntryOutOfBounds.into());
776            }
777
778            let header = XattrEntryHeader::parse(self.reader, self.inline_offset)?;
779            let next_offset = self
780                .inline_offset
781                .checked_add(header.entry_aligned_size)
782                .ok_or(ParsingError::Overflow)?;
783            if next_offset > self.inline_end {
784                return Err(ParsingError::XattrEntryOutOfBounds.into());
785            }
786            self.inline_offset = next_offset;
787            Ok(Some(header))
788        } else {
789            Ok(None)
790        }
791    }
792}
793
794impl Iterator for XattrIterator<'_> {
795    type Item = Result<XattrEntryHeader, ErofsError>;
796
797    fn next(&mut self) -> Option<Self::Item> {
798        match self.next_inner() {
799            // Throw out the rest of the values if we encounter an error parsing the extended
800            // attributes. Since most of the errors are related to overflows and math issues, there
801            // is no safe way to recover for future attributes as the locations on disk are all
802            // relative to each other.
803            Err(e) => {
804                self.shared_ids = Vec::new();
805                self.inline_offset = self.inline_end;
806                Some(Err(e))
807            }
808            Ok(None) => None,
809            Ok(Some(x)) => Some(Ok(x)),
810        }
811    }
812}
813
814/// A parsed representation of an EROFS xattr entry record header.
815#[derive(Debug, Clone, Copy)]
816pub struct XattrEntryHeader {
817    pub offset: u64,
818    pub prefix: &'static [u8],
819    pub name_index: u8,
820    pub name_len: usize,
821    pub value_size: usize,
822    pub entry_aligned_size: u64,
823}
824
825impl XattrEntryHeader {
826    /// Read and validate an xattr entry record header from the reader.
827    pub fn parse(reader: &dyn Reader, offset: u64) -> Result<Self, ErofsError> {
828        let entry: format::XattrEntry = reader.read_object(offset)?;
829        let prefix = Self::get_xattr_prefix(entry.name_index)?;
830        let name_len = entry.name_len as usize;
831        let value_size = entry.value_size.get() as usize;
832
833        let entry_aligned_size = 4usize
834            .checked_add(name_len)
835            .and_then(|s| s.checked_add(value_size))
836            .and_then(|s| s.checked_next_multiple_of(4))
837            .ok_or(ParsingError::Overflow)? as u64;
838
839        Ok(Self {
840            offset,
841            prefix,
842            name_index: entry.name_index,
843            name_len,
844            value_size,
845            entry_aligned_size,
846        })
847    }
848
849    /// Check if this xattr entry matches the given full attribute name (prefix + suffix).
850    pub fn matches_name(&self, reader: &dyn Reader, name: &[u8]) -> Result<bool, ReaderError> {
851        let Some(suffix) = name.strip_prefix(self.prefix) else {
852            return Ok(false);
853        };
854        if suffix.len() != self.name_len {
855            return Ok(false);
856        }
857        if self.name_len == 0 {
858            // Implies suffix.len() is also zero because of the previous check.
859            return Ok(true);
860        }
861        let mut buf = vec![0u8; self.name_len];
862        reader.read(self.offset + 4, &mut buf)?;
863        Ok(buf == suffix)
864    }
865
866    /// Read the name of this xattr entry (prefix + suffix).
867    pub fn read_name(&self, reader: &dyn Reader) -> Result<Vec<u8>, ReaderError> {
868        let mut name_bytes = Vec::with_capacity(self.prefix.len() + self.name_len);
869        name_bytes.extend_from_slice(self.prefix);
870        if self.name_len > 0 {
871            name_bytes.resize(self.prefix.len() + self.name_len, 0);
872            reader.read(self.offset + 4, &mut name_bytes[self.prefix.len()..])?;
873        }
874        Ok(name_bytes)
875    }
876
877    /// Read the value payload for this entry.
878    pub fn read_value(&self, reader: &dyn Reader) -> Result<Vec<u8>, ReaderError> {
879        let mut value_bytes = vec![0u8; self.value_size];
880        reader.read(self.offset + 4 + self.name_len as u64, &mut value_bytes)?;
881        Ok(value_bytes)
882    }
883
884    /// Read both key name and value payload for this entry.
885    pub fn read_payload(&self, reader: &dyn Reader) -> Result<(Vec<u8>, Vec<u8>), ReaderError> {
886        let name = self.read_name(reader)?;
887        let value = self.read_value(reader)?;
888        Ok((name, value))
889    }
890
891    fn get_xattr_prefix(index: u8) -> Result<&'static [u8], ParsingError> {
892        match index {
893            1 => Ok(b"user."),
894            2 => Ok(b"system.posix_acl_access"),
895            3 => Ok(b"system.posix_acl_default"),
896            4 => Ok(b"trusted."),
897            6 => Ok(b"security."),
898            _ => Err(ParsingError::InvalidXattrNamespace(index)),
899        }
900    }
901}
902
903/// The version of the on-disk format of the inode. Can be either 32-byte compact or 64-byte
904/// extended.
905#[derive(Debug, Clone, Copy, PartialEq, Eq)]
906pub enum InodeVersion {
907    Compact,
908    Extended,
909}
910
911/// The layout of the data portion of the inode.
912#[derive(Debug, Clone, Copy, PartialEq, Eq)]
913pub enum InodeDataLayout {
914    /// The data union is interpreted as a block address. The data for this inode is stored in
915    /// consecutive blocks starting from that block address.
916    FlatPlain,
917    /// The data union is interpreted as a block address. The data for this inode is stored in
918    /// consecutive blocks starting from that block address, except for the tail of the data which
919    /// is stored immediately following this metadata. If the whole tail is inlined, the data union
920    /// is unused and doesn't matter. For this to be used, the data _must_ have a tail section that
921    /// fits within the current metadata block.
922    FlatInline,
923}
924
925/// The format of the inode, containing the version and data layout.
926#[derive(Debug, Clone, Copy)]
927pub struct InodeFormat {
928    pub version: InodeVersion,
929    pub data_layout: InodeDataLayout,
930}
931
932impl InodeFormat {
933    /// Parse the inode format from the given format value.
934    pub fn parse(format: u16) -> Result<Self, ParsingError> {
935        let version =
936            if format & 0x1 == 0 { InodeVersion::Compact } else { InodeVersion::Extended };
937        let data_layout_raw = (format >> 1) & 0x7;
938        let data_layout = match data_layout_raw {
939            0 => InodeDataLayout::FlatPlain,
940            2 => InodeDataLayout::FlatInline,
941            _ => return Err(ParsingError::InvalidInodeDataLayout(data_layout_raw)),
942        };
943        Ok(Self { version, data_layout })
944    }
945}
946
947#[cfg(test)]
948mod tests {
949    use super::*;
950    use crate::readers::VecReader;
951    use std::fs;
952    use test_case::test_case;
953    use zerocopy::byteorder::little_endian::{U16 as LEU16, U32 as LEU32, U64 as LEU64};
954
955    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
956    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
957    #[fuchsia::test]
958    fn test_parse_superblock(path: &str) {
959        let runfiles = fs::read(path).expect("failed to read test file");
960        let reader = Arc::new(VecReader::new(runfiles.clone()));
961        // The fs validates the superblock during construction.
962        let _fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
963
964        // Now mutate a byte in the superblock. This ensures the checksumming is actually happening
965        // and getting evaluated correctly.
966        let mut mutated_runfiles = runfiles.clone();
967        mutated_runfiles[1088] ^= 0xFF;
968
969        let reader = Arc::new(VecReader::new(mutated_runfiles));
970        let fs = ErofsFilesystem::new(reader);
971        assert!(fs.is_err());
972        match fs.err().unwrap() {
973            ErofsError::Parse(ParsingError::ChecksumMismatch(_, _)) => {}
974            e => panic!("Expected ChecksumMismatch error, got {:?}", e),
975        }
976    }
977
978    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
979    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
980    #[fuchsia::test]
981    fn test_list_dir(path: &str) {
982        let runfiles = fs::read(path).expect("failed to read test file");
983        let reader = Arc::new(VecReader::new(runfiles));
984        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
985        let root_node = fs.root_node();
986
987        let mut buf = vec![DirectoryEntry::default(); 16];
988        let filled = fs.read_directory(&root_node, 0, &mut buf).expect("failed to read directory");
989
990        let names: Vec<String> = buf[..filled].iter().map(|e| e.name.clone()).collect();
991        assert_eq!(names, vec![".", "..", "file1", "large_dir", "photosynthesis", "quantum"]);
992    }
993
994    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
995    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
996    #[fuchsia::test]
997    fn test_overflow_nid(path: &str) {
998        let runfiles = fs::read(path).expect("failed to read test file");
999        let reader = Arc::new(VecReader::new(runfiles));
1000        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
1001        let result = fs.node(u64::MAX);
1002        assert!(result.is_err());
1003        assert_eq!(result.unwrap_err(), ErofsError::Parse(ParsingError::InvalidNid(u64::MAX)));
1004    }
1005
1006    #[test_case("/pkg/data/simple.erofs", "file1" ; "4096 block size file1")]
1007    #[test_case("/pkg/data/simple_512.erofs", "file1" ; "512 block size file1")]
1008    #[test_case("/pkg/data/simple.erofs", "photosynthesis" ; "4096 block size photosynthesis")]
1009    #[test_case("/pkg/data/simple_512.erofs", "photosynthesis" ; "512 block size photosynthesis")]
1010    #[fuchsia::test]
1011    fn test_read_file_range(path: &str, name: &str) {
1012        let runfiles = fs::read(path).expect("failed to read test file");
1013        let reader = Arc::new(VecReader::new(runfiles));
1014        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
1015        let root_node = fs.root_node();
1016
1017        let node = fs.lookup(&root_node, name).expect("failed to lookup").expect("file not found");
1018        let file_node = match node {
1019            Node::File(f) => f,
1020            _ => panic!("Expected file node"),
1021        };
1022
1023        let size = file_node.size() as usize;
1024        let mut buf = vec![0u8; size];
1025        let bytes_read = fs.read_file_range(&file_node, 0, &mut buf).expect("failed to read");
1026        assert_eq!(bytes_read, size);
1027        if name == "file1" {
1028            assert_eq!(&buf[..14], b"this is a file");
1029        }
1030
1031        // Test partial read within file
1032        let mut buf = vec![0u8; 5];
1033        let bytes_read = fs.read_file_range(&file_node, 5, &mut buf).expect("failed to read");
1034        assert_eq!(bytes_read, 5);
1035        if name == "file1" {
1036            assert_eq!(&buf, b"is a ");
1037        }
1038
1039        // Test read spanning across EOF (buffer larger than remaining data)
1040        let mut buf = vec![0u8; 100];
1041        let bytes_read =
1042            fs.read_file_range(&file_node, (size - 5) as u64, &mut buf).expect("failed to read");
1043        assert_eq!(bytes_read, 5);
1044        if name == "file1" {
1045            assert_eq!(&buf[..5], b"file\n");
1046        }
1047
1048        // Test read at EOF
1049        let mut buf = vec![0u8; 100];
1050        let bytes_read =
1051            fs.read_file_range(&file_node, size as u64, &mut buf).expect("failed to read");
1052        assert_eq!(bytes_read, 0);
1053    }
1054
1055    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
1056    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
1057    #[fuchsia::test]
1058    fn test_read_directory_pagination(path: &str) {
1059        let runfiles = fs::read(path).expect("failed to read test file");
1060        let reader = Arc::new(VecReader::new(runfiles));
1061        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
1062        let root_node = fs.root_node();
1063
1064        let expected_names = vec![".", "..", "file1", "large_dir", "photosynthesis", "quantum"];
1065
1066        // Test reading with buffer size 2 (pagination)
1067        let mut buf = vec![DirectoryEntry::default(); 2];
1068
1069        // Page 1 (offset 0)
1070        let filled = fs.read_directory(&root_node, 0, &mut buf).expect("failed to read dir");
1071        assert_eq!(filled, 2);
1072        assert_eq!(buf[0].name, expected_names[0]);
1073        assert_eq!(buf[1].name, expected_names[1]);
1074
1075        // Page 2 (offset 2)
1076        let filled = fs.read_directory(&root_node, 2, &mut buf).expect("failed to read dir");
1077        assert_eq!(filled, 2);
1078        assert_eq!(buf[0].name, expected_names[2]);
1079        assert_eq!(buf[1].name, expected_names[3]);
1080
1081        // Page 4 (offset 5)
1082        let filled = fs.read_directory(&root_node, 5, &mut buf).expect("failed to read dir");
1083        assert_eq!(filled, 1);
1084        assert_eq!(buf[0].name, expected_names[5]);
1085
1086        // Page 5 (offset 6 - EOF)
1087        let filled = fs.read_directory(&root_node, 6, &mut buf).expect("failed to read dir");
1088        assert_eq!(filled, 0);
1089
1090        // Test reading with buffer size 1 (extreme pagination)
1091        let mut buf1 = vec![DirectoryEntry::default(); 1];
1092        for i in 0..expected_names.len() {
1093            let filled = fs.read_directory(&root_node, i, &mut buf1).expect("failed to read dir");
1094            assert_eq!(filled, 1);
1095            assert_eq!(buf1[0].name, expected_names[i]);
1096        }
1097        let filled = fs
1098            .read_directory(&root_node, expected_names.len(), &mut buf1)
1099            .expect("failed to read dir");
1100        assert_eq!(filled, 0);
1101    }
1102
1103    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
1104    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
1105    #[fuchsia::test]
1106    fn test_read_directory_large_dir(path: &str) {
1107        // Note: the large directory in the golden image is only large enough to split the entries
1108        // into multiple blocks on the 512 block size golden.
1109        let runfiles = fs::read(path).expect("failed to read test file");
1110        let reader = Arc::new(VecReader::new(runfiles));
1111        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
1112        let root_node = fs.root_node();
1113
1114        let large_dir_node = fs
1115            .lookup(&root_node, "large_dir")
1116            .expect("failed to look up large_dir")
1117            .expect("large_dir not found");
1118
1119        let large_dir = match large_dir_node {
1120            Node::Directory(d) => d,
1121            _ => panic!("Expected directory node"),
1122        };
1123
1124        // Skip the first two entries, . and ..
1125        let mut entry_offset = 2;
1126        let mut buffer = vec![DirectoryEntry::default(); 16];
1127        loop {
1128            let filled = fs.read_directory(&large_dir, entry_offset, &mut buffer).unwrap();
1129            for i in 0..filled {
1130                // check the prefix
1131                assert_eq!(buffer[i].name[..12], format!("file_number_"));
1132            }
1133            if filled < buffer.len() {
1134                break;
1135            }
1136            entry_offset += filled;
1137        }
1138    }
1139
1140    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
1141    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
1142    #[fuchsia::test]
1143    fn test_filesystem_metadata(path: &str) {
1144        let runfiles = fs::read(path).expect("failed to read test file");
1145        let reader = Arc::new(VecReader::new(runfiles));
1146        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
1147
1148        assert!(fs.total_bytes() > 0);
1149        assert!(fs.total_inodes() > 0);
1150    }
1151
1152    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
1153    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
1154    #[fuchsia::test]
1155    fn test_node_metadata(path: &str) {
1156        let runfiles = fs::read(path).expect("failed to read test file");
1157        let reader = Arc::new(VecReader::new(runfiles));
1158        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
1159        let root_node = fs.root_node();
1160
1161        assert!(root_node.link_count() >= 2);
1162        assert!(root_node.mtime_ns() > 0);
1163
1164        let file1_node = fs.lookup(&root_node, "file1").unwrap().unwrap();
1165        assert_eq!(file1_node.link_count(), 1);
1166        assert!(file1_node.mtime_ns() > 0);
1167    }
1168
1169    #[test_case("/pkg/data/simple.erofs" ; "4096 block size")]
1170    #[test_case("/pkg/data/simple_512.erofs" ; "512 block size")]
1171    #[fuchsia::test]
1172    fn test_xattrs(path: &str) {
1173        let runfiles = fs::read(path).expect("failed to read test file");
1174        let reader = Arc::new(VecReader::new(runfiles));
1175        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
1176        let root_node = fs.root_node();
1177
1178        // Check file1 (has both inline and shared xattrs)
1179        let file1_node = fs.lookup(&root_node, "file1").unwrap().unwrap();
1180
1181        let xattr_names = fs.list_xattrs(&file1_node).unwrap();
1182        // Should contain user.flavor, user.security, user.shared
1183        assert!(xattr_names.contains(&b"user.flavor".to_vec()));
1184        assert!(xattr_names.contains(&b"user.security".to_vec()));
1185        assert!(xattr_names.contains(&b"user.shared".to_vec()));
1186        assert_eq!(xattr_names.len(), 3);
1187
1188        let flavor_val = fs.get_xattr(&file1_node, b"user.flavor").unwrap().unwrap();
1189        assert_eq!(flavor_val, b"vanilla");
1190
1191        let security_val = fs.get_xattr(&file1_node, b"user.security").unwrap().unwrap();
1192        assert_eq!(security_val, b"high");
1193
1194        let shared_val = fs.get_xattr(&file1_node, b"user.shared").unwrap().unwrap();
1195        assert_eq!(shared_val, b"same_value");
1196
1197        // Check photosynthesis (has only shared xattr)
1198        let photo_node = fs.lookup(&root_node, "photosynthesis").unwrap().unwrap();
1199
1200        let photo_xattrs = fs.list_xattrs(&photo_node).unwrap();
1201        assert_eq!(photo_xattrs, vec![b"user.shared".to_vec()]);
1202
1203        let photo_shared_val = fs.get_xattr(&photo_node, b"user.shared").unwrap().unwrap();
1204        assert_eq!(photo_shared_val, b"same_value");
1205
1206        // Verify that we can still read the file content of photosynthesis
1207        let file_node = match photo_node {
1208            Node::File(f) => f,
1209            _ => panic!("Expected file node"),
1210        };
1211        let size = file_node.size() as usize;
1212        let mut buf = vec![0u8; size];
1213        let bytes_read = fs.read_file_range(&file_node, 0, &mut buf).expect("failed to read");
1214        assert_eq!(bytes_read, size);
1215        assert!(size > 0);
1216
1217        // Check non-existent xattr
1218        let val = fs.get_xattr(&file1_node, b"user.non_existent").unwrap();
1219        assert_eq!(val, None);
1220    }
1221
1222    // EROFS doesn't seem to make shared xattr groups with simple xattrs like the one above (I
1223    // assume it has some heuristics for judging when it is worth the cost) so this test manually
1224    // constructs a shared xattr area to test that part of the parsing logic.
1225    #[fuchsia::test]
1226    fn test_shared_xattr_parsing() {
1227        let block_size = 4096usize;
1228        let mut buf = vec![0u8; 3 * block_size];
1229
1230        // Superblock at offset 1024
1231        let sb = format::SuperBlock {
1232            magic: LEU32::new(format::EROFS_MAGIC),
1233            checksum: LEU32::new(0),
1234            feature_compat: LEU32::new(0),
1235            block_size_bits: 12,
1236            sb_ext_slots: 0,
1237            root_nid: LEU16::new(0),
1238            inode_count: LEU64::new(1),
1239            epoch: LEU64::new(0),
1240            fixed_nsec: LEU32::new(0),
1241            blocks: LEU32::new(3),
1242            meta_block_addr: LEU32::new(1),
1243            xattr_block_addr: LEU32::new(0),
1244            uuid: [0; 16],
1245            volume_name: [0; 16],
1246            feature_incompat: LEU32::new(0),
1247            available_compr_algs: LEU16::new(0),
1248            extra_devices: LEU32::new(0),
1249            dirblkbits: 0,
1250            reserved: [0; 37],
1251        };
1252        buf[1024..1024 + 128].copy_from_slice(sb.as_bytes());
1253
1254        // Compact Inode at meta_block_addr (block 1, offset 4096)
1255        let inode = format::InodeCompact {
1256            format: LEU16::new(0),
1257            xattr_icount: LEU16::new(2),
1258            mode: LEU16::new(0o040755),
1259            link_count: LEU16::new(1),
1260            size: LEU32::new(0),
1261            reserved_1: [0; 4],
1262            i_u: [0; 4],
1263            ino: LEU32::new(0),
1264            uid: LEU16::new(0),
1265            gid: LEU16::new(0),
1266            reserved_2: [0; 4],
1267        };
1268        buf[4096..4096 + 32].copy_from_slice(inode.as_bytes());
1269
1270        // XattrInlineBodyHeader at offset 4128
1271        let header = format::XattrInlineBodyHeader {
1272            name_filter: LEU32::new(0),
1273            shared_count: 1,
1274            reserved: [0; 7],
1275        };
1276        buf[4128..4128 + 12].copy_from_slice(header.as_bytes());
1277        // shared_id index 512 (512 * 4 = offset 2048 in block 0) at offset 4140
1278        buf[4140..4144].copy_from_slice(&512u32.to_le_bytes());
1279
1280        // Shared Xattr Entry at xattr_block_addr (block 0, offset 2048)
1281        let xentry = format::XattrEntry {
1282            name_len: 6,
1283            name_index: 1, // "user."
1284            value_size: LEU16::new(10),
1285        };
1286        buf[2048..2052].copy_from_slice(xentry.as_bytes());
1287        buf[2052..2058].copy_from_slice(b"shared");
1288        buf[2058..2068].copy_from_slice(b"same_value");
1289
1290        let reader = Arc::new(VecReader::new(buf));
1291        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
1292        let root_node = fs.root_node();
1293
1294        let xattr_names = fs.list_xattrs(&root_node).expect("failed to list xattrs");
1295        assert_eq!(xattr_names, vec![b"user.shared".to_vec()]);
1296
1297        let shared_val =
1298            fs.get_xattr(&root_node, b"user.shared").expect("failed to get xattr").unwrap();
1299        assert_eq!(shared_val, b"same_value");
1300    }
1301
1302    #[fuchsia::test]
1303    fn test_xattr_iterator_fusing_on_error() {
1304        let block_size = 4096usize;
1305        let mut buf = vec![0u8; 3 * block_size];
1306
1307        let sb = format::SuperBlock {
1308            magic: LEU32::new(format::EROFS_MAGIC),
1309            checksum: LEU32::new(0),
1310            feature_compat: LEU32::new(0),
1311            block_size_bits: 12,
1312            sb_ext_slots: 0,
1313            root_nid: LEU16::new(0),
1314            inode_count: LEU64::new(1),
1315            epoch: LEU64::new(0),
1316            fixed_nsec: LEU32::new(0),
1317            blocks: LEU32::new(3),
1318            meta_block_addr: LEU32::new(1),
1319            xattr_block_addr: LEU32::new(2),
1320            uuid: [0; 16],
1321            volume_name: [0; 16],
1322            feature_incompat: LEU32::new(0),
1323            available_compr_algs: LEU16::new(0),
1324            extra_devices: LEU32::new(0),
1325            dirblkbits: 0,
1326            reserved: [0; 37],
1327        };
1328        buf[1024..1024 + 128].copy_from_slice(sb.as_bytes());
1329
1330        let inode = format::InodeCompact {
1331            format: LEU16::new(0),
1332            xattr_icount: LEU16::new(3),
1333            mode: LEU16::new(0o040755),
1334            link_count: LEU16::new(1),
1335            size: LEU32::new(0),
1336            reserved_1: [0; 4],
1337            i_u: [0; 4],
1338            ino: LEU32::new(0),
1339            uid: LEU16::new(0),
1340            gid: LEU16::new(0),
1341            reserved_2: [0; 4],
1342        };
1343        buf[4096..4096 + 32].copy_from_slice(inode.as_bytes());
1344
1345        let header = format::XattrInlineBodyHeader {
1346            name_filter: LEU32::new(0),
1347            shared_count: 2,
1348            reserved: [0; 7],
1349        };
1350        buf[4128..4128 + 12].copy_from_slice(header.as_bytes());
1351        buf[4140..4144].copy_from_slice(&0u32.to_le_bytes());
1352        buf[4144..4148].copy_from_slice(&1u32.to_le_bytes());
1353
1354        // Invalid shared xattr entry (invalid name_index 99) at xattr_block_addr for shared_id 0
1355        // (offset 8192)
1356        let invalid_xentry =
1357            format::XattrEntry { name_len: 6, name_index: 99, value_size: LEU16::new(10) };
1358        buf[8192..8196].copy_from_slice(invalid_xentry.as_bytes());
1359
1360        let reader = Arc::new(VecReader::new(buf));
1361        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
1362        let root_node = fs.root_node();
1363
1364        let mut iter = fs.iter_xattrs(&root_node).expect("failed to create iterator");
1365        assert!(iter.next().unwrap().is_err());
1366        // Iterator MUST be fused now: subsequent next() calls MUST return None
1367        assert!(iter.next().is_none());
1368    }
1369}