1use 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
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, PathBuf::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 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 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 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 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 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 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 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
749struct 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 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
819fn 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
980pub 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#[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}