1use 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
23pub 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#[derive(Clone)]
54pub struct ErofsTreeEntry {
55 pub path: PathBuf,
57 pub nid: u32,
59 pub kind: ErofsEntryKind,
61 pub metadata: InodeMetadata,
63 pub xattrs: Vec<Xattr>,
65 pub size: u64,
67 pub rdev: Option<(u32, u32)>,
69}
70
71pub 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
88impl ErofsReader {
93 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 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 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 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 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 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 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 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 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 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 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 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 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 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
795struct 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 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
865fn 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
1047pub 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#[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}