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
28bitflags! {
29    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
30    pub struct FeatureIncompat: u32 {
31        /// If this feature is set, compressed data is right-aligned and the beginning is padded
32        /// with zeros, which the decompression logic needs to trim to find the real data. This is
33        /// done to support a memory optimization when decompressing in linux.
34        const ZERO_PADDING = 0x00000001;
35    }
36}
37
38bitflags! {
39    /// Flags for various compression behaviors, stored per-inode in the compression header when
40    /// the CompressedFull or CompressedCompact data layout are used.
41    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
42    pub struct CompressionAdvise: u16 {
43        /// There are two possible entry table layouts when using the CompressedCompact data
44        /// layout. This indicates that we should expect the even more compact one.
45        const COMPACTED_2B = 0x0001;
46    }
47}
48
49/// The bit width of the low value field in compact cluster index entries (clusterofs / delta0).
50/// This is fixed at 12 bits for supported block sizes (512B to 4KB).
51pub const COMPACT_ENTRY_LOBITS: u32 = 12;
52
53/// Errors that can occur while interacting with an EROFS image.
54#[derive(Debug, Error, Clone, PartialEq)]
55pub enum ErofsError {
56    #[error("Unsupported feature incompat flags: 0x{:X}. Only 0x{:X} is supported", _0, _1)]
57    UnsupportedFeatureIncompat(u32, u32),
58
59    #[error("Parsing error: {}", _0)]
60    Parse(#[from] ParsingError),
61    #[error("Reader error: {}", _0)]
62    ReadError(#[from] ReaderError),
63}
64
65#[cfg(target_os = "fuchsia")]
66impl ErofsError {
67    pub fn to_status(self) -> zx::Status {
68        match self {
69            Self::UnsupportedFeatureIncompat(_, _) => zx::Status::NOT_SUPPORTED,
70            Self::Parse(_) => zx::Status::IO_DATA_INTEGRITY,
71            Self::ReadError(_) => zx::Status::IO,
72        }
73    }
74}
75
76/// Errors that can occur during parsing of an EROFS image.
77#[derive(Debug, Error, Clone, PartialEq)]
78pub enum ParsingError {
79    #[error("Invalid super block magic: 0x{:X}, should be 0x{:X}", _0, format::EROFS_MAGIC)]
80    InvalidSuperBlockMagic(u32),
81    #[error("Checksum mismatch: expected 0x{:X}, computed 0x{:X}", _0, _1)]
82    ChecksumMismatch(u32, u32),
83    #[error("Invalid block size bits: {}, must be between 9 and 12", _0)]
84    InvalidBlockSizeBits(u8),
85
86    #[error("Invalid inode data layout: 0x{:X}", _0)]
87    InvalidInodeDataLayout(u16),
88    #[error("Expected compressed inode layout, found {:?}", _0)]
89    UnexpectedInodeDataLayout(InodeDataLayout),
90    #[error("Missing compression map header on compressed inode")]
91    MissingCompressionHeader,
92    #[error("Unexpected compression algorithm type: {}", _0)]
93    UnexpectedCompressionAlgorithm(u8),
94    #[error("Invalid directory entry")]
95    InvalidDirectoryEntry,
96    #[error("Invalid file type: {}", _0)]
97    InvalidFileType(u8),
98    #[error("Directory entry name was not valid utf8: {}", _0)]
99    InvalidDirectoryEntryName(#[source] std::str::Utf8Error),
100    #[error("Inline data layout missing inline data")]
101    InlineDataLayoutMissingInlineData,
102
103    #[error("Invalid root node")]
104    InvalidRootNode,
105    #[error("Node has an invalid U value for its data layout")]
106    InvalidUValue,
107    #[error("Invalid nid: {}", _0)]
108    InvalidNid(u64),
109    #[error("Integer overflow during calculation")]
110    Overflow,
111    #[error("Decompression failed: {}", _0)]
112    DecompressionFailed(#[from] lz4::Error),
113    #[error("Missing shared xattr area but inode has shared xattrs")]
114    MissingSharedXattrArea,
115    #[error("Xattr entry extends past the end of the inline xattr region")]
116    XattrEntryOutOfBounds,
117    #[error("Invalid xattr namespace index: {}", _0)]
118    InvalidXattrNamespace(u8),
119
120    #[error("Invalid logical cluster type {}", _0)]
121    InvalidLClusterType(u16),
122    #[error("Expected HEAD logical cluster at lcn {}", _0)]
123    ExpectedHeadLCluster(u64),
124    #[error(
125        "Logical cluster number {} out of bounds (total clusters: {})",
126        cluster_index,
127        total_lclusters
128    )]
129    LClusterOutOfBounds { cluster_index: u64, total_lclusters: u64 },
130    #[error("Invalid NonHead delta0 {} at cluster index {}", delta0, cluster_index)]
131    InvalidLClusterDelta { cluster_index: u64, delta0: u16 },
132    #[error("Corrupted compact cluster index pack: {}", _0)]
133    CorruptedCompactClusterPack(&'static str),
134    #[error(
135        "Compact cluster pack offset {}..{} out of bounds (pack size: {})",
136        byte_offset,
137        byte_offset + 4,
138        pack_size
139    )]
140    CompactPackOutOfBounds { byte_offset: usize, pack_size: usize },
141    #[error("Logical offset {} is before start of initial cluster {}", offset, cluster_start)]
142    InvalidClusterOffset { offset: u64, cluster_start: u64 },
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146enum InodeDataUnion {
147    DataBlkAddrPlain(u32),
148    DataBlkAddrInline(u32),
149    CompressedBlocks(u32),
150}
151
152impl InodeDataUnion {
153    fn parse(data: [u8; 4], format: InodeFormat) -> Self {
154        match format.data_layout {
155            InodeDataLayout::FlatPlain => {
156                InodeDataUnion::DataBlkAddrPlain(u32::from_le_bytes(data))
157            }
158            // Technically this is only valid for inline data where the size is more than a block.
159            InodeDataLayout::FlatInline => {
160                InodeDataUnion::DataBlkAddrInline(u32::from_le_bytes(data))
161            }
162            InodeDataLayout::CompressedFull | InodeDataLayout::CompressedCompact => {
163                InodeDataUnion::CompressedBlocks(u32::from_le_bytes(data))
164            }
165        }
166    }
167}
168
169#[derive(Debug, Clone)]
170pub struct NodeInner {
171    inode_offset: u64,
172    format: InodeFormat,
173    mode: u16,
174    size: u64,
175    data_union: InodeDataUnion,
176    ino: u32,
177    nid: u64,
178    link_count: u32,
179    uid: u32,
180    gid: u32,
181    mtime_ns: u64,
182    xattr_icount: u16,
183    compression_header: Option<CompressionHeader>,
184}
185
186impl NodeInner {
187    fn is_dir(&self) -> bool {
188        (self.mode & 0xf000) == 0x4000
189    }
190
191    fn is_symlink(&self) -> bool {
192        (self.mode & 0xf000) == 0xa000
193    }
194
195    fn inode_offset(&self) -> u64 {
196        self.inode_offset
197    }
198
199    /// Returns the data layout of this node.
200    pub fn data_layout(&self) -> InodeDataLayout {
201        self.format.data_layout
202    }
203
204    /// Interpret the u field as a block address. This is only a valid interpretation on FlatPlain,
205    /// or on FlatInline if the size is larger than a block.
206    fn blkaddr(&self, block_size: u64) -> Option<u64> {
207        match self.data_union {
208            InodeDataUnion::DataBlkAddrPlain(addr) => Some(addr.into()),
209            InodeDataUnion::DataBlkAddrInline(addr) => {
210                debug_assert!(self.size / block_size > 0);
211                Some(addr.into())
212            }
213            InodeDataUnion::CompressedBlocks(_) => None,
214        }
215    }
216
217    /// Safely calculate the on-disk offset for a read in this nodes data. This doesn't check out
218    /// of bounds errors.
219    fn blkaddr_offset(&self, block_size: u64, offset: u64) -> Result<u64, ParsingError> {
220        self.blkaddr(block_size)
221            .ok_or(ParsingError::InvalidUValue)?
222            .checked_mul(block_size)
223            .ok_or(ParsingError::Overflow)?
224            .checked_add(offset)
225            .ok_or(ParsingError::Overflow)
226    }
227
228    fn metadata_size(&self) -> u64 {
229        match self.format.version {
230            InodeVersion::Compact => 32,
231            InodeVersion::Extended => 64,
232        }
233    }
234
235    fn inline_xattr_size(&self) -> u64 {
236        if self.xattr_icount == 0 { 0 } else { ((self.xattr_icount as u64 - 1) * 4) + 12 }
237    }
238
239    /// Offset immediately following this node's metadata and inline xattrs.
240    fn metadata_end_offset(&self) -> Result<u64, ParsingError> {
241        self.inode_offset()
242            .checked_add(self.metadata_size())
243            .ok_or(ParsingError::Overflow)?
244            .checked_add(self.inline_xattr_size())
245            .ok_or(ParsingError::Overflow)
246    }
247
248    /// Offset of the compression MapHeader (8-byte aligned after metadata and inline xattrs).
249    fn map_header_offset(&self) -> Result<u64, ParsingError> {
250        let metadata_end = self.metadata_end_offset()?;
251        Ok(metadata_end.next_multiple_of(8))
252    }
253
254    /// Offset of the start of the logical cluster index table.
255    fn index_table_offset(&self) -> Result<u64, ParsingError> {
256        let map_header_offset = self.map_header_offset()?;
257        match self.format.data_layout {
258            InodeDataLayout::CompressedFull => {
259                Ok(map_header_offset + format::LEGACY_MAP_HEADER_SIZE)
260            }
261            InodeDataLayout::CompressedCompact => {
262                Ok(map_header_offset + std::mem::size_of::<format::CompressionMapHeader>() as u64)
263            }
264            _ => Err(ParsingError::UnexpectedInodeDataLayout(self.format.data_layout)),
265        }
266    }
267
268    /// Returns the compression header for this node, if it has a compressed data layout.
269    pub fn compression_header(&self) -> Option<&CompressionHeader> {
270        self.compression_header.as_ref()
271    }
272
273    /// Returns the position and layout of a compact logical cluster index entry.
274    pub fn compact_entry_pos(
275        &self,
276        block_size: u64,
277        cluster_index: u64,
278    ) -> Result<CompactEntry, ParsingError> {
279        if self.format.data_layout != InodeDataLayout::CompressedCompact {
280            return Err(ParsingError::UnexpectedInodeDataLayout(self.format.data_layout));
281        }
282        let total_clusters = self.total_lclusters(block_size);
283        if cluster_index >= total_clusters {
284            return Err(ParsingError::LClusterOutOfBounds {
285                cluster_index,
286                total_lclusters: total_clusters,
287            });
288        }
289
290        let table_offset = self.index_table_offset()?;
291        let header =
292            self.compression_header.as_ref().ok_or(ParsingError::MissingCompressionHeader)?;
293        let is_compact_2b = header.advise.contains(CompressionAdvise::COMPACTED_2B);
294
295        // Number of 4B entries needed to align to a 32-byte boundary (for 2B packs)
296        let initial_4b_count = ((32 - (table_offset % 32)) / 4) & 7;
297        let middle_2b_count = if is_compact_2b && initial_4b_count < total_clusters {
298            (total_clusters - initial_4b_count) & !15
299        } else {
300            0
301        };
302
303        let (pack_pos, layout, entry_index) = if cluster_index < initial_4b_count {
304            let pack_idx = cluster_index / 2;
305            (table_offset + pack_idx * 8, CompactPackLayout::Pack4B, (cluster_index % 2) as usize)
306        } else if cluster_index < initial_4b_count + middle_2b_count {
307            let rel_lcn = cluster_index - initial_4b_count;
308            let base_2b_offset = table_offset + initial_4b_count * 4;
309            let pack_idx = rel_lcn / 16;
310            (base_2b_offset + pack_idx * 32, CompactPackLayout::Pack2B, (rel_lcn % 16) as usize)
311        } else {
312            let rel_lcn = cluster_index - initial_4b_count - middle_2b_count;
313            let base_trailing_offset = table_offset + initial_4b_count * 4 + middle_2b_count * 2;
314            let pack_idx = rel_lcn / 2;
315            (base_trailing_offset + pack_idx * 8, CompactPackLayout::Pack4B, (rel_lcn % 2) as usize)
316        };
317
318        Ok(CompactEntry { pack_offset_bytes: pack_pos, layout, entry_index })
319    }
320
321    /// Offset immediately following the logical cluster index table. For files with inline data,
322    /// this is where the inline data is stored.
323    pub fn index_end_offset(&self, block_size: u64) -> Result<u64, ParsingError> {
324        let max_lcn = self.total_lclusters(block_size);
325        match self.format.data_layout {
326            InodeDataLayout::CompressedFull => {
327                let index_table_offset = self.index_table_offset()?;
328                index_table_offset
329                    .checked_add(max_lcn.checked_mul(8).ok_or(ParsingError::Overflow)?)
330                    .ok_or(ParsingError::Overflow)
331            }
332            InodeDataLayout::CompressedCompact => {
333                if max_lcn == 0 {
334                    return self.index_table_offset();
335                }
336                let pos = self.compact_entry_pos(block_size, max_lcn - 1)?;
337                pos.pack_offset_bytes.checked_add(pos.pack_size()).ok_or(ParsingError::Overflow)
338            }
339            _ => Err(ParsingError::UnexpectedInodeDataLayout(self.format.data_layout)),
340        }
341    }
342
343    /// Returns the total number of logical clusters for this node.
344    pub fn total_lclusters(&self, lcluster_size: u64) -> u64 {
345        self.size.div_ceil(lcluster_size)
346    }
347
348    pub fn size(&self) -> u64 {
349        self.size
350    }
351    pub fn ino(&self) -> u32 {
352        self.ino
353    }
354    pub fn nid(&self) -> u64 {
355        self.nid
356    }
357    pub fn link_count(&self) -> u32 {
358        self.link_count
359    }
360    pub fn uid(&self) -> u32 {
361        self.uid
362    }
363    pub fn gid(&self) -> u32 {
364        self.gid
365    }
366    pub fn mtime_ns(&self) -> u64 {
367        self.mtime_ns
368    }
369    pub fn mode(&self) -> u16 {
370        self.mode
371    }
372
373    /// Returns the storage size in bytes taken up by this node on disk.
374    pub fn storage_size(&self, block_size: u64) -> u64 {
375        match self.data_union {
376            InodeDataUnion::CompressedBlocks(blocks) => (blocks as u64) * block_size,
377            _ => self.size,
378        }
379    }
380}
381
382/// A directory node in the EROFS image.
383#[derive(Debug, Clone)]
384pub struct DirectoryNode(NodeInner);
385
386impl std::ops::Deref for DirectoryNode {
387    type Target = NodeInner;
388    fn deref(&self) -> &Self::Target {
389        &self.0
390    }
391}
392
393/// File type for a directory entry.
394#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
395pub enum FileType {
396    #[default]
397    Unknown = 0,
398    RegFile = 1,
399    Dir = 2,
400    ChrDev = 3,
401    BlkDev = 4,
402    Fifo = 5,
403    Sock = 6,
404    Symlink = 7,
405}
406
407impl TryFrom<u8> for FileType {
408    type Error = ParsingError;
409
410    fn try_from(value: u8) -> Result<Self, Self::Error> {
411        match value {
412            0 => Ok(FileType::Unknown),
413            1 => Ok(FileType::RegFile),
414            2 => Ok(FileType::Dir),
415            3 => Ok(FileType::ChrDev),
416            4 => Ok(FileType::BlkDev),
417            5 => Ok(FileType::Fifo),
418            6 => Ok(FileType::Sock),
419            7 => Ok(FileType::Symlink),
420            _ => Err(ParsingError::InvalidFileType(value)),
421        }
422    }
423}
424
425/// A directory entry in the EROFS image.
426#[derive(Debug, Clone, Default)]
427pub struct DirectoryEntry {
428    pub nid: u64,
429    pub file_type: FileType,
430    pub name: String,
431}
432
433/// A file node in the EROFS image.
434#[derive(Debug, Clone)]
435pub struct FileNode(NodeInner);
436
437impl std::ops::Deref for FileNode {
438    type Target = NodeInner;
439    fn deref(&self) -> &Self::Target {
440        &self.0
441    }
442}
443
444/// A symbolic link node in the EROFS image.
445#[derive(Debug, Clone)]
446pub struct SymlinkNode(NodeInner);
447
448impl std::ops::Deref for SymlinkNode {
449    type Target = NodeInner;
450    fn deref(&self) -> &Self::Target {
451        &self.0
452    }
453}
454
455/// A node in the EROFS image.
456#[derive(Debug, Clone)]
457pub enum Node {
458    Directory(DirectoryNode),
459    File(FileNode),
460    Symlink(SymlinkNode),
461}
462
463impl Node {
464    fn new(inner: NodeInner) -> Self {
465        if inner.is_dir() {
466            Node::Directory(DirectoryNode(inner))
467        } else if inner.is_symlink() {
468            Node::Symlink(SymlinkNode(inner))
469        } else {
470            Node::File(FileNode(inner))
471        }
472    }
473
474    fn parse_compact(
475        nid: u64,
476        inode_offset: u64,
477        format: InodeFormat,
478        inode: format::InodeCompact,
479        build_time_ns: u64,
480        reader: &dyn Reader,
481    ) -> Result<Self, ErofsError> {
482        let data_union = InodeDataUnion::parse(inode.i_u, format);
483        let mut inner = NodeInner {
484            inode_offset,
485            format,
486            mode: inode.mode.get(),
487            size: inode.size.get().into(),
488            data_union,
489            ino: inode.ino.get(),
490            nid,
491            link_count: inode.link_count.get().into(),
492            uid: inode.uid.get().into(),
493            gid: inode.gid.get().into(),
494            mtime_ns: build_time_ns,
495            xattr_icount: inode.xattr_icount.get(),
496            compression_header: None,
497        };
498        if matches!(
499            format.data_layout,
500            InodeDataLayout::CompressedFull | InodeDataLayout::CompressedCompact
501        ) {
502            let map_header_offset = inner.map_header_offset()?;
503            inner.compression_header = Some(CompressionHeader::read(reader, map_header_offset)?);
504        }
505        Ok(Self::new(inner))
506    }
507
508    fn parse_extended(
509        nid: u64,
510        inode_offset: u64,
511        format: InodeFormat,
512        inode: format::InodeExtended,
513        reader: &dyn Reader,
514    ) -> Result<Self, ErofsError> {
515        let data_union = InodeDataUnion::parse(inode.i_u, format);
516        let mtime_ns = inode
517            .mtime
518            .get()
519            .checked_mul(1_000_000_000)
520            .and_then(|t| t.checked_add(inode.mtime_ns.get().into()))
521            .ok_or(ParsingError::Overflow)?;
522        let mut inner = NodeInner {
523            inode_offset,
524            format,
525            mode: inode.mode.get(),
526            size: inode.size.get(),
527            data_union,
528            ino: inode.ino.get(),
529            nid,
530            link_count: inode.link_count.get(),
531            uid: inode.uid.get(),
532            gid: inode.gid.get(),
533            mtime_ns,
534            xattr_icount: inode.xattr_icount.get(),
535            compression_header: None,
536        };
537        if matches!(
538            format.data_layout,
539            InodeDataLayout::CompressedFull | InodeDataLayout::CompressedCompact
540        ) {
541            let map_header_offset = inner.map_header_offset()?;
542            inner.compression_header = Some(CompressionHeader::read(reader, map_header_offset)?);
543        }
544        Ok(Self::new(inner))
545    }
546
547    fn from_nid(
548        nid: u64,
549        meta_addr: u64,
550        build_time_ns: u64,
551        reader: &dyn Reader,
552    ) -> Result<Self, ErofsError> {
553        let node_offset =
554            nid.checked_mul(format::INODE_SLOT_SIZE).ok_or(ParsingError::InvalidNid(nid))?;
555        let inode_offset =
556            meta_addr.checked_add(node_offset).ok_or(ParsingError::InvalidNid(nid))?;
557        // Read the first 2 bytes to determine the inode format.
558        let mut head = [0u8; 2];
559        reader.read(inode_offset, &mut head)?;
560        let format = InodeFormat::parse(u16::from_le_bytes(head))?;
561        let node = match format.version {
562            InodeVersion::Compact => Self::parse_compact(
563                nid,
564                inode_offset,
565                format,
566                reader.read_object(inode_offset)?,
567                build_time_ns,
568                reader,
569            )?,
570            InodeVersion::Extended => Self::parse_extended(
571                nid,
572                inode_offset,
573                format,
574                reader.read_object(inode_offset)?,
575                reader,
576            )?,
577        };
578        Ok(node)
579    }
580}
581
582impl std::ops::Deref for Node {
583    type Target = NodeInner;
584    fn deref(&self) -> &Self::Target {
585        match self {
586            Node::Directory(d) => d,
587            Node::File(f) => f,
588            Node::Symlink(s) => s,
589        }
590    }
591}
592
593/// The representation of an extent's backing content.
594#[derive(Debug, Clone, Copy)]
595enum ExtentKind {
596    /// Sparse extents are regions of zeros without physical backing.
597    Sparse,
598    /// Plain extents are uncompressed data stored at a fixed on-disk offset.
599    Plain { byte_offset: u64 },
600    /// Compressed extents are compressed data that require decompression to read.
601    Compressed { block_addr: u32 },
602}
603
604/// A compression extent is a logical, unaligned region of a file that maps to a single physical
605/// cluster.
606#[derive(Debug, Clone, Copy)]
607struct CompressionExtent {
608    logical_start: u64,
609    logical_len: u32,
610    kind: ExtentKind,
611}
612
613/// The filesystem implementation for an EROFS image.
614pub struct ErofsFilesystem {
615    reader: Arc<dyn Reader>,
616    feature_incompat: FeatureIncompat,
617    block_size: u64,
618    meta_addr: u64,
619    xattr_addr: u64,
620    root_node: DirectoryNode,
621    total_bytes: u64,
622    total_inodes: u64,
623    build_time_ns: u64,
624}
625
626impl ErofsFilesystem {
627    /// Creates a new filesystem instance for an EROFS image from a reader.
628    pub fn new(reader: Arc<dyn Reader>) -> Result<Self, ErofsError> {
629        let (super_block, feature_incompat) = Self::parse_superblock(&reader)?;
630        let block_size = 1u64 << super_block.block_size_bits;
631        let meta_block_addr = super_block.meta_block_addr.get().into();
632        let meta_addr = block_size.checked_mul(meta_block_addr).ok_or(ParsingError::Overflow)?;
633        let total_inodes = super_block.inode_count.get();
634        let build_time_ns = super_block
635            .epoch
636            .get()
637            .checked_mul(1_000_000_000)
638            .and_then(|t| t.checked_add(super_block.fixed_nsec.get().into()))
639            .ok_or(ParsingError::Overflow)?;
640        let total_bytes = (super_block.blocks.get() as u64) * block_size;
641        let xattr_block_addr = super_block.xattr_block_addr.get().into();
642        let xattr_addr = block_size.checked_mul(xattr_block_addr).ok_or(ParsingError::Overflow)?;
643        let root_nid = super_block.root_nid.get().into();
644        let root_node = match Node::from_nid(root_nid, meta_addr, build_time_ns, &reader)? {
645            Node::Directory(node) => node,
646            _ => return Err(ParsingError::InvalidRootNode.into()),
647        };
648        Ok(Self {
649            reader,
650            feature_incompat,
651            block_size,
652            meta_addr,
653            xattr_addr,
654            root_node,
655            total_bytes,
656            total_inodes,
657            build_time_ns,
658        })
659    }
660
661    /// Returns the feature incompat flags of the EROFS image.
662    pub fn feature_incompat(&self) -> FeatureIncompat {
663        self.feature_incompat
664    }
665
666    fn parse_superblock(
667        reader: &dyn Reader,
668    ) -> Result<(format::SuperBlock, FeatureIncompat), ErofsError> {
669        let sb: format::SuperBlock = reader.read_object(format::SUPERBLOCK_OFFSET)?;
670        if sb.magic.get() != format::EROFS_MAGIC {
671            return Err(ParsingError::InvalidSuperBlockMagic(sb.magic.get()).into());
672        }
673        // The max block size that can be made by tooling is 4096 right now, and the specified
674        // minimum is 512, so make sure we are in that window.
675        if sb.block_size_bits < 9 || sb.block_size_bits > 12 {
676            return Err(ParsingError::InvalidBlockSizeBits(sb.block_size_bits).into());
677        }
678        // TODO(https://fxbug.dev/479841115): Handle more feature_compat flags.
679        let feature_compat = FeatureCompat::from_bits_truncate(sb.feature_compat.get());
680        if feature_compat.contains(FeatureCompat::SB_CHKSUM) {
681            Self::check_superblock_checksum(reader, &sb)?;
682        }
683        let incompat_raw = sb.feature_incompat.get();
684        let incompat = FeatureIncompat::from_bits(incompat_raw).ok_or(
685            ErofsError::UnsupportedFeatureIncompat(incompat_raw, FeatureIncompat::all().bits()),
686        )?;
687        Ok((sb, incompat))
688    }
689
690    fn check_superblock_checksum(
691        reader: &dyn Reader,
692        sb: &format::SuperBlock,
693    ) -> Result<(), ErofsError> {
694        let block_size = 1usize << sb.block_size_bits;
695        let len = block_size - (format::SUPERBLOCK_OFFSET as usize) % block_size;
696        let mut buf = vec![0u8; len];
697        reader.read(format::SUPERBLOCK_OFFSET, &mut buf)?;
698
699        // Zero out checksum field, which is at a well-known offset off the superblock offset.
700        buf[4..8].copy_from_slice(&[0u8; 4]);
701
702        let crc = Crc::<u32>::new(&CRC_32_ISCSI);
703        let checksum = crc.checksum(&buf);
704        // Undo final bitwise inversion applied by the crc crate, as suggested by the EROFS docs
705        // (https://erofs.docs.kernel.org/en/latest/ondisk/core_ondisk.html#superblock-checksum)
706        let checksum = !checksum;
707
708        if checksum != sb.checksum.get() {
709            Err(ParsingError::ChecksumMismatch(sb.checksum.get(), checksum).into())
710        } else {
711            Ok(())
712        }
713    }
714
715    /// Returns the block size of the EROFS image.
716    pub fn block_size(&self) -> u64 {
717        self.block_size
718    }
719
720    /// Returns the node with the given nid.
721    pub fn node(&self, nid: u64) -> Result<Node, ErofsError> {
722        Node::from_nid(nid, self.meta_addr, self.build_time_ns, &self.reader)
723    }
724
725    /// Returns the root node of the EROFS image.
726    pub fn root_node(&self) -> DirectoryNode {
727        self.root_node.clone()
728    }
729
730    pub fn total_bytes(&self) -> u64 {
731        self.total_bytes
732    }
733
734    pub fn total_inodes(&self) -> u64 {
735        self.total_inodes
736    }
737
738    /// Reads the data of the given file node into a buffer.
739    pub fn read_file_range(
740        &self,
741        node: &FileNode,
742        offset: u64,
743        buf: &mut [u8],
744    ) -> Result<usize, ErofsError> {
745        self.read_node_range(&node.0, offset, buf)
746    }
747
748    /// Reads the target path of the given symlink node.
749    pub fn read_symlink(&self, node: &SymlinkNode) -> Result<Vec<u8>, ErofsError> {
750        let mut target = vec![0u8; node.size() as usize];
751        let read_bytes = self.read_node_range(&node.0, 0, &mut target)?;
752        target.truncate(read_bytes);
753        Ok(target)
754    }
755
756    /// Read bytes from the node's data at an offset. The length of the read is determined by the
757    /// length of the provided output buf. The data is written into that buf. Returns the number of
758    /// bytes read.
759    ///
760    /// TODO(https://fxbug.dev/479841115): This is a traditional unix-y way of handling reads -
761    /// potentially reading less data than asked for - but we should determine whether that fits
762    /// our apis and tweak it if needed.
763    fn read_node_range(
764        &self,
765        node: &NodeInner,
766        offset: u64,
767        buf: &mut [u8],
768    ) -> Result<usize, ErofsError> {
769        if offset >= node.size {
770            return Ok(0);
771        }
772        let read_len = std::cmp::min(buf.len() as u64, node.size - offset) as usize;
773        let buf = &mut buf[..read_len];
774        let block_size = self.block_size();
775
776        match node.format.data_layout {
777            InodeDataLayout::FlatPlain => {
778                let read_offset = node.blkaddr_offset(block_size, offset)?;
779                self.reader.read(read_offset, buf)?;
780                Ok(read_len)
781            }
782            InodeDataLayout::FlatInline => {
783                // A node will _only_ have the flat inline layout if it has a tail that that fits
784                // inline after the inode, so we can assume any tail data is there.
785                let full_blocks_len = (node.size / block_size) * block_size;
786                let mut bytes_read = 0;
787
788                if offset < full_blocks_len {
789                    // If there are no full blocks and the full file is in the tail section, this
790                    // check will never be true, so this is a valid use of the u value.
791                    let current_read_len =
792                        std::cmp::min(read_len as u64, full_blocks_len - offset) as usize;
793                    let read_offset = node.blkaddr_offset(block_size, offset)?;
794                    self.reader.read(read_offset, &mut buf[..current_read_len])?;
795                    bytes_read += current_read_len;
796                }
797
798                if bytes_read < read_len {
799                    let remaining_len = read_len - bytes_read;
800                    let current_offset = offset + bytes_read as u64;
801                    let inline_data_offset = node.metadata_end_offset()?;
802                    let tail_offset = current_offset - full_blocks_len;
803                    let tail_read_offset = inline_data_offset
804                        .checked_add(tail_offset)
805                        .ok_or(ParsingError::Overflow)?;
806                    self.reader.read(tail_read_offset, &mut buf[bytes_read..])?;
807                    bytes_read += remaining_len;
808                }
809
810                Ok(bytes_read)
811            }
812            InodeDataLayout::CompressedFull | InodeDataLayout::CompressedCompact => {
813                self.read_compressed_range(node, offset, buf)
814            }
815        }
816    }
817
818    fn read_lcluster_entry(
819        &self,
820        node: &NodeInner,
821        cluster_index: u64,
822    ) -> Result<LClusterEntry, ErofsError> {
823        match node.format.data_layout {
824            InodeDataLayout::CompressedFull => {
825                let index_table_offset = node.index_table_offset()?;
826                let entry_offset = index_table_offset + cluster_index * 8;
827                LClusterEntry::read_full_entry(self.reader.as_ref(), entry_offset)
828            }
829            InodeDataLayout::CompressedCompact => {
830                self.read_compact_lcluster_entry(node, cluster_index)
831            }
832            _ => Err(ParsingError::UnexpectedInodeDataLayout(node.format.data_layout).into()),
833        }
834    }
835
836    fn read_compact_lcluster_entry(
837        &self,
838        node: &NodeInner,
839        cluster_index: u64,
840    ) -> Result<LClusterEntry, ErofsError> {
841        let pos = node.compact_entry_pos(self.block_size(), cluster_index)?;
842        let pack = CompactPack::read(self.reader.as_ref(), &pos)?;
843        let entry = pack.entry(pos.entry_index)?;
844
845        if entry.cluster_type() == LClusterType::NonHead {
846            let delta0 = if pos.entry_index + 1 != pos.entry_count() {
847                entry.delta0()
848            } else {
849                if pos.entry_index == 0 {
850                    return Err(ParsingError::CorruptedCompactClusterPack(
851                        "missing preceding entry for delta0 inference",
852                    )
853                    .into());
854                }
855                let prev_entry = pack.entry(pos.entry_index - 1)?;
856                if prev_entry.cluster_type() != LClusterType::NonHead {
857                    1
858                } else {
859                    prev_entry.delta0() + 1
860                }
861            };
862
863            Ok(LClusterEntry::NonHead { delta0 })
864        } else {
865            let base_pblk = pack.base_pblk()?;
866
867            let mut entry_i = pos.entry_index;
868            let mut nblk = 1u32;
869
870            while entry_i > 0 {
871                entry_i -= 1;
872                let prev_entry = pack.entry(entry_i)?;
873                if prev_entry.cluster_type() == LClusterType::NonHead {
874                    let step = prev_entry.delta0() as usize;
875                    if entry_i >= step {
876                        entry_i -= step;
877                        nblk += 1;
878                    } else {
879                        break;
880                    }
881                } else {
882                    nblk += 1;
883                }
884            }
885
886            let extent_start_offset = entry.extent_start_offset();
887            let block_addr = if base_pblk == u32::MAX { 0 } else { base_pblk + nblk };
888            Ok(LClusterEntry::Head(LClusterHead {
889                cluster_type: entry.cluster_type(),
890                block_addr,
891                extent_start_offset,
892            }))
893        }
894    }
895
896    /// Resolves the HEAD logical cluster for the extent that contains the given logical cluster
897    /// and returns its logical cluster index and head entry.
898    fn resolve_head_lcluster(
899        &self,
900        node: &NodeInner,
901        cluster_index: u64,
902    ) -> Result<(u64, LClusterHead), ErofsError> {
903        let entry = self.read_lcluster_entry(node, cluster_index)?;
904        match entry {
905            LClusterEntry::Head(head) => Ok((cluster_index, head)),
906            LClusterEntry::NonHead { delta0 } => {
907                let head_cluster_index = cluster_index
908                    .checked_sub(delta0 as u64)
909                    .ok_or(ParsingError::InvalidLClusterDelta { cluster_index, delta0 })?;
910                let head_entry = self.read_lcluster_entry(node, head_cluster_index)?;
911                match head_entry {
912                    LClusterEntry::Head(head) => Ok((head_cluster_index, head)),
913                    LClusterEntry::NonHead { .. } => {
914                        Err(ParsingError::ExpectedHeadLCluster(head_cluster_index).into())
915                    }
916                }
917            }
918        }
919    }
920
921    /// Finds the HEAD logical cluster containing the given `logical_offset`, stepping back by one
922    /// cluster if `logical_offset` is before the cluster's offset. Returns the head cluster index,
923    /// the head entry, and its logical start byte offset.
924    fn find_head_cluster(
925        &self,
926        node: &NodeInner,
927        logical_offset: u64,
928    ) -> Result<(u64, LClusterHead, u64), ErofsError> {
929        let lcluster_size = self.block_size();
930        let cluster_index = logical_offset / lcluster_size;
931
932        let (mut head_cluster_index, mut head) = self.resolve_head_lcluster(node, cluster_index)?;
933        let mut logical_start =
934            head.logical_start(head_cluster_index, lcluster_size).ok_or(ParsingError::Overflow)?;
935
936        if logical_offset < logical_start {
937            let prev_cluster_index =
938                head_cluster_index.checked_sub(1).ok_or(ParsingError::InvalidClusterOffset {
939                    offset: logical_offset,
940                    cluster_start: logical_start,
941                })?;
942            (head_cluster_index, head) = self.resolve_head_lcluster(node, prev_cluster_index)?;
943            logical_start = head
944                .logical_start(head_cluster_index, lcluster_size)
945                .ok_or(ParsingError::Overflow)?;
946        }
947
948        Ok((head_cluster_index, head, logical_start))
949    }
950
951    /// Maps a logical offset in a compressed file to the logical compression extent that contains
952    /// it. This extent contains the metadata for this section of compressed data, and how to
953    /// decompress it.
954    fn get_extent_at(
955        &self,
956        node: &NodeInner,
957        logical_offset: u64,
958    ) -> Result<CompressionExtent, ErofsError> {
959        let block_size = self.block_size();
960        let lcluster_size = block_size;
961
962        let (head_cluster_index, head, logical_start) =
963            self.find_head_cluster(node, logical_offset)?;
964
965        let mut logical_end = node.size;
966        let max_cluster_index = node.total_lclusters(lcluster_size);
967        for next_cluster_index in (head_cluster_index + 1)..max_cluster_index {
968            let next_entry = self.read_lcluster_entry(node, next_cluster_index)?;
969            if let LClusterEntry::Head(next_head) = next_entry {
970                if let Some(next_start) = next_head.logical_start(next_cluster_index, lcluster_size)
971                {
972                    logical_end = next_start;
973                    break;
974                }
975            }
976        }
977
978        let logical_len =
979            logical_end.checked_sub(logical_start).ok_or(ParsingError::Overflow)? as u32;
980
981        let kind = if head.block_addr == 0 {
982            ExtentKind::Sparse
983        } else if head.is_plain() {
984            ExtentKind::Plain { byte_offset: head.block_addr as u64 * block_size }
985        } else {
986            ExtentKind::Compressed { block_addr: head.block_addr }
987        };
988
989        Ok(CompressionExtent { logical_start, logical_len, kind })
990    }
991
992    /// Reads a range of bytes from a compressed file node, decompressing as needed. This method is
993    /// intended to be called by read_node_range. Use that for general reading to handle all
994    /// possible data layouts.
995    fn read_compressed_range(
996        &self,
997        node: &NodeInner,
998        mut offset: u64,
999        mut buf: &mut [u8],
1000    ) -> Result<usize, ErofsError> {
1001        // This value is checked and tweaked as needed by read_node_range.
1002        let read_len = buf.len();
1003        let block_size = self.block_size();
1004
1005        while !buf.is_empty() {
1006            let extent = self.get_extent_at(node, offset)?;
1007            let offset_in_cluster = (offset - extent.logical_start) as usize;
1008            let available = extent.logical_len as usize - offset_in_cluster;
1009            let copy_len = std::cmp::min(buf.len(), available);
1010            let (head, tail) = buf.split_at_mut(copy_len);
1011
1012            match &extent.kind {
1013                ExtentKind::Sparse => {
1014                    head.fill(0);
1015                }
1016                ExtentKind::Plain { byte_offset: disk_offset } => {
1017                    self.reader.read(*disk_offset + offset_in_cluster as u64, head)?;
1018                }
1019                ExtentKind::Compressed { block_addr } => {
1020                    let mut compressed_buf = vec![0u8; block_size as usize];
1021                    self.reader.read(*block_addr as u64 * block_size, &mut compressed_buf)?;
1022                    let margin = if self.feature_incompat.contains(FeatureIncompat::ZERO_PADDING) {
1023                        compressed_buf.iter().position(|&b| b != 0).unwrap_or(0)
1024                    } else {
1025                        0
1026                    };
1027                    let compressed_data = &compressed_buf[margin..];
1028
1029                    if offset_in_cluster == 0 && copy_len == extent.logical_len as usize {
1030                        lz4::decompress_into(compressed_data, head)
1031                            .map_err(|e| ParsingError::DecompressionFailed(e))?;
1032                    } else {
1033                        let mut decompressed_buf = vec![0u8; extent.logical_len as usize];
1034                        lz4::decompress_into(compressed_data, &mut decompressed_buf)
1035                            .map_err(|e| ParsingError::DecompressionFailed(e))?;
1036                        head.copy_from_slice(
1037                            &decompressed_buf[offset_in_cluster..offset_in_cluster + copy_len],
1038                        );
1039                    }
1040                }
1041            }
1042
1043            buf = tail;
1044            offset += copy_len as u64;
1045        }
1046
1047        Ok(read_len)
1048    }
1049
1050    /// Read a number of entries from a directory, starting at entry_offset. Will retrieve up to
1051    /// the number of entries in the directory or the size of the provided buffer, returning the
1052    /// number of entries filled in the buffer. If there are less filled entries then the number of
1053    /// entry slots provided in the buffer, there are no more entries in this directory. Entries
1054    /// are sorted lexicographically. Reads past the end of the number of entries will return zero
1055    /// entries filled.
1056    ///
1057    /// TODO(https://fxbug.dev/479841115): It is possible for directories to omit their "." entries
1058    /// in erofs, and in that case there is a flag marking it and we are expected to synthesize it.
1059    /// Parse that flag and implement it.
1060    /// TODO(https://fxbug.dev/479841115): This API is slightly awkward to hold. We should consider
1061    /// making it an iterator interface.
1062    pub fn read_directory(
1063        &self,
1064        node: &DirectoryNode,
1065        mut entry_offset: usize,
1066        entries: &mut [DirectoryEntry],
1067    ) -> Result<usize, ErofsError> {
1068        let block_size = self.block_size();
1069        let block_size_usize: usize = block_size as usize;
1070        let mut entries_filled = 0;
1071        let mut current_entry_index = 0;
1072        let mut block_data = vec![0u8; block_size_usize];
1073
1074        for block in 0.. {
1075            let base_offset = block * block_size;
1076            let bytes_read = self.read_node_range(&node.0, base_offset, &mut block_data)?;
1077            if bytes_read < format::DIRENT_SIZE {
1078                // We must be done if there wasn't enough data left for another dirent.
1079                return Ok(entries_filled);
1080            }
1081            block_data[bytes_read..].fill(0);
1082
1083            // Get the first dirent in the block to calculate the number of entries.
1084            let (dirent0, _) = zerocopy::Ref::<&[u8], format::Dirent>::from_prefix(&block_data)
1085                .map_err(|_| ParsingError::InvalidDirectoryEntry)?;
1086            let nameoff0 = dirent0.nameoff.get() as usize;
1087            if nameoff0 < format::DIRENT_SIZE || nameoff0 >= block_size_usize {
1088                return Err(ParsingError::InvalidDirectoryEntry.into());
1089            }
1090            let entry_count = nameoff0 / format::DIRENT_SIZE;
1091
1092            // Check if the offset we want is even in this block.
1093            if current_entry_index + entry_count <= entry_offset {
1094                current_entry_index += entry_count;
1095                continue;
1096            }
1097
1098            // Get all the dirents and make sure the nameoffs won't cause out of bounds errors.
1099            let dirents_raw = block_data
1100                .get(..entry_count * format::DIRENT_SIZE)
1101                .ok_or(ParsingError::InvalidDirectoryEntry)?;
1102            let dirents: &[format::Dirent] =
1103                &*zerocopy::Ref::<&[u8], [format::Dirent]>::from_bytes(dirents_raw)
1104                    .map_err(|_| ParsingError::InvalidDirectoryEntry)?;
1105
1106            let block_entry_offset = entry_offset - current_entry_index;
1107            let space = entries.len() - entries_filled;
1108            let block_entry_end = std::cmp::min(
1109                entry_count,
1110                block_entry_offset.checked_add(space).ok_or(ParsingError::Overflow)?,
1111            );
1112
1113            for i in block_entry_offset..block_entry_end {
1114                let last_entry = i + 1 == entry_count;
1115                let nameoff = dirents[i].nameoff.get() as usize;
1116
1117                let name_bytes = if last_entry {
1118                    // For the last entry, it ends at the end of the block or is null-terminated.
1119                    // Since block_data is padded with nulls, we can just split by 0.
1120                    let name_data =
1121                        block_data.get(nameoff..).ok_or(ParsingError::InvalidDirectoryEntry)?;
1122                    name_data.split(|&x| x == 0).next().unwrap()
1123                } else {
1124                    let nameoff_next = dirents[i + 1].nameoff.get() as usize;
1125                    block_data
1126                        .get(nameoff..nameoff_next)
1127                        .ok_or(ParsingError::InvalidDirectoryEntry)?
1128                };
1129
1130                let name = std::str::from_utf8(name_bytes)
1131                    .map_err(|e| ParsingError::InvalidDirectoryEntryName(e))?
1132                    .to_string();
1133                entries[entries_filled] = DirectoryEntry {
1134                    nid: dirents[i].nid.get(),
1135                    file_type: dirents[i].file_type.try_into()?,
1136                    name,
1137                };
1138                entries_filled += 1;
1139                if entries_filled == entries.len() {
1140                    return Ok(entries_filled);
1141                }
1142            }
1143
1144            current_entry_index =
1145                current_entry_index.checked_add(entry_count).ok_or(ParsingError::Overflow)?;
1146            entry_offset = current_entry_index;
1147        }
1148
1149        Ok(entries_filled)
1150    }
1151
1152    /// Looks up a node by name in a directory.
1153    pub fn lookup(&self, dir: &DirectoryNode, name: &str) -> Result<Option<Node>, ErofsError> {
1154        let mut entry_offset = 0;
1155        let mut buffer = vec![DirectoryEntry::default(); 16];
1156
1157        loop {
1158            let filled = self.read_directory(dir, entry_offset, &mut buffer)?;
1159            for i in 0..filled {
1160                if buffer[i].name == name {
1161                    let node = self.node(buffer[i].nid)?;
1162                    return Ok(Some(node));
1163                }
1164            }
1165            if filled < buffer.len() {
1166                break;
1167            }
1168            entry_offset += filled;
1169        }
1170
1171        Ok(None)
1172    }
1173
1174    /// Returns an iterator over the xattr entry headers for a node.
1175    pub fn iter_xattrs<'a>(&'a self, node: &NodeInner) -> Result<XattrIterator<'a>, ErofsError> {
1176        if node.xattr_icount == 0 {
1177            return Ok(XattrIterator {
1178                reader: self.reader.as_ref(),
1179                xattr_addr: self.xattr_addr,
1180                shared_ids: Vec::new(),
1181                inline_offset: 0,
1182                inline_end: 0,
1183            });
1184        }
1185        let xattr_metadata_size = node.inline_xattr_size();
1186        let xattr_metadata_start =
1187            node.inode_offset().checked_add(node.metadata_size()).ok_or(ParsingError::Overflow)?;
1188
1189        // Read the inline xattr header to get the details on the extended attributes for this node
1190        let header: format::XattrInlineBodyHeader =
1191            self.reader.read_object(xattr_metadata_start)?;
1192        let shared_count = header.shared_count as usize;
1193
1194        let shared_ids_size = shared_count as u64 * 4;
1195        let inline_entries_start = xattr_metadata_start + 12 + shared_ids_size;
1196        let inline_end = xattr_metadata_start + xattr_metadata_size;
1197
1198        if inline_entries_start > inline_end {
1199            return Err(ParsingError::XattrEntryOutOfBounds.into());
1200        }
1201
1202        let shared_ids = if shared_count > 0 {
1203            let mut ids = vec![LEU32::ZERO; shared_count];
1204            self.reader.read(xattr_metadata_start + 12, ids.as_mut_bytes())?;
1205            ids
1206        } else {
1207            Vec::new()
1208        };
1209
1210        Ok(XattrIterator {
1211            reader: self.reader.as_ref(),
1212            xattr_addr: self.xattr_addr,
1213            shared_ids,
1214            inline_offset: inline_entries_start,
1215            inline_end,
1216        })
1217    }
1218
1219    /// List all xattr names for a given node.
1220    pub fn list_xattrs(&self, node: &NodeInner) -> Result<Vec<Vec<u8>>, ErofsError> {
1221        let mut names = Vec::new();
1222        for entry in self.iter_xattrs(node)? {
1223            let entry = entry?;
1224            names.push(entry.read_name(self.reader.as_ref())?);
1225        }
1226        Ok(names)
1227    }
1228
1229    /// Get the value of a specific xattr for a given node.
1230    pub fn get_xattr(&self, node: &NodeInner, name: &[u8]) -> Result<Option<Vec<u8>>, ErofsError> {
1231        for entry in self.iter_xattrs(node)? {
1232            let entry = entry?;
1233            if entry.matches_name(self.reader.as_ref(), name)? {
1234                return Ok(Some(entry.read_value(self.reader.as_ref())?));
1235            }
1236        }
1237        Ok(None)
1238    }
1239}
1240
1241/// An iterator over xattr entry headers for an inode.
1242pub struct XattrIterator<'a> {
1243    reader: &'a dyn Reader,
1244    xattr_addr: u64,
1245    shared_ids: Vec<LEU32>,
1246    inline_offset: u64,
1247    inline_end: u64,
1248}
1249
1250impl XattrIterator<'_> {
1251    fn next_inner(&mut self) -> Result<Option<XattrEntryHeader>, ErofsError> {
1252        if let Some(shared_id) = self.shared_ids.pop() {
1253            let shared_entry_offset = self.xattr_addr + (shared_id.get() as u64 * 4);
1254            if self.shared_ids.is_empty() {
1255                self.shared_ids = Vec::new();
1256            }
1257            return Ok(Some(XattrEntryHeader::parse(self.reader, shared_entry_offset)?));
1258        }
1259
1260        if self.inline_offset < self.inline_end {
1261            if self.inline_offset + 4 > self.inline_end {
1262                return Err(ParsingError::XattrEntryOutOfBounds.into());
1263            }
1264
1265            let header = XattrEntryHeader::parse(self.reader, self.inline_offset)?;
1266            let next_offset = self
1267                .inline_offset
1268                .checked_add(header.entry_aligned_size)
1269                .ok_or(ParsingError::Overflow)?;
1270            if next_offset > self.inline_end {
1271                return Err(ParsingError::XattrEntryOutOfBounds.into());
1272            }
1273            self.inline_offset = next_offset;
1274            Ok(Some(header))
1275        } else {
1276            Ok(None)
1277        }
1278    }
1279}
1280
1281impl Iterator for XattrIterator<'_> {
1282    type Item = Result<XattrEntryHeader, ErofsError>;
1283
1284    fn next(&mut self) -> Option<Self::Item> {
1285        match self.next_inner() {
1286            // Throw out the rest of the values if we encounter an error parsing the extended
1287            // attributes. Since most of the errors are related to overflows and math issues, there
1288            // is no safe way to recover for future attributes as the locations on disk are all
1289            // relative to each other.
1290            Err(e) => {
1291                self.shared_ids = Vec::new();
1292                self.inline_offset = self.inline_end;
1293                Some(Err(e))
1294            }
1295            Ok(None) => None,
1296            Ok(Some(x)) => Some(Ok(x)),
1297        }
1298    }
1299}
1300
1301/// A parsed representation of an EROFS xattr entry record header.
1302#[derive(Debug, Clone, Copy)]
1303pub struct XattrEntryHeader {
1304    pub offset: u64,
1305    pub prefix: &'static [u8],
1306    pub name_index: u8,
1307    pub name_len: usize,
1308    pub value_size: usize,
1309    pub entry_aligned_size: u64,
1310}
1311
1312impl XattrEntryHeader {
1313    /// Read and validate an xattr entry record header from the reader.
1314    pub fn parse(reader: &dyn Reader, offset: u64) -> Result<Self, ErofsError> {
1315        let entry: format::XattrEntry = reader.read_object(offset)?;
1316        let prefix = Self::get_xattr_prefix(entry.name_index)?;
1317        let name_len = entry.name_len as usize;
1318        let value_size = entry.value_size.get() as usize;
1319
1320        let entry_aligned_size = 4usize
1321            .checked_add(name_len)
1322            .and_then(|s| s.checked_add(value_size))
1323            .and_then(|s| s.checked_next_multiple_of(4))
1324            .ok_or(ParsingError::Overflow)? as u64;
1325
1326        Ok(Self {
1327            offset,
1328            prefix,
1329            name_index: entry.name_index,
1330            name_len,
1331            value_size,
1332            entry_aligned_size,
1333        })
1334    }
1335
1336    /// Check if this xattr entry matches the given full attribute name (prefix + suffix).
1337    pub fn matches_name(&self, reader: &dyn Reader, name: &[u8]) -> Result<bool, ReaderError> {
1338        let Some(suffix) = name.strip_prefix(self.prefix) else {
1339            return Ok(false);
1340        };
1341        if suffix.len() != self.name_len {
1342            return Ok(false);
1343        }
1344        if self.name_len == 0 {
1345            // Implies suffix.len() is also zero because of the previous check.
1346            return Ok(true);
1347        }
1348        let mut buf = vec![0u8; self.name_len];
1349        reader.read(self.offset + 4, &mut buf)?;
1350        Ok(buf == suffix)
1351    }
1352
1353    /// Read the name of this xattr entry (prefix + suffix).
1354    pub fn read_name(&self, reader: &dyn Reader) -> Result<Vec<u8>, ReaderError> {
1355        let mut name_bytes = Vec::with_capacity(self.prefix.len() + self.name_len);
1356        name_bytes.extend_from_slice(self.prefix);
1357        if self.name_len > 0 {
1358            name_bytes.resize(self.prefix.len() + self.name_len, 0);
1359            reader.read(self.offset + 4, &mut name_bytes[self.prefix.len()..])?;
1360        }
1361        Ok(name_bytes)
1362    }
1363
1364    /// Read the value payload for this entry.
1365    pub fn read_value(&self, reader: &dyn Reader) -> Result<Vec<u8>, ReaderError> {
1366        let mut value_bytes = vec![0u8; self.value_size];
1367        reader.read(self.offset + 4 + self.name_len as u64, &mut value_bytes)?;
1368        Ok(value_bytes)
1369    }
1370
1371    /// Read both key name and value payload for this entry.
1372    pub fn read_payload(&self, reader: &dyn Reader) -> Result<(Vec<u8>, Vec<u8>), ReaderError> {
1373        let name = self.read_name(reader)?;
1374        let value = self.read_value(reader)?;
1375        Ok((name, value))
1376    }
1377
1378    fn get_xattr_prefix(index: u8) -> Result<&'static [u8], ParsingError> {
1379        match index {
1380            1 => Ok(b"user."),
1381            2 => Ok(b"system.posix_acl_access"),
1382            3 => Ok(b"system.posix_acl_default"),
1383            4 => Ok(b"trusted."),
1384            6 => Ok(b"security."),
1385            _ => Err(ParsingError::InvalidXattrNamespace(index)),
1386        }
1387    }
1388}
1389
1390/// The version of the on-disk format of the inode. Can be either 32-byte compact or 64-byte
1391/// extended.
1392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1393pub enum InodeVersion {
1394    Compact,
1395    Extended,
1396}
1397
1398/// The layout of the data portion of the inode.
1399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1400pub enum InodeDataLayout {
1401    /// The data union is interpreted as a block address. The data for this inode is stored in
1402    /// consecutive blocks starting from that block address.
1403    FlatPlain,
1404    /// Compressed inode with non-compact indexes. This is a legacy metadata layout, by default
1405    /// erofs images use CompressedCompact for compressed nodes.
1406    CompressedFull,
1407    /// The data union is interpreted as a block address. The data for this inode is stored in
1408    /// consecutive blocks starting from that block address, except for the tail of the data which
1409    /// is stored immediately following this metadata. If the whole tail is inlined, the data union
1410    /// is unused and doesn't matter. For this to be used, the data _must_ have a tail section that
1411    /// fits within the current metadata block.
1412    FlatInline,
1413    /// Compressed inode with compact indexes.
1414    CompressedCompact,
1415}
1416
1417/// The format of the inode, containing the version and data layout.
1418#[derive(Debug, Clone, Copy)]
1419pub struct InodeFormat {
1420    pub version: InodeVersion,
1421    pub data_layout: InodeDataLayout,
1422}
1423
1424impl InodeFormat {
1425    /// Parse the inode format from the given format value.
1426    pub fn parse(format: u16) -> Result<Self, ParsingError> {
1427        let version =
1428            if format & 0x1 == 0 { InodeVersion::Compact } else { InodeVersion::Extended };
1429        let data_layout_raw = (format >> 1) & 0x7;
1430        let data_layout = match data_layout_raw {
1431            0 => InodeDataLayout::FlatPlain,
1432            1 => InodeDataLayout::CompressedFull,
1433            2 => InodeDataLayout::FlatInline,
1434            3 => InodeDataLayout::CompressedCompact,
1435            _ => return Err(ParsingError::InvalidInodeDataLayout(data_layout_raw)),
1436        };
1437        Ok(Self { version, data_layout })
1438    }
1439}
1440
1441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1442pub enum LClusterType {
1443    Plain,
1444    Head1,
1445    NonHead,
1446    Head2,
1447}
1448
1449impl LClusterType {
1450    pub fn is_head(&self) -> bool {
1451        matches!(self, Self::Head1 | Self::Head2 | Self::Plain)
1452    }
1453}
1454
1455impl TryFrom<u16> for LClusterType {
1456    type Error = ParsingError;
1457
1458    fn try_from(advise: u16) -> Result<Self, Self::Error> {
1459        match advise & 3 {
1460            0 => Ok(Self::Plain),
1461            1 => Ok(Self::Head1),
1462            2 => Ok(Self::NonHead),
1463            3 => Ok(Self::Head2),
1464            other => Err(ParsingError::InvalidLClusterType(other)),
1465        }
1466    }
1467}
1468
1469/// A head lcluster is one where the data for a particular extent starts. The logical data
1470/// potentially starts at an unaligned address within this lcluster, described by
1471/// [`extent_start_offset`].
1472#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1473pub struct LClusterHead {
1474    /// The type of this cluster. Will be Head1, Head2, or Plain for this struct.
1475    pub cluster_type: LClusterType,
1476    /// The physical block address where the data lives for the extent described by this set of
1477    /// entries.
1478    pub block_addr: u32,
1479    /// The offset into this lcluster where the data it is describing actually starts. Anything
1480    /// before this offset is actually from the previous extent, so when looking at these entries,
1481    /// if the requested read offset is before this start offset, the read logic needs to walk back
1482    /// one extent to find the relevant data.
1483    pub extent_start_offset: u16,
1484}
1485
1486impl LClusterHead {
1487    pub fn is_plain(&self) -> bool {
1488        self.cluster_type == LClusterType::Plain
1489    }
1490
1491    pub fn logical_start(&self, cluster_index: u64, lcluster_size: u64) -> Option<u64> {
1492        cluster_index.checked_mul(lcluster_size)?.checked_add(self.extent_start_offset as u64)
1493    }
1494}
1495
1496/// An entry describing a single logical cluster, which most often corresponds with a single
1497/// logical, uncompressed block. These entries build a map for where to find the data in the
1498/// compressed physical blocks.
1499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1500pub enum LClusterEntry {
1501    /// A head cluster. See LClusterHead.
1502    Head(LClusterHead),
1503    /// A nonhead cluster. These are blocks of data contained within the extent described by the
1504    /// most recent head lcluster.
1505    NonHead {
1506        /// The distance back to the most recent head lcluster.
1507        delta0: u16,
1508    },
1509}
1510
1511impl LClusterEntry {
1512    /// Parse a single lcluster entry. This is for the CompressedFull data layout, for the
1513    /// CompressedCompact data layout, see [`read_compact_lcluster_entry`].
1514    pub fn read_full_entry(reader: &dyn Reader, offset: u64) -> Result<Self, ErofsError> {
1515        let raw: format::LClusterIndex = reader.read_object(offset)?;
1516        let advise = raw.advisory_flags.get();
1517        let cluster_type = LClusterType::try_from(advise)?;
1518        let extent_start_offset = raw.extent_start_offset.get();
1519
1520        if cluster_type.is_head() {
1521            let block_addr = u32::from_le_bytes(raw.data_union);
1522            Ok(Self::Head(LClusterHead { cluster_type, block_addr, extent_start_offset }))
1523        } else {
1524            let delta0 = u16::from_le_bytes([raw.data_union[0], raw.data_union[1]]);
1525            Ok(Self::NonHead { delta0 })
1526        }
1527    }
1528}
1529
1530/// The layout and geometry of a compact logical cluster index pack.
1531#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1532pub enum CompactPackLayout {
1533    /// 8-byte pack: 2 entries (16 bits each) followed by 4-byte base_pblk. Used for 4B entries.
1534    Pack4B,
1535    /// 32-byte pack: 16 entries (14 bits each) followed by 4-byte base_pblk. Used for 2B entries.
1536    Pack2B,
1537}
1538
1539impl CompactPackLayout {
1540    pub const fn pack_size(&self) -> u64 {
1541        match self {
1542            Self::Pack4B => 8,
1543            Self::Pack2B => 32,
1544        }
1545    }
1546
1547    pub const fn entry_count(&self) -> usize {
1548        match self {
1549            Self::Pack4B => 2,
1550            Self::Pack2B => 16,
1551        }
1552    }
1553
1554    pub const fn encode_bits(&self) -> usize {
1555        match self {
1556            Self::Pack4B => 16,
1557            Self::Pack2B => 14,
1558        }
1559    }
1560}
1561
1562/// Position and layout information for a compact logical cluster index entry on disk.
1563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1564pub struct CompactEntry {
1565    /// On-disk start offset of the pack containing this entry.
1566    pub pack_offset_bytes: u64,
1567    /// Layout of the pack (Pack4B or Pack2B).
1568    pub layout: CompactPackLayout,
1569    /// Index of this entry within the pack.
1570    pub entry_index: usize,
1571}
1572
1573impl CompactEntry {
1574    pub fn pack_size(&self) -> u64 {
1575        self.layout.pack_size()
1576    }
1577
1578    pub fn entry_count(&self) -> usize {
1579        self.layout.entry_count()
1580    }
1581
1582    pub fn encode_bits(&self) -> usize {
1583        self.layout.encode_bits()
1584    }
1585}
1586
1587/// An on-disk compact logical cluster index pack.
1588#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1589pub struct CompactPack {
1590    layout: CompactPackLayout,
1591    data: [u8; 32],
1592}
1593
1594impl CompactPack {
1595    pub fn read(reader: &dyn Reader, pos: &CompactEntry) -> Result<Self, ErofsError> {
1596        let mut data = [0u8; 32];
1597        let pack_size = pos.pack_size() as usize;
1598        reader.read(pos.pack_offset_bytes, &mut data[..pack_size])?;
1599        Ok(Self { layout: pos.layout, data })
1600    }
1601
1602    pub fn from_bytes(layout: CompactPackLayout, buf: &[u8]) -> Result<Self, ParsingError> {
1603        let min_size = layout.pack_size() as usize;
1604        if buf.len() < min_size {
1605            return Err(ParsingError::CompactPackOutOfBounds {
1606                byte_offset: 0,
1607                pack_size: buf.len(),
1608            });
1609        }
1610        let mut data = [0u8; 32];
1611        data[..min_size].copy_from_slice(&buf[..min_size]);
1612        Ok(Self { layout, data })
1613    }
1614
1615    pub fn base_pblk(&self) -> Result<u32, ParsingError> {
1616        let size = self.layout.pack_size() as usize;
1617        let bytes: [u8; 4] = self
1618            .data
1619            .get(size - 4..size)
1620            .ok_or(ParsingError::CompactPackOutOfBounds { byte_offset: size - 4, pack_size: size })?
1621            .try_into()
1622            .map_err(|_| ParsingError::CompactPackOutOfBounds {
1623                byte_offset: size - 4,
1624                pack_size: size,
1625            })?;
1626        Ok(u32::from_le_bytes(bytes))
1627    }
1628
1629    pub fn entry(&self, entry_index: usize) -> Result<CompactPackEntry, ParsingError> {
1630        let pack_size = self.layout.pack_size() as usize;
1631        let bit_offset = self.layout.encode_bits() * entry_index;
1632        let byte_offset = bit_offset / 8;
1633        let bit_shift = bit_offset & 7;
1634        let bytes: [u8; 4] = self
1635            .data
1636            .get(byte_offset..byte_offset + 4)
1637            .ok_or(ParsingError::CompactPackOutOfBounds { byte_offset, pack_size })?
1638            .try_into()
1639            .map_err(|_| ParsingError::CompactPackOutOfBounds { byte_offset, pack_size })?;
1640        let v = u32::from_le_bytes(bytes) >> bit_shift;
1641        let mask = (1u32 << COMPACT_ENTRY_LOBITS) - 1;
1642        let data = (v & mask) as u16;
1643        let type_raw = ((v >> COMPACT_ENTRY_LOBITS) & 3) as u16;
1644        let cluster_type = LClusterType::try_from(type_raw)?;
1645        Ok(CompactPackEntry { data, cluster_type })
1646    }
1647}
1648
1649/// An individual entry decoded from a compact logical cluster index pack.
1650#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1651pub struct CompactPackEntry {
1652    /// The entry bytes store either the extent start offset, for HEAD type clusters, or the
1653    /// distance back to the previous head for nonhead clusters.
1654    data: u16,
1655    /// The type of this cluster.
1656    cluster_type: LClusterType,
1657}
1658
1659impl CompactPackEntry {
1660    pub fn cluster_type(&self) -> LClusterType {
1661        self.cluster_type
1662    }
1663
1664    /// For HEAD or PLAIN entries, interpret the data as the start offset of the extent within in
1665    /// the logical cluster.
1666    pub fn extent_start_offset(&self) -> u16 {
1667        self.data
1668    }
1669
1670    /// For NONHEAD entries, returns the raw delta0 distance value.
1671    pub fn delta0(&self) -> u16 {
1672        self.data
1673    }
1674}
1675
1676#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1677pub struct CompressionHeader {
1678    pub advise: CompressionAdvise,
1679}
1680
1681impl CompressionHeader {
1682    pub fn read(reader: &dyn Reader, offset: u64) -> Result<Self, ErofsError> {
1683        let raw: format::CompressionMapHeader = reader.read_object(offset)?;
1684        if raw.algorithm_type != 0 {
1685            return Err(ParsingError::UnexpectedCompressionAlgorithm(raw.algorithm_type).into());
1686        }
1687        let advise = CompressionAdvise::from_bits_truncate(raw.advisory_flags.get());
1688        Ok(Self { advise })
1689    }
1690}
1691
1692#[cfg(test)]
1693mod tests {
1694    use super::*;
1695    use crate::readers::VecReader;
1696    use std::fs;
1697    use test_case::test_case;
1698    use zerocopy::byteorder::little_endian::{U16 as LEU16, U32 as LEU32, U64 as LEU64};
1699
1700    #[test]
1701    fn test_lcluster_type_and_parsing() {
1702        assert_eq!(LClusterType::try_from(0).unwrap(), LClusterType::Plain);
1703        assert_eq!(LClusterType::try_from(1).unwrap(), LClusterType::Head1);
1704        assert_eq!(LClusterType::try_from(2).unwrap(), LClusterType::NonHead);
1705        assert_eq!(LClusterType::try_from(3).unwrap(), LClusterType::Head2);
1706        assert!(LClusterType::Head1.is_head());
1707        assert!(LClusterType::Plain.is_head());
1708        assert!(!LClusterType::NonHead.is_head());
1709
1710        // Parse Head entry
1711        let raw_head = format::LClusterIndex {
1712            advisory_flags: LEU16::new(1),
1713            extent_start_offset: LEU16::new(128),
1714            data_union: 0x1000u32.to_le_bytes(),
1715        };
1716        let reader = VecReader::new(raw_head.as_bytes().to_vec());
1717        let parsed_head = LClusterEntry::read_full_entry(&reader, 0).unwrap();
1718        assert_eq!(
1719            parsed_head,
1720            LClusterEntry::Head(LClusterHead {
1721                cluster_type: LClusterType::Head1,
1722                block_addr: 0x1000,
1723                extent_start_offset: 128,
1724            })
1725        );
1726
1727        // Parse NonHead entry
1728        let raw_nonhead = format::LClusterIndex {
1729            advisory_flags: LEU16::new(2),
1730            extent_start_offset: LEU16::new(0),
1731            data_union: [4, 0, 0, 0],
1732        };
1733        let reader_nonhead = VecReader::new(raw_nonhead.as_bytes().to_vec());
1734        let parsed_nonhead = LClusterEntry::read_full_entry(&reader_nonhead, 0).unwrap();
1735        assert_eq!(parsed_nonhead, LClusterEntry::NonHead { delta0: 4 });
1736    }
1737
1738    #[test]
1739    fn test_compact_entry_pos_and_index_end_offset() {
1740        let block_size = 4096u64;
1741        let num_lclusters = 10u64;
1742        let node = NodeInner {
1743            inode_offset: 4096,
1744            format: InodeFormat {
1745                version: InodeVersion::Compact,
1746                data_layout: InodeDataLayout::CompressedCompact,
1747            },
1748            mode: 0o100644,
1749            size: num_lclusters * block_size,
1750            data_union: InodeDataUnion::CompressedBlocks(0),
1751            ino: 1,
1752            nid: 1,
1753            link_count: 1,
1754            uid: 0,
1755            gid: 0,
1756            mtime_ns: 0,
1757            xattr_icount: 0,
1758            compression_header: Some(CompressionHeader { advise: CompressionAdvise::empty() }),
1759        };
1760
1761        // map_header_offset = (4096 + 32).next_multiple_of(8) = 4128
1762        assert_eq!(node.map_header_offset().unwrap(), 4128);
1763        // index_table_offset = 4128 + 8 = 4136
1764        assert_eq!(node.index_table_offset().unwrap(), 4136);
1765
1766        // Test compact_entry_pos for 4B entries (8-byte packs)
1767        let pos0 = node.compact_entry_pos(block_size, 0).unwrap();
1768        assert_eq!(pos0.pack_offset_bytes, 4136);
1769        assert_eq!(pos0.layout, CompactPackLayout::Pack4B);
1770        assert_eq!(pos0.pack_size(), 8);
1771        assert_eq!(pos0.entry_index, 0);
1772        assert_eq!(pos0.encode_bits(), 16);
1773        assert_eq!(pos0.entry_count(), 2);
1774
1775        let pos1 = node.compact_entry_pos(block_size, 1).unwrap();
1776        assert_eq!(pos1.pack_offset_bytes, 4136);
1777        assert_eq!(pos1.layout, CompactPackLayout::Pack4B);
1778        assert_eq!(pos1.entry_index, 1);
1779
1780        let pos2 = node.compact_entry_pos(block_size, 2).unwrap();
1781        assert_eq!(pos2.pack_offset_bytes, 4144);
1782        assert_eq!(pos2.layout, CompactPackLayout::Pack4B);
1783        assert_eq!(pos2.entry_index, 0);
1784
1785        // Out of bounds lcn
1786        assert_eq!(
1787            node.compact_entry_pos(block_size, num_lclusters),
1788            Err(ParsingError::LClusterOutOfBounds { cluster_index: 10, total_lclusters: 10 })
1789        );
1790
1791        // Missing compression header
1792        let mut no_header_node = node.clone();
1793        no_header_node.compression_header = None;
1794        assert_eq!(
1795            no_header_node.compact_entry_pos(block_size, 0),
1796            Err(ParsingError::MissingCompressionHeader)
1797        );
1798
1799        // Unexpected layout (FlatPlain)
1800        let mut plain_node = node.clone();
1801        plain_node.format.data_layout = InodeDataLayout::FlatPlain;
1802        assert_eq!(
1803            plain_node.compact_entry_pos(block_size, 0),
1804            Err(ParsingError::UnexpectedInodeDataLayout(InodeDataLayout::FlatPlain))
1805        );
1806        assert_eq!(
1807            plain_node.index_end_offset(block_size),
1808            Err(ParsingError::UnexpectedInodeDataLayout(InodeDataLayout::FlatPlain))
1809        );
1810
1811        // index_end_offset for 10 clusters (last cluster 9 is in pack 4168..4176)
1812        assert_eq!(node.index_end_offset(block_size).unwrap(), 4176);
1813
1814        // Empty file with CompressedCompact layout
1815        let mut empty_compact_node = node.clone();
1816        empty_compact_node.size = 0;
1817        assert_eq!(empty_compact_node.index_end_offset(block_size).unwrap(), 4136);
1818
1819        // CompressedFull layout
1820        let mut full_node = node.clone();
1821        full_node.format.data_layout = InodeDataLayout::CompressedFull;
1822        // index_table_offset = 4128 + 16 = 4144
1823        assert_eq!(full_node.index_table_offset().unwrap(), 4144);
1824        // index_end_offset = 4144 + 10 * 8 = 4224
1825        assert_eq!(full_node.index_end_offset(block_size).unwrap(), 4224);
1826    }
1827
1828    #[test]
1829    fn test_unexpected_compression_algorithm() {
1830        let raw = format::CompressionMapHeader {
1831            reserved_1: [0; 2],
1832            inline_data_size: LEU16::new(0),
1833            advisory_flags: LEU16::new(0),
1834            algorithm_type: 2, // Non-zero (e.g. LZMA)
1835            lcluster_bits: 0,
1836        };
1837        let reader = VecReader::new(raw.as_bytes().to_vec());
1838        let result = CompressionHeader::read(&reader, 0);
1839        assert_eq!(result, Err(ErofsError::Parse(ParsingError::UnexpectedCompressionAlgorithm(2))));
1840    }
1841
1842    #[test]
1843    fn test_compact_entry_decode() {
1844        // Pack buffer with 2 entries of 16 bits each (4 bytes total + 4 bytes base_pblk)
1845        // Entry 0: lo = 128 (0x0080), type = Head1 (1) -> raw = (1 << 12) | 128 = 0x1080
1846        // Entry 1: lo = 3, type = NonHead (2) -> raw = (2 << 12) | 3 = 0x2003
1847        // base_pblk: 0x00000020
1848        let entry1_raw = (2u16 << 12) | 3;
1849        let mut pack_buf = [0u8; 8];
1850        pack_buf[0..2].copy_from_slice(&0x1080u16.to_le_bytes());
1851        pack_buf[2..4].copy_from_slice(&entry1_raw.to_le_bytes());
1852        pack_buf[4..8].copy_from_slice(&0x00000020u32.to_le_bytes());
1853
1854        let pack = CompactPack::from_bytes(CompactPackLayout::Pack4B, &pack_buf).unwrap();
1855        assert_eq!(pack.base_pblk().unwrap(), 0x20);
1856
1857        let entry0 = pack.entry(0).unwrap();
1858        assert_eq!(entry0.extent_start_offset(), 128);
1859        assert_eq!(entry0.cluster_type(), LClusterType::Head1);
1860
1861        let entry1 = pack.entry(1).unwrap();
1862        assert_eq!(entry1.cluster_type(), LClusterType::NonHead);
1863        assert_eq!(entry1.delta0(), 3);
1864    }
1865
1866    fn create_synthetic_sparse_filesystem() -> (ErofsFilesystem, FileNode) {
1867        let block_size = 4096usize;
1868        let num_blocks = 8;
1869        let mut image = vec![0u8; num_blocks * block_size];
1870
1871        // 1. Superblock at offset 1024
1872        let sb = format::SuperBlock {
1873            magic: LEU32::new(format::EROFS_MAGIC),
1874            checksum: LEU32::new(0),
1875            feature_compat: LEU32::new(0),
1876            block_size_bits: 12,
1877            sb_ext_slots: 0,
1878            root_nid: LEU16::new(0),
1879            inode_count: LEU64::new(2),
1880            epoch: LEU64::new(0),
1881            fixed_nsec: LEU32::new(0),
1882            blocks: LEU32::new(num_blocks as u32),
1883            meta_block_addr: LEU32::new(1),
1884            xattr_block_addr: LEU32::new(0),
1885            uuid: [0; 16],
1886            volume_name: [0; 16],
1887            feature_incompat: LEU32::new(0),
1888            available_compr_algs: LEU16::new(0),
1889            extra_devices: LEU32::new(0),
1890            dirblkbits: 0,
1891            reserved: [0; 37],
1892        };
1893        image[format::SUPERBLOCK_OFFSET as usize
1894            ..format::SUPERBLOCK_OFFSET as usize + std::mem::size_of::<format::SuperBlock>()]
1895            .copy_from_slice(sb.as_bytes());
1896
1897        // 2. Root directory inode at nid 0 (offset 4096)
1898        let root_inode = format::InodeCompact {
1899            format: LEU16::new(0),
1900            xattr_icount: LEU16::new(0),
1901            mode: LEU16::new(0o040755),
1902            link_count: LEU16::new(2),
1903            size: LEU32::new(0),
1904            reserved_1: [0; 4],
1905            i_u: [0; 4],
1906            ino: LEU32::new(1),
1907            uid: LEU16::new(0),
1908            gid: LEU16::new(0),
1909            reserved_2: [0; 4],
1910        };
1911        image[4096..4096 + 32].copy_from_slice(root_inode.as_bytes());
1912
1913        // 3. Compressed file inode at nid 1 (offset 4128)
1914        let file_size = 16384u32; // 4 clusters of 4096
1915        let file_inode = format::InodeCompact {
1916            format: LEU16::new((InodeDataLayout::CompressedFull as u16) << 1),
1917            xattr_icount: LEU16::new(0),
1918            mode: LEU16::new(0o100644),
1919            link_count: LEU16::new(1),
1920            size: LEU32::new(file_size),
1921            reserved_1: [0; 4],
1922            i_u: [0; 4],
1923            ino: LEU32::new(2),
1924            uid: LEU16::new(0),
1925            gid: LEU16::new(0),
1926            reserved_2: [0; 4],
1927        };
1928        image[4128..4128 + 32].copy_from_slice(file_inode.as_bytes());
1929
1930        // 4. CompressionMapHeader at map_header_offset = 4160
1931        let map_header = format::CompressionMapHeader {
1932            reserved_1: [0; 2],
1933            inline_data_size: LEU16::new(0),
1934            advisory_flags: LEU16::new(0),
1935            algorithm_type: 0,
1936            lcluster_bits: 0,
1937        };
1938        image[4160..4160 + 8].copy_from_slice(map_header.as_bytes());
1939
1940        // 5. LClusterIndex table at index_table_offset = 4160 + 16 = 4176
1941        // Cluster 0 (0..4096): Plain data pointing to Block 4
1942        let cluster0 = format::LClusterIndex {
1943            advisory_flags: LEU16::new(0),
1944            extent_start_offset: LEU16::new(0),
1945            data_union: 4u32.to_le_bytes(),
1946        };
1947        // Cluster 1 (4096..8192): Sparse hole (blkaddr = 0)
1948        let cluster1 = format::LClusterIndex {
1949            advisory_flags: LEU16::new(0),
1950            extent_start_offset: LEU16::new(0),
1951            data_union: 0u32.to_le_bytes(),
1952        };
1953        // Cluster 2 (8192..12288): Sparse hole (blkaddr = 0)
1954        let cluster2 = format::LClusterIndex {
1955            advisory_flags: LEU16::new(0),
1956            extent_start_offset: LEU16::new(0),
1957            data_union: 0u32.to_le_bytes(),
1958        };
1959        // Cluster 3 (12288..16384): Plain data pointing to Block 5
1960        let cluster3 = format::LClusterIndex {
1961            advisory_flags: LEU16::new(0),
1962            extent_start_offset: LEU16::new(0),
1963            data_union: 5u32.to_le_bytes(),
1964        };
1965        image[4176..4176 + 8].copy_from_slice(cluster0.as_bytes());
1966        image[4184..4184 + 8].copy_from_slice(cluster1.as_bytes());
1967        image[4192..4192 + 8].copy_from_slice(cluster2.as_bytes());
1968        image[4200..4200 + 8].copy_from_slice(cluster3.as_bytes());
1969
1970        // 6. Data in Block 4 (offset 16384) and Block 5 (offset 20480)
1971        image[16384..16384 + 4096].fill(0xAA);
1972        image[20480..20480 + 4096].fill(0xBB);
1973
1974        let reader = Arc::new(VecReader::new(image));
1975        let fs = ErofsFilesystem::new(reader).expect("failed to parse synthetic fs");
1976        let node = fs.node(1).expect("failed to get node 1");
1977        let Node::File(file_node) = node else { panic!("expected file node") };
1978
1979        (fs, file_node)
1980    }
1981
1982    #[test]
1983    fn test_synthetic_sparse_extents() {
1984        let (fs, file_node) = create_synthetic_sparse_filesystem();
1985
1986        // Cluster 0: Block extent (0..4096)
1987        let extent0 = fs.get_extent_at(&file_node.0, 0).unwrap();
1988        assert_eq!(extent0.logical_start, 0);
1989        assert_eq!(extent0.logical_len, 4096);
1990        match extent0.kind {
1991            ExtentKind::Plain { byte_offset: disk_offset } => {
1992                assert_eq!(disk_offset, 4 * 4096);
1993            }
1994            other => panic!("expected Plain extent at 0, got {:?}", other),
1995        }
1996
1997        // Cluster 1: Sparse extent (4096..8192)
1998        let extent1 = fs.get_extent_at(&file_node.0, 4096).unwrap();
1999        assert_eq!(extent1.logical_start, 4096);
2000        assert_eq!(extent1.logical_len, 4096);
2001        assert!(matches!(extent1.kind, ExtentKind::Sparse));
2002
2003        // Inside Cluster 1: Sparse extent at offset 5000
2004        let extent_mid = fs.get_extent_at(&file_node.0, 5000).unwrap();
2005        assert_eq!(extent_mid.logical_start, 4096);
2006        assert_eq!(extent_mid.logical_len, 4096);
2007        assert!(matches!(extent_mid.kind, ExtentKind::Sparse));
2008
2009        // Cluster 2: Sparse extent (8192..12288)
2010        let extent2 = fs.get_extent_at(&file_node.0, 8192).unwrap();
2011        assert_eq!(extent2.logical_start, 8192);
2012        assert_eq!(extent2.logical_len, 4096);
2013        assert!(matches!(extent2.kind, ExtentKind::Sparse));
2014
2015        // Cluster 3: Block extent (12288..16384)
2016        let extent3 = fs.get_extent_at(&file_node.0, 12288).unwrap();
2017        assert_eq!(extent3.logical_start, 12288);
2018        assert_eq!(extent3.logical_len, 4096);
2019        match extent3.kind {
2020            ExtentKind::Plain { byte_offset: disk_offset } => {
2021                assert_eq!(disk_offset, 5 * 4096);
2022            }
2023            other => panic!("expected Plain extent at 12288, got {:?}", other),
2024        }
2025    }
2026
2027    #[test]
2028    fn test_synthetic_sparse_reads() {
2029        let (fs, file_node) = create_synthetic_sparse_filesystem();
2030
2031        // 1. Full read across all extents (Plain -> Sparse -> Sparse -> Plain)
2032        let mut full_buf = vec![0u8; 16384];
2033        let bytes_read = fs.read_file_range(&file_node, 0, &mut full_buf).unwrap();
2034        assert_eq!(bytes_read, 16384);
2035        assert_eq!(&full_buf[0..4096], &[0xAA; 4096]);
2036        assert_eq!(&full_buf[4096..12288], &[0x00; 8192]);
2037        assert_eq!(&full_buf[12288..16384], &[0xBB; 4096]);
2038
2039        // 2. Read entirely within a sparse extent
2040        let mut sparse_buf = vec![0xFFu8; 2000];
2041        let bytes_read = fs.read_file_range(&file_node, 5000, &mut sparse_buf).unwrap();
2042        assert_eq!(bytes_read, 2000);
2043        assert_eq!(&sparse_buf, &[0x00; 2000]);
2044
2045        // 3. Read spanning Plain -> Sparse boundary
2046        let mut span_start_buf = vec![0xFFu8; 200];
2047        let bytes_read = fs.read_file_range(&file_node, 4000, &mut span_start_buf).unwrap();
2048        assert_eq!(bytes_read, 200);
2049        assert_eq!(&span_start_buf[..96], &[0xAA; 96]); // 4000..4096
2050        assert_eq!(&span_start_buf[96..], &[0x00; 104]); // 4096..4200
2051
2052        // 4. Read spanning Sparse -> Plain boundary
2053        let mut span_end_buf = vec![0xFFu8; 200];
2054        let bytes_read = fs.read_file_range(&file_node, 12200, &mut span_end_buf).unwrap();
2055        assert_eq!(bytes_read, 200);
2056        assert_eq!(&span_end_buf[..88], &[0x00; 88]); // 12200..12288
2057        assert_eq!(&span_end_buf[88..], &[0xBB; 112]); // 12288..12400
2058
2059        // 5. Read spanning multiple sparse clusters (Plain -> Sparse 1 -> Sparse 2 -> Plain)
2060        let mut multi_sparse_buf = vec![0xFFu8; 9000];
2061        let bytes_read = fs.read_file_range(&file_node, 4000, &mut multi_sparse_buf).unwrap();
2062        assert_eq!(bytes_read, 9000);
2063        assert_eq!(&multi_sparse_buf[..96], &[0xAA; 96]); // 4000..4096
2064        assert_eq!(&multi_sparse_buf[96..8288], &[0x00; 8192]); // 4096..12288
2065        assert_eq!(&multi_sparse_buf[8288..9000], &[0xBB; 712]); // 12288..13000
2066    }
2067
2068    fn load_image(file: &str) -> Vec<u8> {
2069        fs::read(format!("/pkg/data/{file}")).expect("failed to read test file")
2070    }
2071
2072    #[test_case("simple.erofs" ; "4096 block size")]
2073    #[test_case("simple_512.erofs" ; "512 block size")]
2074    #[test_case("simple_lz4.erofs" ; "4096 block size lz4 compressed")]
2075    #[test_case("simple_lz4_legacy.erofs" ; "4096 block size lz4 legacy compressed")]
2076    #[fuchsia::test]
2077    fn test_parse_superblock(file: &str) {
2078        let runfiles = load_image(file);
2079        let reader = Arc::new(VecReader::new(runfiles.clone()));
2080        // The fs validates the superblock during construction.
2081        let _fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2082
2083        // Now mutate a byte in the superblock. This ensures the checksumming is actually happening
2084        // and getting evaluated correctly.
2085        let mut mutated_runfiles = runfiles.clone();
2086        mutated_runfiles[1088] ^= 0xFF;
2087
2088        let reader = Arc::new(VecReader::new(mutated_runfiles));
2089        let fs = ErofsFilesystem::new(reader);
2090        assert!(fs.is_err());
2091        match fs.err().unwrap() {
2092            ErofsError::Parse(ParsingError::ChecksumMismatch(_, _)) => {}
2093            e => panic!("Expected ChecksumMismatch error, got {:?}", e),
2094        }
2095    }
2096
2097    #[test_case("simple.erofs" ; "4096 block size")]
2098    #[test_case("simple_512.erofs" ; "512 block size")]
2099    #[test_case("simple_lz4.erofs" ; "4096 block size lz4 compressed")]
2100    #[test_case("simple_lz4_legacy.erofs" ; "4096 block size lz4 legacy compressed")]
2101    #[fuchsia::test]
2102    fn test_list_dir(file: &str) {
2103        let runfiles = load_image(file);
2104        let reader = Arc::new(VecReader::new(runfiles));
2105        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2106        let root_node = fs.root_node();
2107
2108        let mut buf = vec![DirectoryEntry::default(); 16];
2109        let filled = fs.read_directory(&root_node, 0, &mut buf).expect("failed to read directory");
2110
2111        let names: Vec<String> = buf[..filled].iter().map(|e| e.name.clone()).collect();
2112        assert_eq!(
2113            names,
2114            vec![
2115                ".",
2116                "..",
2117                "file1",
2118                "large_dir",
2119                "mixed_compression",
2120                "photosynthesis",
2121                "quantum",
2122                "symlink_to_file1",
2123            ]
2124        );
2125    }
2126
2127    #[test_case("simple.erofs" ; "4096 block size")]
2128    #[test_case("simple_512.erofs" ; "512 block size")]
2129    #[test_case("simple_lz4.erofs" ; "4096 block size lz4 compressed")]
2130    #[test_case("simple_lz4_legacy.erofs" ; "4096 block size lz4 legacy compressed")]
2131    #[fuchsia::test]
2132    fn test_overflow_nid(file: &str) {
2133        let runfiles = load_image(file);
2134        let reader = Arc::new(VecReader::new(runfiles));
2135        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2136        let result = fs.node(u64::MAX);
2137        assert!(result.is_err());
2138        assert_eq!(result.unwrap_err(), ErofsError::Parse(ParsingError::InvalidNid(u64::MAX)));
2139    }
2140
2141    #[test_case("simple.erofs", "file1" ; "4096 block size file1")]
2142    #[test_case("simple_512.erofs", "file1" ; "512 block size file1")]
2143    #[test_case("simple_lz4.erofs", "file1" ; "4096 block size lz4 file1")]
2144    #[test_case("simple_lz4_legacy.erofs", "file1" ; "4096 block size lz4 legacy file1")]
2145    #[test_case("simple.erofs", "photosynthesis" ; "4096 block size photosynthesis")]
2146    #[test_case("simple_512.erofs", "photosynthesis" ; "512 block size photosynthesis")]
2147    #[test_case("simple_lz4.erofs", "photosynthesis" ; "4096 block size lz4 photosynthesis")]
2148    #[test_case("simple_lz4_legacy.erofs", "photosynthesis" ; "4096 block size lz4 legacy photosynthesis")]
2149    #[test_case("simple_lz4.erofs", "quantum" ; "4096 block size lz4 quantum")]
2150    #[test_case("simple_lz4_legacy.erofs", "quantum" ; "4096 block size lz4 legacy quantum")]
2151    #[test_case("simple.erofs", "mixed_compression" ; "4096 block size mixed_compression")]
2152    #[test_case("simple_512.erofs", "mixed_compression" ; "512 block size mixed_compression")]
2153    #[test_case("simple_lz4.erofs", "mixed_compression" ; "4096 block size lz4 mixed_compression")]
2154    #[test_case("simple_lz4_legacy.erofs", "mixed_compression" ; "4096 block size lz4 legacy mixed_compression")]
2155    #[fuchsia::test]
2156    fn test_read_file_range(file: &str, name: &str) {
2157        let runfiles = load_image(file);
2158        let reader = Arc::new(VecReader::new(runfiles));
2159        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2160        let root_node = fs.root_node();
2161
2162        let node = fs.lookup(&root_node, name).expect("failed to lookup").expect("file not found");
2163        let file_node = match node {
2164            Node::File(f) => f,
2165            _ => panic!("Expected file node"),
2166        };
2167
2168        let size = file_node.size() as usize;
2169        let mut buf = vec![0u8; size];
2170        let bytes_read = fs.read_file_range(&file_node, 0, &mut buf).expect("failed to read");
2171        assert_eq!(bytes_read, size);
2172        let expected =
2173            fs::read(format!("/pkg/data/simple/{}", name)).expect("failed to read source file");
2174        assert_eq!(buf, expected);
2175
2176        // Test partial read within file
2177        let mut buf = vec![0u8; 5];
2178        let bytes_read =
2179            fs.read_file_range(&file_node, 5, &mut buf).expect("failed to read partial");
2180        assert_eq!(bytes_read, 5);
2181        assert_eq!(&buf, &expected[5..10]);
2182
2183        // Test non-extent-aligned seek reads into multi-cluster extents
2184        if size > 5000 {
2185            let mut buf = vec![0u8; 100];
2186            let bytes_read =
2187                fs.read_file_range(&file_node, 5000, &mut buf).expect("failed to read at 5000");
2188            assert_eq!(bytes_read, 100);
2189            assert_eq!(&buf, &expected[5000..5100]);
2190        }
2191        if size > 15000 {
2192            let mut buf = vec![0u8; 200];
2193            let bytes_read =
2194                fs.read_file_range(&file_node, 15000, &mut buf).expect("failed to read at 15000");
2195            assert_eq!(bytes_read, 200);
2196            assert_eq!(&buf, &expected[15000..15200]);
2197        }
2198
2199        // Test read spanning across EOF (buffer larger than remaining data)
2200        let mut buf = vec![0u8; 100];
2201        let bytes_read = fs
2202            .read_file_range(&file_node, (size - 5) as u64, &mut buf)
2203            .expect("failed to read past eof");
2204        assert_eq!(bytes_read, 5);
2205        if name == "file1" {
2206            assert_eq!(&buf[..5], b"file\n");
2207        }
2208
2209        // Test read at EOF
2210        let mut buf = vec![0u8; 100];
2211        let bytes_read =
2212            fs.read_file_range(&file_node, size as u64, &mut buf).expect("failed to read");
2213        assert_eq!(bytes_read, 0);
2214    }
2215
2216    #[test_case("simple_lz4.erofs" ; "4096 block size lz4 compressed")]
2217    #[test_case("simple_lz4_legacy.erofs" ; "4096 block size lz4 legacy compressed")]
2218    #[fuchsia::test]
2219    fn test_mixed_compression(file: &str) {
2220        let runfiles = load_image(file);
2221        let fs = ErofsFilesystem::new(Arc::new(VecReader::new(runfiles))).unwrap();
2222        let root = fs.root_node();
2223        let node = fs.lookup(&root, "mixed_compression").unwrap().unwrap();
2224        let Node::File(file_node) = node else { panic!() };
2225
2226        let lcluster_size = fs.block_size();
2227        let totalidx = file_node.total_lclusters(lcluster_size);
2228        let mut has_compressed_head = false;
2229        let mut has_plain_cluster = false;
2230        for lcn in 0..totalidx {
2231            if let Ok(LClusterEntry::Head(head)) = fs.read_lcluster_entry(&file_node.0, lcn) {
2232                if head.cluster_type.is_head() && head.cluster_type != LClusterType::Plain {
2233                    has_compressed_head = true;
2234                }
2235                if head.cluster_type == LClusterType::Plain {
2236                    has_plain_cluster = true;
2237                }
2238            }
2239        }
2240        assert!(
2241            has_compressed_head && has_plain_cluster,
2242            "mixed_compression should contain some plain clusters"
2243        );
2244
2245        let size = file_node.size() as usize;
2246        let mut buf = vec![0u8; size];
2247        let bytes_read = fs.read_file_range(&file_node, 0, &mut buf).expect("failed to read");
2248        assert_eq!(bytes_read, size);
2249
2250        let expected =
2251            fs::read("/pkg/data/simple/mixed_compression").expect("failed to read source file");
2252        assert_eq!(buf, expected);
2253    }
2254
2255    #[test_case("simple.erofs" ; "4096 block size")]
2256    #[test_case("simple_512.erofs" ; "512 block size")]
2257    #[test_case("simple_lz4.erofs" ; "4096 block size lz4 compressed")]
2258    #[test_case("simple_lz4_legacy.erofs" ; "4096 block size lz4 legacy compressed")]
2259    #[fuchsia::test]
2260    fn test_read_symlink(file: &str) {
2261        let runfiles = load_image(file);
2262        let reader = Arc::new(VecReader::new(runfiles));
2263        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2264        let root_node = fs.root_node();
2265
2266        let node = fs
2267            .lookup(&root_node, "symlink_to_file1")
2268            .expect("failed to lookup")
2269            .expect("symlink not found");
2270        let symlink_node = match node {
2271            Node::Symlink(s) => s,
2272            _ => panic!("Expected symlink node"),
2273        };
2274
2275        let target = fs.read_symlink(&symlink_node).expect("failed to read symlink");
2276        assert_eq!(target, b"file1");
2277
2278        let selinux_val = fs.get_xattr(&symlink_node, b"security.selinux").unwrap().unwrap();
2279        assert_eq!(selinux_val, b"u:object_r:symlink_t:s0");
2280    }
2281
2282    #[test_case("simple.erofs" ; "4096 block size")]
2283    #[test_case("simple_512.erofs" ; "512 block size")]
2284    #[fuchsia::test]
2285    fn test_read_directory_pagination(file: &str) {
2286        let runfiles = load_image(file);
2287        let reader = Arc::new(VecReader::new(runfiles));
2288        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2289        let root_node = fs.root_node();
2290
2291        let expected_names = vec![
2292            ".",
2293            "..",
2294            "file1",
2295            "large_dir",
2296            "mixed_compression",
2297            "photosynthesis",
2298            "quantum",
2299            "symlink_to_file1",
2300        ];
2301
2302        // Test reading with buffer size 2 (pagination)
2303        let mut buf = vec![DirectoryEntry::default(); 2];
2304
2305        // Page 1 (offset 0)
2306        let filled = fs.read_directory(&root_node, 0, &mut buf).expect("failed to read dir");
2307        assert_eq!(filled, 2);
2308        assert_eq!(buf[0].name, expected_names[0]);
2309        assert_eq!(buf[1].name, expected_names[1]);
2310
2311        // Page 2 (offset 2)
2312        let filled = fs.read_directory(&root_node, 2, &mut buf).expect("failed to read dir");
2313        assert_eq!(filled, 2);
2314        assert_eq!(buf[0].name, expected_names[2]);
2315        assert_eq!(buf[1].name, expected_names[3]);
2316
2317        // Page 3 (offset 4)
2318        let filled = fs.read_directory(&root_node, 4, &mut buf).expect("failed to read dir");
2319        assert_eq!(filled, 2);
2320        assert_eq!(buf[0].name, expected_names[4]);
2321        assert_eq!(buf[1].name, expected_names[5]);
2322
2323        // Page 4 (offset 6)
2324        let filled = fs.read_directory(&root_node, 6, &mut buf).expect("failed to read dir");
2325        assert_eq!(filled, 2);
2326        assert_eq!(buf[0].name, expected_names[6]);
2327        assert_eq!(buf[1].name, expected_names[7]);
2328
2329        // Page 5 (offset 8 - EOF)
2330        let filled = fs.read_directory(&root_node, 8, &mut buf).expect("failed to read dir");
2331        assert_eq!(filled, 0);
2332
2333        // Test reading with buffer size 1 (extreme pagination)
2334        let mut buf1 = vec![DirectoryEntry::default(); 1];
2335        for i in 0..expected_names.len() {
2336            let filled = fs.read_directory(&root_node, i, &mut buf1).expect("failed to read dir");
2337            assert_eq!(filled, 1);
2338            assert_eq!(buf1[0].name, expected_names[i]);
2339        }
2340        let filled = fs
2341            .read_directory(&root_node, expected_names.len(), &mut buf1)
2342            .expect("failed to read dir");
2343        assert_eq!(filled, 0);
2344    }
2345
2346    #[test_case("simple.erofs" ; "4096 block size")]
2347    #[test_case("simple_512.erofs" ; "512 block size")]
2348    #[test_case("simple_lz4.erofs" ; "4096 block size lz4 compressed")]
2349    #[test_case("simple_lz4_legacy.erofs" ; "4096 block size lz4 legacy compressed")]
2350    #[fuchsia::test]
2351    fn test_read_directory_large_dir(file: &str) {
2352        // Note: the large directory in the golden image is only large enough to split the entries
2353        // into multiple blocks on the 512 block size golden.
2354        let runfiles = load_image(file);
2355        let reader = Arc::new(VecReader::new(runfiles));
2356        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2357        let root_node = fs.root_node();
2358
2359        let large_dir_node = fs
2360            .lookup(&root_node, "large_dir")
2361            .expect("failed to look up large_dir")
2362            .expect("large_dir not found");
2363
2364        let large_dir = match large_dir_node {
2365            Node::Directory(d) => d,
2366            _ => panic!("Expected directory node"),
2367        };
2368
2369        assert_eq!(fs.get_xattr(&root_node, b"security.selinux").unwrap(), None);
2370        let selinux_val = fs.get_xattr(&large_dir, b"security.selinux").unwrap().unwrap();
2371        assert_eq!(
2372            selinux_val,
2373            b"u:object_r:very_long_selinux_context_exceeding_the_inline_limit_of_two_hundred_and_fifty_six_bytes_and_requiring_the_use_of_extended_attributes_instead_of_returning_the_context_inline_in_the_node_attributes_table_representation_as_dictated_by_the_fuchsia_io_node_fidl_specification:s0"
2374        );
2375        let file1_node = fs.lookup(&large_dir, "file_number_1").unwrap().unwrap();
2376        let file1_selinux = fs.get_xattr(&file1_node, b"security.selinux").unwrap().unwrap();
2377        assert_eq!(
2378            file1_selinux,
2379            b"u:object_r:very_long_selinux_context_exceeding_the_inline_limit_of_two_hundred_and_fifty_six_bytes_and_requiring_the_use_of_extended_attributes_instead_of_returning_the_context_inline_in_the_node_attributes_table_representation_as_dictated_by_the_fuchsia_io_node_fidl_specification:s0"
2380        );
2381
2382        // Skip the first two entries, . and ..
2383        let mut entry_offset = 2;
2384        let mut buffer = vec![DirectoryEntry::default(); 16];
2385        loop {
2386            let filled = fs.read_directory(&large_dir, entry_offset, &mut buffer).unwrap();
2387            for i in 0..filled {
2388                // check the prefix
2389                assert_eq!(buffer[i].name[..12], format!("file_number_"));
2390            }
2391            if filled < buffer.len() {
2392                break;
2393            }
2394            entry_offset += filled;
2395        }
2396    }
2397
2398    #[test_case("simple.erofs" ; "4096 block size")]
2399    #[test_case("simple_512.erofs" ; "512 block size")]
2400    #[test_case("simple_lz4.erofs" ; "4096 block size lz4 compressed")]
2401    #[test_case("simple_lz4_legacy.erofs" ; "4096 block size lz4 legacy compressed")]
2402    #[fuchsia::test]
2403    fn test_filesystem_metadata(file: &str) {
2404        let runfiles = load_image(file);
2405        let reader = Arc::new(VecReader::new(runfiles));
2406        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2407
2408        assert!(fs.total_bytes() > 0);
2409        assert!(fs.total_inodes() > 0);
2410    }
2411
2412    #[test_case("simple.erofs" ; "4096 block size")]
2413    #[test_case("simple_512.erofs" ; "512 block size")]
2414    #[fuchsia::test]
2415    fn test_node_metadata(file: &str) {
2416        let runfiles = load_image(file);
2417        let reader = Arc::new(VecReader::new(runfiles));
2418        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2419        let root_node = fs.root_node();
2420
2421        assert!(root_node.link_count() >= 2);
2422        assert!(root_node.mtime_ns() > 0);
2423
2424        let file1_node = fs.lookup(&root_node, "file1").unwrap().unwrap();
2425        assert_eq!(file1_node.link_count(), 1);
2426        assert!(file1_node.mtime_ns() > 0);
2427    }
2428
2429    #[test_case("simple.erofs" ; "4096 block size")]
2430    #[test_case("simple_512.erofs" ; "512 block size")]
2431    #[fuchsia::test]
2432    fn test_xattrs(file: &str) {
2433        let runfiles = load_image(file);
2434        let reader = Arc::new(VecReader::new(runfiles));
2435        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2436        let root_node = fs.root_node();
2437
2438        // Check file1 (has both inline and shared xattrs)
2439        let file1_node = fs.lookup(&root_node, "file1").unwrap().unwrap();
2440
2441        let xattr_names = fs.list_xattrs(&file1_node).unwrap();
2442        // Should contain user.flavor, user.security, user.shared, security.selinux
2443        assert!(xattr_names.contains(&b"user.flavor".to_vec()));
2444        assert!(xattr_names.contains(&b"user.security".to_vec()));
2445        assert!(xattr_names.contains(&b"user.shared".to_vec()));
2446        assert!(xattr_names.contains(&b"security.selinux".to_vec()));
2447        assert_eq!(xattr_names.len(), 4);
2448
2449        let flavor_val = fs.get_xattr(&file1_node, b"user.flavor").unwrap().unwrap();
2450        assert_eq!(flavor_val, b"vanilla");
2451
2452        let security_val = fs.get_xattr(&file1_node, b"user.security").unwrap().unwrap();
2453        assert_eq!(security_val, b"high");
2454
2455        let shared_val = fs.get_xattr(&file1_node, b"user.shared").unwrap().unwrap();
2456        assert_eq!(shared_val, b"same_value");
2457
2458        let selinux_val = fs.get_xattr(&file1_node, b"security.selinux").unwrap().unwrap();
2459        assert_eq!(selinux_val, b"u:object_r:file1_t:s0");
2460
2461        // Check photosynthesis (has only shared xattr)
2462        let photo_node = fs.lookup(&root_node, "photosynthesis").unwrap().unwrap();
2463
2464        let photo_xattrs = fs.list_xattrs(&photo_node).unwrap();
2465        assert_eq!(photo_xattrs, vec![b"user.shared".to_vec()]);
2466
2467        let photo_shared_val = fs.get_xattr(&photo_node, b"user.shared").unwrap().unwrap();
2468        assert_eq!(photo_shared_val, b"same_value");
2469        assert_eq!(fs.get_xattr(&photo_node, b"security.selinux").unwrap(), None);
2470
2471        // Check quantum (has no xattrs)
2472        let quantum_node = fs.lookup(&root_node, "quantum").unwrap().unwrap();
2473        assert_eq!(fs.get_xattr(&quantum_node, b"security.selinux").unwrap(), None);
2474
2475        // Verify that we can still read the file content of photosynthesis
2476        let file_node = match photo_node {
2477            Node::File(f) => f,
2478            _ => panic!("Expected file node"),
2479        };
2480        let size = file_node.size() as usize;
2481        let mut buf = vec![0u8; size];
2482        let bytes_read = fs.read_file_range(&file_node, 0, &mut buf).expect("failed to read");
2483        assert_eq!(bytes_read, size);
2484        assert!(size > 0);
2485
2486        // Check non-existent xattr
2487        let val = fs.get_xattr(&file1_node, b"user.non_existent").unwrap();
2488        assert_eq!(val, None);
2489    }
2490
2491    // EROFS doesn't seem to make shared xattr groups with simple xattrs like the one above (I
2492    // assume it has some heuristics for judging when it is worth the cost) so this test manually
2493    // constructs a shared xattr area to test that part of the parsing logic.
2494    #[fuchsia::test]
2495    fn test_shared_xattr_parsing() {
2496        let block_size = 4096usize;
2497        let mut buf = vec![0u8; 3 * block_size];
2498
2499        // Superblock at offset 1024
2500        let sb = format::SuperBlock {
2501            magic: LEU32::new(format::EROFS_MAGIC),
2502            checksum: LEU32::new(0),
2503            feature_compat: LEU32::new(0),
2504            block_size_bits: 12,
2505            sb_ext_slots: 0,
2506            root_nid: LEU16::new(0),
2507            inode_count: LEU64::new(1),
2508            epoch: LEU64::new(0),
2509            fixed_nsec: LEU32::new(0),
2510            blocks: LEU32::new(3),
2511            meta_block_addr: LEU32::new(1),
2512            xattr_block_addr: LEU32::new(0),
2513            uuid: [0; 16],
2514            volume_name: [0; 16],
2515            feature_incompat: LEU32::new(0),
2516            available_compr_algs: LEU16::new(0),
2517            extra_devices: LEU32::new(0),
2518            dirblkbits: 0,
2519            reserved: [0; 37],
2520        };
2521        buf[1024..1024 + 128].copy_from_slice(sb.as_bytes());
2522
2523        // Compact Inode at meta_block_addr (block 1, offset 4096)
2524        let inode = format::InodeCompact {
2525            format: LEU16::new(0),
2526            xattr_icount: LEU16::new(2),
2527            mode: LEU16::new(0o040755),
2528            link_count: LEU16::new(1),
2529            size: LEU32::new(0),
2530            reserved_1: [0; 4],
2531            i_u: [0; 4],
2532            ino: LEU32::new(0),
2533            uid: LEU16::new(0),
2534            gid: LEU16::new(0),
2535            reserved_2: [0; 4],
2536        };
2537        buf[4096..4096 + 32].copy_from_slice(inode.as_bytes());
2538
2539        // XattrInlineBodyHeader at offset 4128
2540        let header = format::XattrInlineBodyHeader {
2541            name_filter: LEU32::new(0),
2542            shared_count: 1,
2543            reserved: [0; 7],
2544        };
2545        buf[4128..4128 + 12].copy_from_slice(header.as_bytes());
2546        // shared_id index 512 (512 * 4 = offset 2048 in block 0) at offset 4140
2547        buf[4140..4144].copy_from_slice(&512u32.to_le_bytes());
2548
2549        // Shared Xattr Entry at xattr_block_addr (block 0, offset 2048)
2550        let xentry = format::XattrEntry {
2551            name_len: 6,
2552            name_index: 1, // "user."
2553            value_size: LEU16::new(10),
2554        };
2555        buf[2048..2052].copy_from_slice(xentry.as_bytes());
2556        buf[2052..2058].copy_from_slice(b"shared");
2557        buf[2058..2068].copy_from_slice(b"same_value");
2558
2559        let reader = Arc::new(VecReader::new(buf));
2560        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2561        let root_node = fs.root_node();
2562
2563        let xattr_names = fs.list_xattrs(&root_node).expect("failed to list xattrs");
2564        assert_eq!(xattr_names, vec![b"user.shared".to_vec()]);
2565
2566        let shared_val =
2567            fs.get_xattr(&root_node, b"user.shared").expect("failed to get xattr").unwrap();
2568        assert_eq!(shared_val, b"same_value");
2569    }
2570
2571    #[fuchsia::test]
2572    fn test_xattr_iterator_fusing_on_error() {
2573        let block_size = 4096usize;
2574        let mut buf = vec![0u8; 3 * block_size];
2575
2576        let sb = format::SuperBlock {
2577            magic: LEU32::new(format::EROFS_MAGIC),
2578            checksum: LEU32::new(0),
2579            feature_compat: LEU32::new(0),
2580            block_size_bits: 12,
2581            sb_ext_slots: 0,
2582            root_nid: LEU16::new(0),
2583            inode_count: LEU64::new(1),
2584            epoch: LEU64::new(0),
2585            fixed_nsec: LEU32::new(0),
2586            blocks: LEU32::new(3),
2587            meta_block_addr: LEU32::new(1),
2588            xattr_block_addr: LEU32::new(2),
2589            uuid: [0; 16],
2590            volume_name: [0; 16],
2591            feature_incompat: LEU32::new(0),
2592            available_compr_algs: LEU16::new(0),
2593            extra_devices: LEU32::new(0),
2594            dirblkbits: 0,
2595            reserved: [0; 37],
2596        };
2597        buf[1024..1024 + 128].copy_from_slice(sb.as_bytes());
2598
2599        let inode = format::InodeCompact {
2600            format: LEU16::new(0),
2601            xattr_icount: LEU16::new(3),
2602            mode: LEU16::new(0o040755),
2603            link_count: LEU16::new(1),
2604            size: LEU32::new(0),
2605            reserved_1: [0; 4],
2606            i_u: [0; 4],
2607            ino: LEU32::new(0),
2608            uid: LEU16::new(0),
2609            gid: LEU16::new(0),
2610            reserved_2: [0; 4],
2611        };
2612        buf[4096..4096 + 32].copy_from_slice(inode.as_bytes());
2613
2614        let header = format::XattrInlineBodyHeader {
2615            name_filter: LEU32::new(0),
2616            shared_count: 2,
2617            reserved: [0; 7],
2618        };
2619        buf[4128..4128 + 12].copy_from_slice(header.as_bytes());
2620        buf[4140..4144].copy_from_slice(&0u32.to_le_bytes());
2621        buf[4144..4148].copy_from_slice(&1u32.to_le_bytes());
2622
2623        // Invalid shared xattr entry (invalid name_index 99) at xattr_block_addr for shared_id 0
2624        // (offset 8192)
2625        let invalid_xentry =
2626            format::XattrEntry { name_len: 6, name_index: 99, value_size: LEU16::new(10) };
2627        buf[8192..8196].copy_from_slice(invalid_xentry.as_bytes());
2628
2629        let reader = Arc::new(VecReader::new(buf));
2630        let fs = ErofsFilesystem::new(reader).expect("failed to parse superblock");
2631        let root_node = fs.root_node();
2632
2633        let mut iter = fs.iter_xattrs(&root_node).expect("failed to create iterator");
2634        assert!(iter.next().unwrap().is_err());
2635        // Iterator MUST be fused now: subsequent next() calls MUST return None
2636        assert!(iter.next().is_none());
2637    }
2638}