Skip to main content

microsandbox_image/erofs/
reader.rs

1//! Minimal EROFS reader for extracting file contents from our own images.
2//!
3//! Only supports the subset of EROFS that our writer produces:
4//! - Extended inodes (64 bytes)
5//! - Uncompressed data (FLAT_PLAIN or FLAT_INLINE)
6//! - Sorted directory entries (binary search)
7//! - No shared xattrs, no compression, no chunks
8
9use std::collections::HashSet;
10use std::io::Read;
11use std::path::Path;
12use std::{ffi::OsString, fs::File, io, path::PathBuf};
13
14use super::format::{
15    EROFS_BLKSIZ, EROFS_DIRENT_SIZE, EROFS_INODE_EXTENDED_SIZE, EROFS_INODE_FLAT_INLINE,
16    EROFS_INODE_FLAT_PLAIN, EROFS_NULL_ADDR, EROFS_SUPER_OFFSET, EROFS_XATTR_IBODY_HEADER_SIZE,
17    EROFS_XATTR_INDEX_SECURITY, EROFS_XATTR_INDEX_TRUSTED, EROFS_XATTR_INDEX_USER, S_IFBLK,
18    S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, erofs_xattr_align,
19};
20use crate::path_bytes::{os_str_bytes, os_string_from_vec};
21use crate::tree::{InodeMetadata, Xattr};
22
23//--------------------------------------------------------------------------------------------------
24// Types
25//--------------------------------------------------------------------------------------------------
26
27/// A handle to an open EROFS image for reading.
28pub struct ErofsReader {
29    file: File,
30    meta_blkaddr: u32,
31    root_nid: u32,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ErofsEntryKind {
36    RegularFile,
37    Directory,
38    Symlink,
39    CharDevice,
40    BlockDevice,
41    Fifo,
42    Socket,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct ErofsEntryInfo {
47    pub kind: ErofsEntryKind,
48    pub opaque: bool,
49    pub whiteout: bool,
50}
51
52/// A filesystem entry discovered while walking an EROFS image.
53#[derive(Clone)]
54pub struct ErofsTreeEntry {
55    /// Path relative to the image root.
56    pub path: PathBuf,
57    /// Stable EROFS inode identifier.
58    pub nid: u32,
59    /// Entry kind.
60    pub kind: ErofsEntryKind,
61    /// POSIX inode metadata.
62    pub metadata: InodeMetadata,
63    /// Inline xattrs stored on the inode.
64    pub xattrs: Vec<Xattr>,
65    /// File or symlink data size.
66    pub size: u64,
67    /// Device major/minor for device nodes.
68    pub rdev: Option<(u32, u32)>,
69}
70
71/// Streaming reader for a regular file stored inside an EROFS image.
72pub struct ErofsFileDataReader {
73    file: File,
74    segments: Vec<(u64, u64)>,
75    segment_index: usize,
76    segment_offset: u64,
77}
78
79#[cfg(test)]
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub(crate) struct ErofsInodeDebugInfo {
82    pub nid: u32,
83    pub nlink: u32,
84    pub size: u64,
85    pub data_layout: u8,
86}
87
88//--------------------------------------------------------------------------------------------------
89// Methods
90//--------------------------------------------------------------------------------------------------
91
92impl ErofsReader {
93    /// Open an EROFS image by parsing the superblock.
94    pub fn new(file: File) -> io::Result<Self> {
95        let mut sb = [0u8; 128];
96        read_exact_at(&file, EROFS_SUPER_OFFSET, &mut sb)?;
97
98        let magic = u32::from_le_bytes([sb[0], sb[1], sb[2], sb[3]]);
99        if magic != 0xE0F5_E1E2 {
100            return Err(io::Error::new(
101                io::ErrorKind::InvalidData,
102                format!("bad EROFS magic: {magic:#x}"),
103            ));
104        }
105
106        let root_nid = u16::from_le_bytes([sb[0x0E], sb[0x0F]]) as u32;
107        let meta_blkaddr = u32::from_le_bytes([sb[0x28], sb[0x29], sb[0x2A], sb[0x2B]]);
108
109        Ok(Self {
110            file,
111            meta_blkaddr,
112            root_nid,
113        })
114    }
115
116    /// Read a file by path from the EROFS image. Returns the file data.
117    pub fn read_file(&mut self, path: &str) -> io::Result<Vec<u8>> {
118        let target_inode = self.lookup_path(path)?;
119        if (target_inode.mode & S_IFMT) != S_IFREG {
120            return Err(io::Error::new(
121                io::ErrorKind::InvalidInput,
122                "target is not a regular file",
123            ));
124        }
125        self.read_inode_data(&target_inode)
126    }
127
128    /// Read a symlink target by path from the EROFS image.
129    pub fn read_link(&mut self, path: &str) -> io::Result<Vec<u8>> {
130        let target_inode = self.lookup_path(path)?;
131        if (target_inode.mode & S_IFMT) != S_IFLNK {
132            return Err(io::Error::new(
133                io::ErrorKind::InvalidInput,
134                "target is not a symlink",
135            ));
136        }
137        self.read_inode_data(&target_inode)
138    }
139
140    pub fn entry_info(&mut self, path: &str) -> io::Result<ErofsEntryInfo> {
141        let inode = self.lookup_path(path)?;
142        let kind = inode_kind(&inode)?;
143        let opaque = if kind == ErofsEntryKind::Directory {
144            self.inode_is_opaque(&inode)?
145        } else {
146            false
147        };
148        let whiteout = kind == ErofsEntryKind::CharDevice && inode.rdev == 0;
149
150        Ok(ErofsEntryInfo {
151            kind,
152            opaque,
153            whiteout,
154        })
155    }
156
157    /// Walk all entries in the image in stable path order.
158    pub fn walk(&mut self) -> io::Result<Vec<ErofsTreeEntry>> {
159        let root = self.read_inode(self.root_nid)?;
160        let mut entries = Vec::new();
161        let mut visited = HashSet::new();
162        self.walk_dir(&root, PathBuf::new(), &mut entries, &mut visited)?;
163        Ok(entries)
164    }
165
166    /// Walk all entries in stable path order, invoking a callback for each entry.
167    pub fn walk_entries<E, F>(&mut self, mut visit: F) -> Result<(), E>
168    where
169        E: From<io::Error>,
170        F: FnMut(&mut Self, ErofsTreeEntry) -> Result<(), E>,
171    {
172        let root = self.read_inode(self.root_nid)?;
173        let mut visited = HashSet::new();
174        self.walk_dir_entries(&root, PathBuf::new(), &mut visited, &mut visit)
175    }
176
177    /// Create a streaming reader for a regular file inode by NID.
178    pub fn file_data_reader(&mut self, nid: u32) -> io::Result<ErofsFileDataReader> {
179        let inode = self.read_inode(nid)?;
180        if (inode.mode & S_IFMT) != S_IFREG {
181            return Err(io::Error::new(
182                io::ErrorKind::InvalidInput,
183                "target is not a regular file",
184            ));
185        }
186
187        Ok(ErofsFileDataReader {
188            file: self.file.try_clone()?,
189            segments: self.inode_data_segments(&inode)?,
190            segment_index: 0,
191            segment_offset: 0,
192        })
193    }
194
195    /// Read a symlink target by NID.
196    pub fn read_link_by_nid(&mut self, nid: u32) -> io::Result<Vec<u8>> {
197        let inode = self.read_inode(nid)?;
198        if (inode.mode & S_IFMT) != S_IFLNK {
199            return Err(io::Error::new(
200                io::ErrorKind::InvalidInput,
201                "target is not a symlink",
202            ));
203        }
204        self.read_inode_data(&inode)
205    }
206
207    #[cfg(test)]
208    pub(crate) fn inode_debug_info(&mut self, path: &str) -> io::Result<ErofsInodeDebugInfo> {
209        let inode = self.lookup_path(path)?;
210        Ok(ErofsInodeDebugInfo {
211            nid: inode.nid,
212            nlink: inode.nlink,
213            size: inode.size,
214            data_layout: inode.data_layout,
215        })
216    }
217
218    fn inode_offset(&self, nid: u32) -> u64 {
219        (self.meta_blkaddr as u64) * (EROFS_BLKSIZ as u64) + (nid as u64) * 32
220    }
221
222    fn read_inode(&mut self, nid: u32) -> io::Result<InodeInfo> {
223        let offset = self.inode_offset(nid);
224
225        let mut buf = [0u8; EROFS_INODE_EXTENDED_SIZE as usize];
226        read_exact_at(&self.file, offset, &mut buf)?;
227
228        let i_format = u16::from_le_bytes([buf[0], buf[1]]);
229        let i_xattr_icount = u16::from_le_bytes([buf[2], buf[3]]);
230        let mode = u16::from_le_bytes([buf[4], buf[5]]);
231        let size = u64::from_le_bytes([
232            buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
233        ]);
234        let i_u = u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]);
235        let nlink = u32::from_le_bytes([buf[44], buf[45], buf[46], buf[47]]);
236        let uid = u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]);
237        let gid = u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]);
238        let mtime = u64::from_le_bytes([
239            buf[32], buf[33], buf[34], buf[35], buf[36], buf[37], buf[38], buf[39],
240        ]);
241        let mtime_nsec = u32::from_le_bytes([buf[40], buf[41], buf[42], buf[43]]);
242
243        let data_layout = ((i_format >> 1) & 0x07) as u8;
244
245        // Compute xattr ibody size to know where inline data starts.
246        // Formula from EROFS spec: ibody = 12-byte header + (i_xattr_icount - 1) * 4 bytes.
247        // The "- 1" accounts for the header occupying the first count unit.
248        let xattr_ibody_size = if i_xattr_icount == 0 {
249            0u32
250        } else {
251            12 + ((i_xattr_icount as u32) - 1) * 4
252        };
253
254        Ok(InodeInfo {
255            nid,
256            mode,
257            size,
258            nlink,
259            uid,
260            gid,
261            mtime,
262            mtime_nsec,
263            data_layout,
264            startblk_lo: i_u,
265            rdev: i_u,
266            xattr_ibody_size,
267        })
268    }
269
270    fn lookup_path(&mut self, path: &str) -> io::Result<InodeInfo> {
271        let components: Vec<&str> = path
272            .trim_start_matches('/')
273            .split('/')
274            .filter(|c| !c.is_empty())
275            .collect();
276
277        if components.is_empty() {
278            if path == "/" {
279                return self.read_inode(self.root_nid);
280            }
281            return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty path"));
282        }
283
284        let mut current_nid = self.root_nid;
285        for (i, component) in components.iter().enumerate() {
286            let inode = self.read_inode(current_nid)?;
287            let mode_type = inode.mode & S_IFMT;
288
289            if mode_type != S_IFDIR {
290                return Err(io::Error::new(
291                    io::ErrorKind::NotFound,
292                    format!("not a directory at component '{component}'"),
293                ));
294            }
295
296            let target_nid = self.lookup_in_dir(&inode, component)?;
297            if i + 1 == components.len() {
298                return self.read_inode(target_nid);
299            }
300
301            current_nid = target_nid;
302        }
303
304        Err(io::Error::new(io::ErrorKind::NotFound, "path not found"))
305    }
306
307    /// Look up a named entry in a directory inode's data.
308    ///
309    /// EROFS directory data is organized as self-contained blocks. Each block
310    /// starts with a packed array of 12-byte dirent headers, followed by the
311    /// concatenated name strings. The first dirent's `nameoff` field divided
312    /// by 12 gives the number of dirents in that block (the kernel uses this
313    /// same trick). Name lengths are derived from consecutive `nameoff`
314    /// values; the last entry's name extends to the end of valid data.
315    fn lookup_in_dir(&mut self, dir_inode: &InodeInfo, name: &str) -> io::Result<u32> {
316        let blksiz = EROFS_BLKSIZ as usize;
317        let target = name.as_bytes();
318        let block_count = self.checked_inode_data_len(dir_inode)?.div_ceil(blksiz);
319        let mut left = 0usize;
320        let mut right = block_count;
321
322        while left < right {
323            let mid = (left + right) / 2;
324            let block = self.read_inode_data_block(dir_inode, mid)?;
325            let dirent_count = dir_block_dirent_count(&block)?;
326            let first_name = dirent_name(&block, 0, dirent_count)?;
327            let last_name = dirent_name(&block, dirent_count - 1, dirent_count)?;
328
329            if target < first_name {
330                right = mid;
331                continue;
332            }
333
334            if target > last_name {
335                left = mid + 1;
336                continue;
337            }
338
339            return lookup_in_dir_block(&block, dirent_count, target)?.ok_or_else(|| {
340                io::Error::new(
341                    io::ErrorKind::NotFound,
342                    format!("entry '{name}' not found in directory"),
343                )
344            });
345        }
346
347        Err(io::Error::new(
348            io::ErrorKind::NotFound,
349            format!("entry '{name}' not found in directory"),
350        ))
351    }
352
353    fn walk_dir(
354        &mut self,
355        dir_inode: &InodeInfo,
356        dir_path: PathBuf,
357        entries: &mut Vec<ErofsTreeEntry>,
358        visited: &mut HashSet<u32>,
359    ) -> io::Result<()> {
360        if !visited.insert(dir_inode.nid) {
361            return Err(io::Error::new(
362                io::ErrorKind::InvalidData,
363                "cycle detected while walking EROFS directory tree",
364            ));
365        }
366
367        self.visit_dir_entries::<io::Error, _>(dir_inode, &mut |reader, name, nid| {
368            if os_str_bytes(&name) == b"." || os_str_bytes(&name) == b".." {
369                return Ok(());
370            }
371
372            let path = dir_path.join(&name);
373            let inode = reader.read_inode(nid)?;
374            let entry = reader.tree_entry(path.clone(), &inode)?;
375            let is_dir = entry.kind == ErofsEntryKind::Directory;
376            entries.push(entry);
377
378            if is_dir {
379                reader.walk_dir(&inode, path, entries, visited)?;
380            }
381            Ok(())
382        })?;
383
384        Ok(())
385    }
386
387    fn walk_dir_entries<E, F>(
388        &mut self,
389        dir_inode: &InodeInfo,
390        dir_path: PathBuf,
391        visited: &mut HashSet<u32>,
392        visit: &mut F,
393    ) -> Result<(), E>
394    where
395        E: From<io::Error>,
396        F: FnMut(&mut Self, ErofsTreeEntry) -> Result<(), E>,
397    {
398        if !visited.insert(dir_inode.nid) {
399            return Err(io::Error::new(
400                io::ErrorKind::InvalidData,
401                "cycle detected while walking EROFS directory tree",
402            )
403            .into());
404        }
405
406        self.visit_dir_entries::<E, _>(dir_inode, &mut |reader, name, nid| {
407            if os_str_bytes(&name) == b"." || os_str_bytes(&name) == b".." {
408                return Ok(());
409            }
410
411            let path = dir_path.join(&name);
412            let inode = reader.read_inode(nid)?;
413            let entry = reader.tree_entry(path.clone(), &inode)?;
414            let is_dir = entry.kind == ErofsEntryKind::Directory;
415            visit(reader, entry)?;
416
417            if is_dir {
418                reader.walk_dir_entries(&inode, path, visited, visit)?;
419            }
420            Ok(())
421        })?;
422
423        Ok(())
424    }
425
426    fn visit_dir_entries<E, F>(&mut self, dir_inode: &InodeInfo, visit: &mut F) -> Result<(), E>
427    where
428        E: From<io::Error>,
429        F: FnMut(&mut Self, OsString, u32) -> Result<(), E>,
430    {
431        if (dir_inode.mode & S_IFMT) != S_IFDIR {
432            return Err(
433                io::Error::new(io::ErrorKind::InvalidInput, "target is not a directory").into(),
434            );
435        }
436
437        let blksiz = EROFS_BLKSIZ as usize;
438        let block_count = self.checked_inode_data_len(dir_inode)?.div_ceil(blksiz);
439
440        for block_index in 0..block_count {
441            let block = self.read_inode_data_block(dir_inode, block_index)?;
442            if block.is_empty() {
443                continue;
444            }
445            let dirent_count = dir_block_dirent_count(&block)?;
446            for idx in 0..dirent_count {
447                let name = dirent_name(&block, idx, dirent_count)?;
448                if name.is_empty() {
449                    continue;
450                }
451                visit(
452                    self,
453                    os_string_from_vec(name.to_vec())?,
454                    dirent_nid(&block, idx)?,
455                )?;
456            }
457        }
458
459        Ok(())
460    }
461
462    fn tree_entry(&mut self, path: PathBuf, inode: &InodeInfo) -> io::Result<ErofsTreeEntry> {
463        let kind = inode_kind(inode)?;
464        let rdev = if matches!(
465            kind,
466            ErofsEntryKind::CharDevice | ErofsEntryKind::BlockDevice
467        ) {
468            Some(decode_dev(inode.rdev))
469        } else {
470            None
471        };
472
473        Ok(ErofsTreeEntry {
474            path,
475            nid: inode.nid,
476            kind,
477            metadata: inode.metadata(),
478            xattrs: self
479                .read_inode_xattrs(inode)?
480                .into_iter()
481                .map(|(name, value)| Xattr { name, value })
482                .collect(),
483            size: inode.size,
484            rdev,
485        })
486    }
487
488    fn read_inode_data(&mut self, inode: &InodeInfo) -> io::Result<Vec<u8>> {
489        let size = self.checked_inode_data_len(inode)?;
490        if size == 0 {
491            return Ok(Vec::new());
492        }
493
494        let blksiz = EROFS_BLKSIZ as usize;
495
496        match inode.data_layout {
497            EROFS_INODE_FLAT_PLAIN => {
498                if inode.startblk_lo == EROFS_NULL_ADDR {
499                    return Ok(Vec::new());
500                }
501                let data_offset = (inode.startblk_lo as u64) * (EROFS_BLKSIZ as u64);
502                let mut data = vec![0u8; size];
503                read_exact_at(&self.file, data_offset, &mut data)?;
504                Ok(data)
505            }
506            EROFS_INODE_FLAT_INLINE => {
507                let full_blocks = size / blksiz;
508                let tail_size = size % blksiz;
509                let mut data = Vec::with_capacity(size);
510
511                // Read full blocks from data area.
512                if full_blocks > 0 && inode.startblk_lo != EROFS_NULL_ADDR {
513                    let data_offset = (inode.startblk_lo as u64) * (EROFS_BLKSIZ as u64);
514                    let mut block_data = vec![0u8; full_blocks * blksiz];
515                    read_exact_at(&self.file, data_offset, &mut block_data)?;
516                    data.extend_from_slice(&block_data);
517                }
518
519                // Read inline tail from after inode metadata.
520                if tail_size > 0 {
521                    let inline_offset = self.inode_offset(inode.nid)
522                        + EROFS_INODE_EXTENDED_SIZE as u64
523                        + inode.xattr_ibody_size as u64;
524                    let mut tail = vec![0u8; tail_size];
525                    read_exact_at(&self.file, inline_offset, &mut tail)?;
526                    data.extend_from_slice(&tail);
527                }
528
529                Ok(data)
530            }
531            _ => Err(io::Error::new(
532                io::ErrorKind::Unsupported,
533                format!("unsupported data layout: {}", inode.data_layout),
534            )),
535        }
536    }
537
538    fn read_inode_data_block(&self, inode: &InodeInfo, block_index: usize) -> io::Result<Vec<u8>> {
539        let blksiz = EROFS_BLKSIZ as usize;
540        let size = self.checked_inode_data_len(inode)?;
541        let start = block_index.checked_mul(blksiz).ok_or_else(|| {
542            io::Error::new(io::ErrorKind::InvalidData, "directory block overflow")
543        })?;
544        if start >= size {
545            return Ok(Vec::new());
546        }
547
548        let remaining = size - start;
549        let len = remaining.min(blksiz);
550        self.read_inode_data_range(inode, start as u64, len)
551    }
552
553    fn read_inode_data_range(
554        &self,
555        inode: &InodeInfo,
556        start: u64,
557        len: usize,
558    ) -> io::Result<Vec<u8>> {
559        let size = self.checked_inode_data_len(inode)? as u64;
560        let end = start
561            .checked_add(len as u64)
562            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "inode range overflow"))?;
563        if end > size {
564            return Err(io::Error::new(
565                io::ErrorKind::InvalidData,
566                "inode data range exceeds inode size",
567            ));
568        }
569
570        let segments = self.inode_data_segments(inode)?;
571        let mut data = vec![0u8; len];
572        let mut copied = 0usize;
573        let mut logical_start = 0u64;
574
575        for (file_offset, segment_len) in segments {
576            let logical_end = logical_start.checked_add(segment_len).ok_or_else(|| {
577                io::Error::new(io::ErrorKind::InvalidData, "inode segment range overflow")
578            })?;
579            let overlap_start = start.max(logical_start);
580            let overlap_end = end.min(logical_end);
581
582            if overlap_start < overlap_end {
583                let dst_start = (overlap_start - start) as usize;
584                let read_len = (overlap_end - overlap_start) as usize;
585                let source_offset = file_offset
586                    .checked_add(overlap_start - logical_start)
587                    .ok_or_else(|| {
588                        io::Error::new(io::ErrorKind::InvalidData, "inode file offset overflow")
589                    })?;
590                read_exact_at(
591                    &self.file,
592                    source_offset,
593                    &mut data[dst_start..dst_start + read_len],
594                )?;
595                copied += read_len;
596            }
597
598            logical_start = logical_end;
599            if logical_start >= end {
600                break;
601            }
602        }
603
604        if copied != len {
605            return Err(io::Error::new(
606                io::ErrorKind::UnexpectedEof,
607                "inode data range is not fully backed",
608            ));
609        }
610
611        Ok(data)
612    }
613
614    fn checked_inode_data_len(&self, inode: &InodeInfo) -> io::Result<usize> {
615        let file_len = self.file.metadata()?.len();
616        if inode.size > file_len {
617            return Err(io::Error::new(
618                io::ErrorKind::InvalidData,
619                "inode data size exceeds EROFS image size",
620            ));
621        }
622
623        usize::try_from(inode.size).map_err(|_| {
624            io::Error::new(
625                io::ErrorKind::InvalidData,
626                "inode data size does not fit in memory",
627            )
628        })
629    }
630
631    fn inode_data_segments(&self, inode: &InodeInfo) -> io::Result<Vec<(u64, u64)>> {
632        let size = inode.size;
633        if size == 0 {
634            return Ok(Vec::new());
635        }
636
637        let blksiz = EROFS_BLKSIZ as u64;
638        match inode.data_layout {
639            EROFS_INODE_FLAT_PLAIN => {
640                if inode.startblk_lo == EROFS_NULL_ADDR {
641                    Ok(Vec::new())
642                } else {
643                    Ok(vec![((inode.startblk_lo as u64) * blksiz, size)])
644                }
645            }
646            EROFS_INODE_FLAT_INLINE => {
647                let full_blocks = size / blksiz;
648                let tail_size = size % blksiz;
649                let mut segments = Vec::new();
650                if full_blocks > 0 && inode.startblk_lo != EROFS_NULL_ADDR {
651                    segments.push(((inode.startblk_lo as u64) * blksiz, full_blocks * blksiz));
652                }
653                if tail_size > 0 {
654                    segments.push((
655                        self.inode_offset(inode.nid)
656                            + EROFS_INODE_EXTENDED_SIZE as u64
657                            + inode.xattr_ibody_size as u64,
658                        tail_size,
659                    ));
660                }
661                Ok(segments)
662            }
663            _ => Err(io::Error::new(
664                io::ErrorKind::Unsupported,
665                format!("unsupported data layout: {}", inode.data_layout),
666            )),
667        }
668    }
669
670    fn inode_is_opaque(&mut self, inode: &InodeInfo) -> io::Result<bool> {
671        for (name, value) in self.read_inode_xattrs(inode)? {
672            if name == b"trusted.overlay.opaque" && value == b"y" {
673                return Ok(true);
674            }
675        }
676
677        Ok(false)
678    }
679
680    fn read_inode_xattrs(&mut self, inode: &InodeInfo) -> io::Result<Vec<(Vec<u8>, Vec<u8>)>> {
681        if inode.xattr_ibody_size == 0 {
682            return Ok(Vec::new());
683        }
684
685        let total = inode.xattr_ibody_size as usize;
686        if total < EROFS_XATTR_IBODY_HEADER_SIZE as usize {
687            return Err(io::Error::new(
688                io::ErrorKind::InvalidData,
689                "xattr ibody smaller than header",
690            ));
691        }
692
693        let mut offset = self.inode_offset(inode.nid)
694            + EROFS_INODE_EXTENDED_SIZE as u64
695            + EROFS_XATTR_IBODY_HEADER_SIZE as u64;
696        let mut remaining = total - EROFS_XATTR_IBODY_HEADER_SIZE as usize;
697        let mut xattrs = Vec::new();
698
699        while remaining > 0 {
700            if remaining < 4 {
701                return Err(io::Error::new(
702                    io::ErrorKind::InvalidData,
703                    "truncated xattr entry header",
704                ));
705            }
706
707            let mut entry = [0u8; 4];
708            read_exact_at(&self.file, offset, &mut entry)?;
709
710            let name_len = entry[0] as usize;
711            let name_index = entry[1];
712            let value_len = u16::from_le_bytes([entry[2], entry[3]]) as usize;
713            let entry_size = 4 + name_len + value_len;
714            let aligned_size = erofs_xattr_align(entry_size);
715
716            if aligned_size > remaining {
717                return Err(io::Error::new(
718                    io::ErrorKind::InvalidData,
719                    "xattr entry exceeds ibody size",
720                ));
721            }
722
723            let mut suffix = vec![0u8; name_len];
724            read_exact_at(&self.file, offset + 4, &mut suffix)?;
725            let mut value = vec![0u8; value_len];
726            read_exact_at(&self.file, offset + 4 + name_len as u64, &mut value)?;
727
728            let name = match name_index {
729                EROFS_XATTR_INDEX_USER => [b"user.".as_slice(), suffix.as_slice()].concat(),
730                EROFS_XATTR_INDEX_TRUSTED => [b"trusted.".as_slice(), suffix.as_slice()].concat(),
731                EROFS_XATTR_INDEX_SECURITY => [b"security.".as_slice(), suffix.as_slice()].concat(),
732                other => {
733                    return Err(io::Error::new(
734                        io::ErrorKind::InvalidData,
735                        format!("unsupported xattr name index: {other}"),
736                    ));
737                }
738            };
739
740            xattrs.push((name, value));
741            offset += aligned_size as u64;
742            remaining -= aligned_size;
743        }
744
745        Ok(xattrs)
746    }
747}
748
749//--------------------------------------------------------------------------------------------------
750// Types: Internal
751//--------------------------------------------------------------------------------------------------
752
753struct InodeInfo {
754    nid: u32,
755    mode: u16,
756    size: u64,
757    #[allow(dead_code)]
758    nlink: u32,
759    uid: u32,
760    gid: u32,
761    mtime: u64,
762    mtime_nsec: u32,
763    data_layout: u8,
764    startblk_lo: u32,
765    rdev: u32,
766    xattr_ibody_size: u32,
767}
768
769impl InodeInfo {
770    fn metadata(&self) -> InodeMetadata {
771        InodeMetadata {
772            uid: self.uid,
773            gid: self.gid,
774            mode: self.mode,
775            mtime: self.mtime,
776            mtime_nsec: self.mtime_nsec,
777        }
778    }
779}
780
781impl ErofsTreeEntry {
782    /// Return true if this directory carries the overlay opaque marker.
783    pub fn is_opaque(&self) -> bool {
784        self.xattrs
785            .iter()
786            .any(|x| x.name == b"trusted.overlay.opaque" && x.value == b"y")
787    }
788}
789
790impl Read for ErofsFileDataReader {
791    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
792        if buf.is_empty() {
793            return Ok(0);
794        }
795
796        while self.segment_index < self.segments.len() {
797            let (offset, len) = self.segments[self.segment_index];
798            if self.segment_offset >= len {
799                self.segment_index += 1;
800                self.segment_offset = 0;
801                continue;
802            }
803
804            let remaining = (len - self.segment_offset) as usize;
805            let to_read = remaining.min(buf.len());
806            let read = read_at_file(
807                &self.file,
808                &mut buf[..to_read],
809                offset + self.segment_offset,
810            )?;
811            self.segment_offset += read as u64;
812            return Ok(read);
813        }
814
815        Ok(0)
816    }
817}
818
819//--------------------------------------------------------------------------------------------------
820// Functions
821//--------------------------------------------------------------------------------------------------
822
823fn read_exact_at(file: &File, offset: u64, mut buf: &mut [u8]) -> io::Result<()> {
824    let mut current_offset = offset;
825    while !buf.is_empty() {
826        let read = read_at_file(file, buf, current_offset)?;
827        if read == 0 {
828            return Err(io::Error::new(
829                io::ErrorKind::UnexpectedEof,
830                "unexpected EOF",
831            ));
832        }
833        current_offset += read as u64;
834        buf = &mut buf[read..];
835    }
836
837    Ok(())
838}
839
840#[cfg(unix)]
841fn read_at_file(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
842    use std::os::unix::fs::FileExt;
843
844    file.read_at(buf, offset)
845}
846
847#[cfg(windows)]
848fn read_at_file(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
849    use std::os::windows::fs::FileExt;
850
851    file.seek_read(buf, offset)
852}
853
854fn dir_block_dirent_count(block: &[u8]) -> io::Result<usize> {
855    if block.len() < EROFS_DIRENT_SIZE as usize {
856        return Err(io::Error::new(
857            io::ErrorKind::InvalidData,
858            "directory block smaller than one dirent",
859        ));
860    }
861
862    let first_nameoff = u16::from_le_bytes([block[8], block[9]]) as usize;
863    let dirent_size = EROFS_DIRENT_SIZE as usize;
864    if first_nameoff < dirent_size
865        || !first_nameoff.is_multiple_of(dirent_size)
866        || first_nameoff > block.len()
867    {
868        return Err(io::Error::new(
869            io::ErrorKind::InvalidData,
870            "invalid first dirent name offset",
871        ));
872    }
873
874    Ok(first_nameoff / dirent_size)
875}
876
877fn dirent_name(block: &[u8], idx: usize, dirent_count: usize) -> io::Result<&[u8]> {
878    let dirent_size = EROFS_DIRENT_SIZE as usize;
879    let dirent_off = idx
880        .checked_mul(dirent_size)
881        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "dirent offset overflow"))?;
882
883    if idx >= dirent_count || dirent_off + dirent_size > block.len() {
884        return Err(io::Error::new(
885            io::ErrorKind::InvalidData,
886            "dirent index out of bounds",
887        ));
888    }
889
890    let nameoff = u16::from_le_bytes([block[dirent_off + 8], block[dirent_off + 9]]) as usize;
891    let mut name_end = if idx + 1 < dirent_count {
892        let next_off = dirent_off + dirent_size;
893        u16::from_le_bytes([block[next_off + 8], block[next_off + 9]]) as usize
894    } else {
895        block.len()
896    };
897
898    if nameoff > name_end || name_end > block.len() {
899        return Err(io::Error::new(
900            io::ErrorKind::InvalidData,
901            "dirent name range out of bounds",
902        ));
903    }
904
905    while name_end > nameoff && block[name_end - 1] == 0 {
906        name_end -= 1;
907    }
908
909    Ok(&block[nameoff..name_end])
910}
911
912fn dirent_nid(block: &[u8], idx: usize) -> io::Result<u32> {
913    let dirent_size = EROFS_DIRENT_SIZE as usize;
914    let dirent_off = idx
915        .checked_mul(dirent_size)
916        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "dirent offset overflow"))?;
917    if dirent_off + dirent_size > block.len() {
918        return Err(io::Error::new(
919            io::ErrorKind::InvalidData,
920            "dirent NID out of bounds",
921        ));
922    }
923
924    let nid = u64::from_le_bytes([
925        block[dirent_off],
926        block[dirent_off + 1],
927        block[dirent_off + 2],
928        block[dirent_off + 3],
929        block[dirent_off + 4],
930        block[dirent_off + 5],
931        block[dirent_off + 6],
932        block[dirent_off + 7],
933    ]);
934    u32::try_from(nid)
935        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "dirent NID overflow"))
936}
937
938fn lookup_in_dir_block(
939    block: &[u8],
940    dirent_count: usize,
941    target: &[u8],
942) -> io::Result<Option<u32>> {
943    let mut left = 0usize;
944    let mut right = dirent_count;
945
946    while left < right {
947        let mid = (left + right) / 2;
948        match target.cmp(dirent_name(block, mid, dirent_count)?) {
949            std::cmp::Ordering::Less => right = mid,
950            std::cmp::Ordering::Greater => left = mid + 1,
951            std::cmp::Ordering::Equal => return dirent_nid(block, mid).map(Some),
952        }
953    }
954
955    Ok(None)
956}
957
958fn inode_kind(inode: &InodeInfo) -> io::Result<ErofsEntryKind> {
959    match inode.mode & S_IFMT {
960        S_IFREG => Ok(ErofsEntryKind::RegularFile),
961        S_IFDIR => Ok(ErofsEntryKind::Directory),
962        S_IFLNK => Ok(ErofsEntryKind::Symlink),
963        S_IFCHR => Ok(ErofsEntryKind::CharDevice),
964        S_IFBLK => Ok(ErofsEntryKind::BlockDevice),
965        S_IFIFO => Ok(ErofsEntryKind::Fifo),
966        S_IFSOCK => Ok(ErofsEntryKind::Socket),
967        other => Err(io::Error::new(
968            io::ErrorKind::InvalidData,
969            format!("unsupported inode mode type: {other:#o}"),
970        )),
971    }
972}
973
974fn decode_dev(encoded: u32) -> (u32, u32) {
975    let major = (encoded >> 8) & 0x0000_0fff;
976    let minor = (encoded & 0x0000_00ff) | ((encoded >> 12) & 0xffff_ff00);
977    (major, minor)
978}
979
980/// Read a file from an EROFS image file on disk.
981pub fn read_file_from_erofs(image_path: &Path, file_path: &str) -> io::Result<Vec<u8>> {
982    let file = std::fs::File::open(image_path)?;
983    let mut reader = ErofsReader::new(file)?;
984    reader.read_file(file_path)
985}
986
987pub fn entry_info_from_erofs(image_path: &Path, file_path: &str) -> io::Result<ErofsEntryInfo> {
988    let file = std::fs::File::open(image_path)?;
989    let mut reader = ErofsReader::new(file)?;
990    reader.entry_info(file_path)
991}
992
993//--------------------------------------------------------------------------------------------------
994// Tests
995//--------------------------------------------------------------------------------------------------
996
997#[cfg(test)]
998mod tests {
999    use std::{fs::File, io, path::PathBuf};
1000
1001    use tempfile::tempdir;
1002
1003    use super::ErofsReader;
1004    use crate::{
1005        erofs::write_erofs,
1006        tree::{FileData, FileTree, InodeMetadata, RegularFileId, RegularFileNode, TreeNode},
1007    };
1008
1009    fn make_regular_file(data: &[u8]) -> TreeNode {
1010        make_regular_file_with_id(data, RegularFileId::new())
1011    }
1012
1013    fn make_regular_file_with_id(data: &[u8], id: RegularFileId) -> TreeNode {
1014        TreeNode::RegularFile(RegularFileNode {
1015            id,
1016            metadata: InodeMetadata::default(),
1017            xattrs: Vec::new(),
1018            data: FileData::Memory(data.to_vec()),
1019            nlink: 1,
1020        })
1021    }
1022
1023    #[test]
1024    fn lookup_path_resolves_large_multi_block_directory() {
1025        let mut tree = FileTree::new();
1026        for i in 0..5000 {
1027            let path = format!("dir/file-{i:04}.txt");
1028            tree.insert(path.as_bytes(), make_regular_file(b"x"))
1029                .expect("insert file");
1030        }
1031
1032        let output_dir = tempdir().expect("tempdir");
1033        let output = output_dir.path().join("large-dir.erofs");
1034        write_erofs(&tree, &output).expect("write erofs");
1035
1036        let file = File::open(&output).expect("open erofs");
1037        let mut reader = ErofsReader::new(file).expect("reader");
1038
1039        assert_eq!(reader.read_file("/dir/file-0000.txt").expect("first"), b"x");
1040        assert_eq!(
1041            reader.read_file("/dir/file-2500.txt").expect("middle"),
1042            b"x"
1043        );
1044        assert_eq!(reader.read_file("/dir/file-4999.txt").expect("last"), b"x");
1045
1046        let err = reader
1047            .entry_info("/dir/file-9999.txt")
1048            .expect_err("missing entry should fail");
1049        assert_eq!(err.kind(), io::ErrorKind::NotFound);
1050    }
1051
1052    #[test]
1053    fn hardlinked_regular_files_share_inode_and_data_blocks() {
1054        let mut tree = FileTree::new();
1055        let file_id = RegularFileId::new();
1056
1057        tree.insert(b"alpha", make_regular_file_with_id(b"shared", file_id))
1058            .expect("insert alpha");
1059        tree.insert(b"beta", make_regular_file_with_id(b"shared", file_id))
1060            .expect("insert beta");
1061
1062        let output_dir = tempdir().expect("tempdir");
1063        let output = output_dir.path().join("hardlinks.erofs");
1064        let data_map = write_erofs(&tree, &output).expect("write erofs");
1065        let alpha_path = PathBuf::from("alpha");
1066        let beta_path = PathBuf::from("beta");
1067
1068        assert_eq!(
1069            data_map
1070                .file_blocks
1071                .get(&alpha_path)
1072                .copied()
1073                .expect("alpha data map"),
1074            data_map
1075                .file_blocks
1076                .get(&beta_path)
1077                .copied()
1078                .expect("beta data map")
1079        );
1080
1081        let file = File::open(&output).expect("open erofs");
1082        let mut reader = ErofsReader::new(file).expect("reader");
1083        let alpha = reader.inode_debug_info("/alpha").expect("alpha inode");
1084        let beta = reader.inode_debug_info("/beta").expect("beta inode");
1085
1086        assert_eq!(alpha.nid, beta.nid);
1087        assert_eq!(alpha.nlink, 2);
1088        assert_eq!(beta.nlink, 2);
1089        assert_eq!(alpha.size, b"shared".len() as u64);
1090        assert_eq!(reader.read_file("/alpha").expect("read alpha"), b"shared");
1091        assert_eq!(reader.read_file("/beta").expect("read beta"), b"shared");
1092    }
1093}