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