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