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::{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_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, Vec::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        self.walk_entries_with_path_bytes(|reader, _path, entry| visit(reader, entry))
173    }
174
175    /// Walk all entries while retaining canonical guest path bytes for internal image pipelines.
176    pub(crate) fn walk_entries_with_path_bytes<E, F>(&mut self, mut visit: F) -> Result<(), E>
177    where
178        E: From<io::Error>,
179        F: FnMut(&mut Self, &[u8], ErofsTreeEntry) -> Result<(), E>,
180    {
181        let root = self.read_inode(self.root_nid)?;
182        let mut visited = HashSet::new();
183        self.walk_dir_entries(&root, Vec::new(), &mut visited, &mut visit)
184    }
185
186    /// Create a streaming reader for a regular file inode by NID.
187    pub fn file_data_reader(&mut self, nid: u32) -> io::Result<ErofsFileDataReader> {
188        let inode = self.read_inode(nid)?;
189        if (inode.mode & S_IFMT) != S_IFREG {
190            return Err(io::Error::new(
191                io::ErrorKind::InvalidInput,
192                "target is not a regular file",
193            ));
194        }
195
196        Ok(ErofsFileDataReader {
197            file: self.file.try_clone()?,
198            segments: self.inode_data_segments(&inode)?,
199            segment_index: 0,
200            segment_offset: 0,
201        })
202    }
203
204    /// Return the block mapping recorded for a regular file in an image produced by our writer.
205    ///
206    /// Regular files are deliberately emitted as `FLAT_PLAIN`, which lets fsmeta be rebuilt from
207    /// a cached EROFS layer without retaining or re-downloading its source tarball.
208    pub(crate) fn file_block_mapping(&mut self, nid: u32) -> io::Result<(u32, u64)> {
209        let inode = self.read_inode(nid)?;
210        if (inode.mode & S_IFMT) != S_IFREG {
211            return Err(io::Error::new(
212                io::ErrorKind::InvalidInput,
213                "block mapping is available only for regular files",
214            ));
215        }
216        if inode.size == 0 {
217            return Ok((EROFS_NULL_ADDR, 0));
218        }
219        if inode.data_layout != EROFS_INODE_FLAT_PLAIN || inode.startblk_lo == EROFS_NULL_ADDR {
220            return Err(io::Error::new(
221                io::ErrorKind::InvalidData,
222                "regular file does not use the expected flat-plain EROFS layout",
223            ));
224        }
225        Ok((inode.startblk_lo, inode.size))
226    }
227
228    /// Return metadata and xattrs for the filesystem root directory.
229    pub(crate) fn root_directory_metadata(&mut self) -> io::Result<(InodeMetadata, Vec<Xattr>)> {
230        let inode = self.read_inode(self.root_nid)?;
231        if (inode.mode & S_IFMT) != S_IFDIR {
232            return Err(io::Error::new(
233                io::ErrorKind::InvalidData,
234                "EROFS root inode is not a directory",
235            ));
236        }
237        let xattrs = self
238            .read_inode_xattrs(&inode)?
239            .into_iter()
240            .map(|(name, value)| Xattr { name, value })
241            .collect();
242        Ok((inode.metadata(), xattrs))
243    }
244
245    /// Read a symlink target by NID.
246    pub fn read_link_by_nid(&mut self, nid: u32) -> io::Result<Vec<u8>> {
247        let inode = self.read_inode(nid)?;
248        if (inode.mode & S_IFMT) != S_IFLNK {
249            return Err(io::Error::new(
250                io::ErrorKind::InvalidInput,
251                "target is not a symlink",
252            ));
253        }
254        self.read_inode_data(&inode)
255    }
256
257    #[cfg(test)]
258    pub(crate) fn inode_debug_info(&mut self, path: &str) -> io::Result<ErofsInodeDebugInfo> {
259        let inode = self.lookup_path(path)?;
260        Ok(ErofsInodeDebugInfo {
261            nid: inode.nid,
262            nlink: inode.nlink,
263            size: inode.size,
264            data_layout: inode.data_layout,
265        })
266    }
267
268    fn inode_offset(&self, nid: u32) -> u64 {
269        (self.meta_blkaddr as u64) * (EROFS_BLKSIZ as u64) + (nid as u64) * 32
270    }
271
272    fn read_inode(&mut self, nid: u32) -> io::Result<InodeInfo> {
273        let offset = self.inode_offset(nid);
274
275        let mut buf = [0u8; EROFS_INODE_EXTENDED_SIZE as usize];
276        read_exact_at(&self.file, offset, &mut buf)?;
277
278        let i_format = u16::from_le_bytes([buf[0], buf[1]]);
279        let i_xattr_icount = u16::from_le_bytes([buf[2], buf[3]]);
280        let mode = u16::from_le_bytes([buf[4], buf[5]]);
281        let size = u64::from_le_bytes([
282            buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
283        ]);
284        let i_u = u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]);
285        let nlink = u32::from_le_bytes([buf[44], buf[45], buf[46], buf[47]]);
286        let uid = u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]);
287        let gid = u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]);
288        let mtime = u64::from_le_bytes([
289            buf[32], buf[33], buf[34], buf[35], buf[36], buf[37], buf[38], buf[39],
290        ]);
291        let mtime_nsec = u32::from_le_bytes([buf[40], buf[41], buf[42], buf[43]]);
292
293        let data_layout = ((i_format >> 1) & 0x07) as u8;
294
295        // Compute xattr ibody size to know where inline data starts.
296        // Formula from EROFS spec: ibody = 12-byte header + (i_xattr_icount - 1) * 4 bytes.
297        // The "- 1" accounts for the header occupying the first count unit.
298        let xattr_ibody_size = if i_xattr_icount == 0 {
299            0u32
300        } else {
301            12 + ((i_xattr_icount as u32) - 1) * 4
302        };
303
304        Ok(InodeInfo {
305            nid,
306            mode,
307            size,
308            nlink,
309            uid,
310            gid,
311            mtime,
312            mtime_nsec,
313            data_layout,
314            startblk_lo: i_u,
315            rdev: i_u,
316            xattr_ibody_size,
317        })
318    }
319
320    fn lookup_path(&mut self, path: &str) -> io::Result<InodeInfo> {
321        let components: Vec<&str> = path
322            .trim_start_matches('/')
323            .split('/')
324            .filter(|c| !c.is_empty())
325            .collect();
326
327        if components.is_empty() {
328            if path == "/" {
329                return self.read_inode(self.root_nid);
330            }
331            return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty path"));
332        }
333
334        let mut current_nid = self.root_nid;
335        for (i, component) in components.iter().enumerate() {
336            let inode = self.read_inode(current_nid)?;
337            let mode_type = inode.mode & S_IFMT;
338
339            if mode_type != S_IFDIR {
340                return Err(io::Error::new(
341                    io::ErrorKind::NotFound,
342                    format!("not a directory at component '{component}'"),
343                ));
344            }
345
346            let target_nid = self.lookup_in_dir(&inode, component)?;
347            if i + 1 == components.len() {
348                return self.read_inode(target_nid);
349            }
350
351            current_nid = target_nid;
352        }
353
354        Err(io::Error::new(io::ErrorKind::NotFound, "path not found"))
355    }
356
357    /// Look up a named entry in a directory inode's data.
358    ///
359    /// EROFS directory data is organized as self-contained blocks. Each block
360    /// starts with a packed array of 12-byte dirent headers, followed by the
361    /// concatenated name strings. The first dirent's `nameoff` field divided
362    /// by 12 gives the number of dirents in that block (the kernel uses this
363    /// same trick). Name lengths are derived from consecutive `nameoff`
364    /// values; the last entry's name extends to the end of valid data.
365    fn lookup_in_dir(&mut self, dir_inode: &InodeInfo, name: &str) -> io::Result<u32> {
366        let blksiz = EROFS_BLKSIZ as usize;
367        let target = name.as_bytes();
368        let block_count = self.checked_inode_data_len(dir_inode)?.div_ceil(blksiz);
369        let mut left = 0usize;
370        let mut right = block_count;
371
372        while left < right {
373            let mid = (left + right) / 2;
374            let block = self.read_inode_data_block(dir_inode, mid)?;
375            let dirent_count = dir_block_dirent_count(&block)?;
376            let first_name = dirent_name(&block, 0, dirent_count)?;
377            let last_name = dirent_name(&block, dirent_count - 1, dirent_count)?;
378
379            if target < first_name {
380                right = mid;
381                continue;
382            }
383
384            if target > last_name {
385                left = mid + 1;
386                continue;
387            }
388
389            return lookup_in_dir_block(&block, dirent_count, target)?.ok_or_else(|| {
390                io::Error::new(
391                    io::ErrorKind::NotFound,
392                    format!("entry '{name}' not found in directory"),
393                )
394            });
395        }
396
397        Err(io::Error::new(
398            io::ErrorKind::NotFound,
399            format!("entry '{name}' not found in directory"),
400        ))
401    }
402
403    fn walk_dir(
404        &mut self,
405        dir_inode: &InodeInfo,
406        dir_path: Vec<u8>,
407        entries: &mut Vec<ErofsTreeEntry>,
408        visited: &mut HashSet<u32>,
409    ) -> io::Result<()> {
410        if !visited.insert(dir_inode.nid) {
411            return Err(io::Error::new(
412                io::ErrorKind::InvalidData,
413                "cycle detected while walking EROFS directory tree",
414            ));
415        }
416
417        self.visit_dir_entries::<io::Error, _>(dir_inode, &mut |reader, name, nid| {
418            if name == b"." || name == b".." {
419                return Ok(());
420            }
421
422            let path = join_image_path(&dir_path, name)?;
423            let inode = reader.read_inode(nid)?;
424            let entry = reader.tree_entry(path.clone(), &inode)?;
425            let is_dir = entry.kind == ErofsEntryKind::Directory;
426            entries.push(entry);
427
428            if is_dir {
429                reader.walk_dir(&inode, path, entries, visited)?;
430            }
431            Ok(())
432        })?;
433
434        Ok(())
435    }
436
437    fn walk_dir_entries<E, F>(
438        &mut self,
439        dir_inode: &InodeInfo,
440        dir_path: Vec<u8>,
441        visited: &mut HashSet<u32>,
442        visit: &mut F,
443    ) -> Result<(), E>
444    where
445        E: From<io::Error>,
446        F: FnMut(&mut Self, &[u8], ErofsTreeEntry) -> Result<(), E>,
447    {
448        if !visited.insert(dir_inode.nid) {
449            return Err(io::Error::new(
450                io::ErrorKind::InvalidData,
451                "cycle detected while walking EROFS directory tree",
452            )
453            .into());
454        }
455
456        self.visit_dir_entries::<E, _>(dir_inode, &mut |reader, name, nid| {
457            if name == b"." || name == b".." {
458                return Ok(());
459            }
460
461            let path = join_image_path(&dir_path, name)?;
462            let inode = reader.read_inode(nid)?;
463            let entry = reader.tree_entry(path.clone(), &inode)?;
464            let is_dir = entry.kind == ErofsEntryKind::Directory;
465            visit(reader, &path, entry)?;
466
467            if is_dir {
468                reader.walk_dir_entries(&inode, path, visited, visit)?;
469            }
470            Ok(())
471        })?;
472
473        Ok(())
474    }
475
476    fn visit_dir_entries<E, F>(&mut self, dir_inode: &InodeInfo, visit: &mut F) -> Result<(), E>
477    where
478        E: From<io::Error>,
479        F: FnMut(&mut Self, &[u8], u32) -> Result<(), E>,
480    {
481        if (dir_inode.mode & S_IFMT) != S_IFDIR {
482            return Err(
483                io::Error::new(io::ErrorKind::InvalidInput, "target is not a directory").into(),
484            );
485        }
486
487        let blksiz = EROFS_BLKSIZ as usize;
488        let block_count = self.checked_inode_data_len(dir_inode)?.div_ceil(blksiz);
489
490        for block_index in 0..block_count {
491            let block = self.read_inode_data_block(dir_inode, block_index)?;
492            if block.is_empty() {
493                continue;
494            }
495            let dirent_count = dir_block_dirent_count(&block)?;
496            for idx in 0..dirent_count {
497                let name = dirent_name(&block, idx, dirent_count)?;
498                if name.is_empty() {
499                    continue;
500                }
501                visit(self, name, dirent_nid(&block, idx)?)?;
502            }
503        }
504
505        Ok(())
506    }
507
508    fn tree_entry(&mut self, path: Vec<u8>, inode: &InodeInfo) -> io::Result<ErofsTreeEntry> {
509        let kind = inode_kind(inode)?;
510        let rdev = if matches!(
511            kind,
512            ErofsEntryKind::CharDevice | ErofsEntryKind::BlockDevice
513        ) {
514            Some(decode_dev(inode.rdev))
515        } else {
516            None
517        };
518
519        Ok(ErofsTreeEntry {
520            path: PathBuf::from(os_string_from_vec(path)?),
521            nid: inode.nid,
522            kind,
523            metadata: inode.metadata(),
524            xattrs: self
525                .read_inode_xattrs(inode)?
526                .into_iter()
527                .map(|(name, value)| Xattr { name, value })
528                .collect(),
529            size: inode.size,
530            rdev,
531        })
532    }
533
534    fn read_inode_data(&mut self, inode: &InodeInfo) -> io::Result<Vec<u8>> {
535        let size = self.checked_inode_data_len(inode)?;
536        if size == 0 {
537            return Ok(Vec::new());
538        }
539
540        let blksiz = EROFS_BLKSIZ as usize;
541
542        match inode.data_layout {
543            EROFS_INODE_FLAT_PLAIN => {
544                if inode.startblk_lo == EROFS_NULL_ADDR {
545                    return Ok(Vec::new());
546                }
547                let data_offset = (inode.startblk_lo as u64) * (EROFS_BLKSIZ as u64);
548                let mut data = vec![0u8; size];
549                read_exact_at(&self.file, data_offset, &mut data)?;
550                Ok(data)
551            }
552            EROFS_INODE_FLAT_INLINE => {
553                let full_blocks = size / blksiz;
554                let tail_size = size % blksiz;
555                let mut data = Vec::with_capacity(size);
556
557                // Read full blocks from data area.
558                if full_blocks > 0 && inode.startblk_lo != EROFS_NULL_ADDR {
559                    let data_offset = (inode.startblk_lo as u64) * (EROFS_BLKSIZ as u64);
560                    let mut block_data = vec![0u8; full_blocks * blksiz];
561                    read_exact_at(&self.file, data_offset, &mut block_data)?;
562                    data.extend_from_slice(&block_data);
563                }
564
565                // Read inline tail from after inode metadata.
566                if tail_size > 0 {
567                    let inline_offset = self.inode_offset(inode.nid)
568                        + EROFS_INODE_EXTENDED_SIZE as u64
569                        + inode.xattr_ibody_size as u64;
570                    let mut tail = vec![0u8; tail_size];
571                    read_exact_at(&self.file, inline_offset, &mut tail)?;
572                    data.extend_from_slice(&tail);
573                }
574
575                Ok(data)
576            }
577            _ => Err(io::Error::new(
578                io::ErrorKind::Unsupported,
579                format!("unsupported data layout: {}", inode.data_layout),
580            )),
581        }
582    }
583
584    fn read_inode_data_block(&self, inode: &InodeInfo, block_index: usize) -> io::Result<Vec<u8>> {
585        let blksiz = EROFS_BLKSIZ as usize;
586        let size = self.checked_inode_data_len(inode)?;
587        let start = block_index.checked_mul(blksiz).ok_or_else(|| {
588            io::Error::new(io::ErrorKind::InvalidData, "directory block overflow")
589        })?;
590        if start >= size {
591            return Ok(Vec::new());
592        }
593
594        let remaining = size - start;
595        let len = remaining.min(blksiz);
596        self.read_inode_data_range(inode, start as u64, len)
597    }
598
599    fn read_inode_data_range(
600        &self,
601        inode: &InodeInfo,
602        start: u64,
603        len: usize,
604    ) -> io::Result<Vec<u8>> {
605        let size = self.checked_inode_data_len(inode)? as u64;
606        let end = start
607            .checked_add(len as u64)
608            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "inode range overflow"))?;
609        if end > size {
610            return Err(io::Error::new(
611                io::ErrorKind::InvalidData,
612                "inode data range exceeds inode size",
613            ));
614        }
615
616        let segments = self.inode_data_segments(inode)?;
617        let mut data = vec![0u8; len];
618        let mut copied = 0usize;
619        let mut logical_start = 0u64;
620
621        for (file_offset, segment_len) in segments {
622            let logical_end = logical_start.checked_add(segment_len).ok_or_else(|| {
623                io::Error::new(io::ErrorKind::InvalidData, "inode segment range overflow")
624            })?;
625            let overlap_start = start.max(logical_start);
626            let overlap_end = end.min(logical_end);
627
628            if overlap_start < overlap_end {
629                let dst_start = (overlap_start - start) as usize;
630                let read_len = (overlap_end - overlap_start) as usize;
631                let source_offset = file_offset
632                    .checked_add(overlap_start - logical_start)
633                    .ok_or_else(|| {
634                        io::Error::new(io::ErrorKind::InvalidData, "inode file offset overflow")
635                    })?;
636                read_exact_at(
637                    &self.file,
638                    source_offset,
639                    &mut data[dst_start..dst_start + read_len],
640                )?;
641                copied += read_len;
642            }
643
644            logical_start = logical_end;
645            if logical_start >= end {
646                break;
647            }
648        }
649
650        if copied != len {
651            return Err(io::Error::new(
652                io::ErrorKind::UnexpectedEof,
653                "inode data range is not fully backed",
654            ));
655        }
656
657        Ok(data)
658    }
659
660    fn checked_inode_data_len(&self, inode: &InodeInfo) -> io::Result<usize> {
661        let file_len = self.file.metadata()?.len();
662        if inode.size > file_len {
663            return Err(io::Error::new(
664                io::ErrorKind::InvalidData,
665                "inode data size exceeds EROFS image size",
666            ));
667        }
668
669        usize::try_from(inode.size).map_err(|_| {
670            io::Error::new(
671                io::ErrorKind::InvalidData,
672                "inode data size does not fit in memory",
673            )
674        })
675    }
676
677    fn inode_data_segments(&self, inode: &InodeInfo) -> io::Result<Vec<(u64, u64)>> {
678        let size = inode.size;
679        if size == 0 {
680            return Ok(Vec::new());
681        }
682
683        let blksiz = EROFS_BLKSIZ as u64;
684        match inode.data_layout {
685            EROFS_INODE_FLAT_PLAIN => {
686                if inode.startblk_lo == EROFS_NULL_ADDR {
687                    Ok(Vec::new())
688                } else {
689                    Ok(vec![((inode.startblk_lo as u64) * blksiz, size)])
690                }
691            }
692            EROFS_INODE_FLAT_INLINE => {
693                let full_blocks = size / blksiz;
694                let tail_size = size % blksiz;
695                let mut segments = Vec::new();
696                if full_blocks > 0 && inode.startblk_lo != EROFS_NULL_ADDR {
697                    segments.push(((inode.startblk_lo as u64) * blksiz, full_blocks * blksiz));
698                }
699                if tail_size > 0 {
700                    segments.push((
701                        self.inode_offset(inode.nid)
702                            + EROFS_INODE_EXTENDED_SIZE as u64
703                            + inode.xattr_ibody_size as u64,
704                        tail_size,
705                    ));
706                }
707                Ok(segments)
708            }
709            _ => Err(io::Error::new(
710                io::ErrorKind::Unsupported,
711                format!("unsupported data layout: {}", inode.data_layout),
712            )),
713        }
714    }
715
716    fn inode_is_opaque(&mut self, inode: &InodeInfo) -> io::Result<bool> {
717        for (name, value) in self.read_inode_xattrs(inode)? {
718            if name == b"trusted.overlay.opaque" && value == b"y" {
719                return Ok(true);
720            }
721        }
722
723        Ok(false)
724    }
725
726    fn read_inode_xattrs(&mut self, inode: &InodeInfo) -> io::Result<Vec<(Vec<u8>, Vec<u8>)>> {
727        if inode.xattr_ibody_size == 0 {
728            return Ok(Vec::new());
729        }
730
731        let total = inode.xattr_ibody_size as usize;
732        if total < EROFS_XATTR_IBODY_HEADER_SIZE as usize {
733            return Err(io::Error::new(
734                io::ErrorKind::InvalidData,
735                "xattr ibody smaller than header",
736            ));
737        }
738
739        let mut offset = self.inode_offset(inode.nid)
740            + EROFS_INODE_EXTENDED_SIZE as u64
741            + EROFS_XATTR_IBODY_HEADER_SIZE as u64;
742        let mut remaining = total - EROFS_XATTR_IBODY_HEADER_SIZE as usize;
743        let mut xattrs = Vec::new();
744
745        while remaining > 0 {
746            if remaining < 4 {
747                return Err(io::Error::new(
748                    io::ErrorKind::InvalidData,
749                    "truncated xattr entry header",
750                ));
751            }
752
753            let mut entry = [0u8; 4];
754            read_exact_at(&self.file, offset, &mut entry)?;
755
756            let name_len = entry[0] as usize;
757            let name_index = entry[1];
758            let value_len = u16::from_le_bytes([entry[2], entry[3]]) as usize;
759            let entry_size = 4 + name_len + value_len;
760            let aligned_size = erofs_xattr_align(entry_size);
761
762            if aligned_size > remaining {
763                return Err(io::Error::new(
764                    io::ErrorKind::InvalidData,
765                    "xattr entry exceeds ibody size",
766                ));
767            }
768
769            let mut suffix = vec![0u8; name_len];
770            read_exact_at(&self.file, offset + 4, &mut suffix)?;
771            let mut value = vec![0u8; value_len];
772            read_exact_at(&self.file, offset + 4 + name_len as u64, &mut value)?;
773
774            let name = match name_index {
775                EROFS_XATTR_INDEX_USER => [b"user.".as_slice(), suffix.as_slice()].concat(),
776                EROFS_XATTR_INDEX_TRUSTED => [b"trusted.".as_slice(), suffix.as_slice()].concat(),
777                EROFS_XATTR_INDEX_SECURITY => [b"security.".as_slice(), suffix.as_slice()].concat(),
778                other => {
779                    return Err(io::Error::new(
780                        io::ErrorKind::InvalidData,
781                        format!("unsupported xattr name index: {other}"),
782                    ));
783                }
784            };
785
786            xattrs.push((name, value));
787            offset += aligned_size as u64;
788            remaining -= aligned_size;
789        }
790
791        Ok(xattrs)
792    }
793}
794
795//--------------------------------------------------------------------------------------------------
796// Types: Internal
797//--------------------------------------------------------------------------------------------------
798
799struct InodeInfo {
800    nid: u32,
801    mode: u16,
802    size: u64,
803    #[allow(dead_code)]
804    nlink: u32,
805    uid: u32,
806    gid: u32,
807    mtime: u64,
808    mtime_nsec: u32,
809    data_layout: u8,
810    startblk_lo: u32,
811    rdev: u32,
812    xattr_ibody_size: u32,
813}
814
815impl InodeInfo {
816    fn metadata(&self) -> InodeMetadata {
817        InodeMetadata {
818            uid: self.uid,
819            gid: self.gid,
820            mode: self.mode,
821            mtime: self.mtime,
822            mtime_nsec: self.mtime_nsec,
823        }
824    }
825}
826
827impl ErofsTreeEntry {
828    /// Return true if this directory carries the overlay opaque marker.
829    pub fn is_opaque(&self) -> bool {
830        self.xattrs
831            .iter()
832            .any(|x| x.name == b"trusted.overlay.opaque" && x.value == b"y")
833    }
834}
835
836impl Read for ErofsFileDataReader {
837    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
838        if buf.is_empty() {
839            return Ok(0);
840        }
841
842        while self.segment_index < self.segments.len() {
843            let (offset, len) = self.segments[self.segment_index];
844            if self.segment_offset >= len {
845                self.segment_index += 1;
846                self.segment_offset = 0;
847                continue;
848            }
849
850            let remaining = (len - self.segment_offset) as usize;
851            let to_read = remaining.min(buf.len());
852            let read = read_at_file(
853                &self.file,
854                &mut buf[..to_read],
855                offset + self.segment_offset,
856            )?;
857            self.segment_offset += read as u64;
858            return Ok(read);
859        }
860
861        Ok(0)
862    }
863}
864
865//--------------------------------------------------------------------------------------------------
866// Functions
867//--------------------------------------------------------------------------------------------------
868
869/// Append one EROFS directory entry using the image's canonical separator.
870///
871/// Image paths belong to the Linux guest namespace, so host-native path joining
872/// must not turn `/` into `\` when materialization runs on Windows.
873fn join_image_path(parent: &[u8], name: &[u8]) -> io::Result<Vec<u8>> {
874    if name.is_empty() || name.contains(&b'/') || name.contains(&0) {
875        return Err(io::Error::new(
876            io::ErrorKind::InvalidData,
877            "EROFS directory entry contains an invalid name",
878        ));
879    }
880
881    let mut path = Vec::with_capacity(parent.len() + usize::from(!parent.is_empty()) + name.len());
882    path.extend_from_slice(parent);
883    if !parent.is_empty() {
884        path.push(b'/');
885    }
886    path.extend_from_slice(name);
887    Ok(path)
888}
889
890fn read_exact_at(file: &File, offset: u64, mut buf: &mut [u8]) -> io::Result<()> {
891    let mut current_offset = offset;
892    while !buf.is_empty() {
893        let read = read_at_file(file, buf, current_offset)?;
894        if read == 0 {
895            return Err(io::Error::new(
896                io::ErrorKind::UnexpectedEof,
897                "unexpected EOF",
898            ));
899        }
900        current_offset += read as u64;
901        buf = &mut buf[read..];
902    }
903
904    Ok(())
905}
906
907#[cfg(unix)]
908fn read_at_file(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
909    use std::os::unix::fs::FileExt;
910
911    file.read_at(buf, offset)
912}
913
914#[cfg(windows)]
915fn read_at_file(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
916    use std::os::windows::fs::FileExt;
917
918    file.seek_read(buf, offset)
919}
920
921fn dir_block_dirent_count(block: &[u8]) -> io::Result<usize> {
922    if block.len() < EROFS_DIRENT_SIZE as usize {
923        return Err(io::Error::new(
924            io::ErrorKind::InvalidData,
925            "directory block smaller than one dirent",
926        ));
927    }
928
929    let first_nameoff = u16::from_le_bytes([block[8], block[9]]) as usize;
930    let dirent_size = EROFS_DIRENT_SIZE as usize;
931    if first_nameoff < dirent_size
932        || !first_nameoff.is_multiple_of(dirent_size)
933        || first_nameoff > block.len()
934    {
935        return Err(io::Error::new(
936            io::ErrorKind::InvalidData,
937            "invalid first dirent name offset",
938        ));
939    }
940
941    Ok(first_nameoff / dirent_size)
942}
943
944fn dirent_name(block: &[u8], idx: usize, dirent_count: usize) -> io::Result<&[u8]> {
945    let dirent_size = EROFS_DIRENT_SIZE as usize;
946    let dirent_off = idx
947        .checked_mul(dirent_size)
948        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "dirent offset overflow"))?;
949
950    if idx >= dirent_count || dirent_off + dirent_size > block.len() {
951        return Err(io::Error::new(
952            io::ErrorKind::InvalidData,
953            "dirent index out of bounds",
954        ));
955    }
956
957    let nameoff = u16::from_le_bytes([block[dirent_off + 8], block[dirent_off + 9]]) as usize;
958    let mut name_end = if idx + 1 < dirent_count {
959        let next_off = dirent_off + dirent_size;
960        u16::from_le_bytes([block[next_off + 8], block[next_off + 9]]) as usize
961    } else {
962        block.len()
963    };
964
965    if nameoff > name_end || name_end > block.len() {
966        return Err(io::Error::new(
967            io::ErrorKind::InvalidData,
968            "dirent name range out of bounds",
969        ));
970    }
971
972    while name_end > nameoff && block[name_end - 1] == 0 {
973        name_end -= 1;
974    }
975
976    Ok(&block[nameoff..name_end])
977}
978
979fn dirent_nid(block: &[u8], idx: usize) -> io::Result<u32> {
980    let dirent_size = EROFS_DIRENT_SIZE as usize;
981    let dirent_off = idx
982        .checked_mul(dirent_size)
983        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "dirent offset overflow"))?;
984    if dirent_off + dirent_size > block.len() {
985        return Err(io::Error::new(
986            io::ErrorKind::InvalidData,
987            "dirent NID out of bounds",
988        ));
989    }
990
991    let nid = u64::from_le_bytes([
992        block[dirent_off],
993        block[dirent_off + 1],
994        block[dirent_off + 2],
995        block[dirent_off + 3],
996        block[dirent_off + 4],
997        block[dirent_off + 5],
998        block[dirent_off + 6],
999        block[dirent_off + 7],
1000    ]);
1001    u32::try_from(nid)
1002        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "dirent NID overflow"))
1003}
1004
1005fn lookup_in_dir_block(
1006    block: &[u8],
1007    dirent_count: usize,
1008    target: &[u8],
1009) -> io::Result<Option<u32>> {
1010    let mut left = 0usize;
1011    let mut right = dirent_count;
1012
1013    while left < right {
1014        let mid = (left + right) / 2;
1015        match target.cmp(dirent_name(block, mid, dirent_count)?) {
1016            std::cmp::Ordering::Less => right = mid,
1017            std::cmp::Ordering::Greater => left = mid + 1,
1018            std::cmp::Ordering::Equal => return dirent_nid(block, mid).map(Some),
1019        }
1020    }
1021
1022    Ok(None)
1023}
1024
1025fn inode_kind(inode: &InodeInfo) -> io::Result<ErofsEntryKind> {
1026    match inode.mode & S_IFMT {
1027        S_IFREG => Ok(ErofsEntryKind::RegularFile),
1028        S_IFDIR => Ok(ErofsEntryKind::Directory),
1029        S_IFLNK => Ok(ErofsEntryKind::Symlink),
1030        S_IFCHR => Ok(ErofsEntryKind::CharDevice),
1031        S_IFBLK => Ok(ErofsEntryKind::BlockDevice),
1032        S_IFIFO => Ok(ErofsEntryKind::Fifo),
1033        S_IFSOCK => Ok(ErofsEntryKind::Socket),
1034        other => Err(io::Error::new(
1035            io::ErrorKind::InvalidData,
1036            format!("unsupported inode mode type: {other:#o}"),
1037        )),
1038    }
1039}
1040
1041fn decode_dev(encoded: u32) -> (u32, u32) {
1042    let major = (encoded >> 8) & 0x0000_0fff;
1043    let minor = (encoded & 0x0000_00ff) | ((encoded >> 12) & 0xffff_ff00);
1044    (major, minor)
1045}
1046
1047/// Read a file from an EROFS image file on disk.
1048pub fn read_file_from_erofs(image_path: &Path, file_path: &str) -> io::Result<Vec<u8>> {
1049    let file = std::fs::File::open(image_path)?;
1050    let mut reader = ErofsReader::new(file)?;
1051    reader.read_file(file_path)
1052}
1053
1054pub fn entry_info_from_erofs(image_path: &Path, file_path: &str) -> io::Result<ErofsEntryInfo> {
1055    let file = std::fs::File::open(image_path)?;
1056    let mut reader = ErofsReader::new(file)?;
1057    reader.entry_info(file_path)
1058}
1059
1060//--------------------------------------------------------------------------------------------------
1061// Tests
1062//--------------------------------------------------------------------------------------------------
1063
1064#[cfg(test)]
1065mod tests {
1066    use std::{fs::File, io, path::PathBuf};
1067
1068    use tempfile::tempdir;
1069
1070    use super::ErofsReader;
1071    use crate::{
1072        erofs::write_erofs,
1073        path_bytes::path_bytes,
1074        tree::{FileData, FileTree, InodeMetadata, RegularFileId, RegularFileNode, TreeNode},
1075    };
1076
1077    fn make_regular_file(data: &[u8]) -> TreeNode {
1078        make_regular_file_with_id(data, RegularFileId::new())
1079    }
1080
1081    fn make_regular_file_with_id(data: &[u8], id: RegularFileId) -> TreeNode {
1082        TreeNode::RegularFile(RegularFileNode {
1083            id,
1084            metadata: InodeMetadata::default(),
1085            xattrs: Vec::new(),
1086            data: FileData::Memory(data.to_vec()),
1087            nlink: 1,
1088        })
1089    }
1090
1091    #[test]
1092    fn lookup_path_resolves_large_multi_block_directory() {
1093        let mut tree = FileTree::new();
1094        for i in 0..5000 {
1095            let path = format!("dir/file-{i:04}.txt");
1096            tree.insert(path.as_bytes(), make_regular_file(b"x"))
1097                .expect("insert file");
1098        }
1099
1100        let output_dir = tempdir().expect("tempdir");
1101        let output = output_dir.path().join("large-dir.erofs");
1102        write_erofs(&tree, &output).expect("write erofs");
1103
1104        let file = File::open(&output).expect("open erofs");
1105        let mut reader = ErofsReader::new(file).expect("reader");
1106
1107        assert_eq!(reader.read_file("/dir/file-0000.txt").expect("first"), b"x");
1108        assert_eq!(
1109            reader.read_file("/dir/file-2500.txt").expect("middle"),
1110            b"x"
1111        );
1112        assert_eq!(reader.read_file("/dir/file-4999.txt").expect("last"), b"x");
1113
1114        let err = reader
1115            .entry_info("/dir/file-9999.txt")
1116            .expect_err("missing entry should fail");
1117        assert_eq!(err.kind(), io::ErrorKind::NotFound);
1118    }
1119
1120    #[test]
1121    fn walk_uses_guest_separators_on_every_host() {
1122        let mut tree = FileTree::new();
1123        tree.insert(b"etc/passwd", make_regular_file(b"root:x:0:0"))
1124            .expect("insert nested file");
1125
1126        let output_dir = tempdir().expect("tempdir");
1127        let output = output_dir.path().join("nested.erofs");
1128        write_erofs(&tree, &output).expect("write erofs");
1129
1130        let file = File::open(&output).expect("open erofs");
1131        let mut reader = ErofsReader::new(file).expect("reader");
1132        let paths = reader
1133            .walk()
1134            .expect("walk erofs")
1135            .into_iter()
1136            .map(|entry| path_bytes(&entry.path).to_vec())
1137            .collect::<Vec<_>>();
1138
1139        assert!(paths.iter().any(|path| path == b"etc/passwd"));
1140        assert!(!paths.iter().any(|path| path == b"etc\\passwd"));
1141
1142        let mut byte_paths = Vec::new();
1143        reader
1144            .walk_entries_with_path_bytes::<io::Error, _>(|_, path, _| {
1145                byte_paths.push(path.to_vec());
1146                Ok(())
1147            })
1148            .expect("walk erofs with canonical bytes");
1149        assert!(byte_paths.iter().any(|path| path == b"etc/passwd"));
1150        assert!(!byte_paths.iter().any(|path| path == b"etc\\passwd"));
1151    }
1152
1153    #[test]
1154    fn hardlinked_regular_files_share_inode_and_data_blocks() {
1155        let mut tree = FileTree::new();
1156        let file_id = RegularFileId::new();
1157
1158        tree.insert(b"alpha", make_regular_file_with_id(b"shared", file_id))
1159            .expect("insert alpha");
1160        tree.insert(b"beta", make_regular_file_with_id(b"shared", file_id))
1161            .expect("insert beta");
1162
1163        let output_dir = tempdir().expect("tempdir");
1164        let output = output_dir.path().join("hardlinks.erofs");
1165        let data_map = write_erofs(&tree, &output).expect("write erofs");
1166        let alpha_path = PathBuf::from("alpha");
1167        let beta_path = PathBuf::from("beta");
1168
1169        assert_eq!(
1170            data_map
1171                .file_blocks
1172                .get(&alpha_path)
1173                .copied()
1174                .expect("alpha data map"),
1175            data_map
1176                .file_blocks
1177                .get(&beta_path)
1178                .copied()
1179                .expect("beta data map")
1180        );
1181
1182        let file = File::open(&output).expect("open erofs");
1183        let mut reader = ErofsReader::new(file).expect("reader");
1184        let alpha = reader.inode_debug_info("/alpha").expect("alpha inode");
1185        let beta = reader.inode_debug_info("/beta").expect("beta inode");
1186
1187        assert_eq!(alpha.nid, beta.nid);
1188        assert_eq!(alpha.nlink, 2);
1189        assert_eq!(beta.nlink, 2);
1190        assert_eq!(alpha.size, b"shared".len() as u64);
1191        assert_eq!(reader.read_file("/alpha").expect("read alpha"), b"shared");
1192        assert_eq!(reader.read_file("/beta").expect("read beta"), b"shared");
1193    }
1194}