1use core::mem::size_of;
8use std::collections::{BTreeSet, HashSet};
9use std::ops::Range;
10
11use thiserror::Error;
12use zerocopy::{little_endian::U32, FromBytes, Immutable, KnownLayout};
13
14use super::{
15 composefs::OverlayMetacopy,
16 format::{
17 CompactInodeHeader, ComposefsHeader, DataLayout, DirectoryEntryHeader, ExtendedInodeHeader,
18 InodeXAttrHeader, ModeField, Superblock, XAttrHeader,
19 },
20};
21use crate::fsverity::FsVerityHashValue;
22
23pub fn round_up(n: usize, to: usize) -> usize {
25 (n + to - 1) & !(to - 1)
26}
27
28pub trait InodeHeader {
30 fn data_layout(&self) -> DataLayout;
32 fn xattr_icount(&self) -> u16;
34 fn mode(&self) -> ModeField;
36 fn size(&self) -> u64;
38 fn u(&self) -> u32;
40
41 fn additional_bytes(&self, blkszbits: u8) -> usize {
43 let block_size = 1 << blkszbits;
44 self.xattr_size()
45 + match self.data_layout() {
46 DataLayout::FlatPlain => 0,
47 DataLayout::FlatInline => self.size() as usize % block_size,
48 DataLayout::ChunkBased => 4,
49 }
50 }
51
52 fn xattr_size(&self) -> usize {
54 match self.xattr_icount() {
55 0 => 0,
56 n => (n as usize - 1) * 4 + 12,
57 }
58 }
59}
60
61impl InodeHeader for ExtendedInodeHeader {
62 fn data_layout(&self) -> DataLayout {
63 self.format.try_into().unwrap()
64 }
65
66 fn xattr_icount(&self) -> u16 {
67 self.xattr_icount.get()
68 }
69
70 fn mode(&self) -> ModeField {
71 self.mode
72 }
73
74 fn size(&self) -> u64 {
75 self.size.get()
76 }
77
78 fn u(&self) -> u32 {
79 self.u.get()
80 }
81}
82
83impl InodeHeader for CompactInodeHeader {
84 fn data_layout(&self) -> DataLayout {
85 self.format.try_into().unwrap()
86 }
87
88 fn xattr_icount(&self) -> u16 {
89 self.xattr_icount.get()
90 }
91
92 fn mode(&self) -> ModeField {
93 self.mode
94 }
95
96 fn size(&self) -> u64 {
97 self.size.get() as u64
98 }
99
100 fn u(&self) -> u32 {
101 self.u.get()
102 }
103}
104
105#[repr(C)]
107#[derive(FromBytes, Immutable, KnownLayout)]
108pub struct XAttr {
109 pub header: XAttrHeader,
111 pub data: [u8],
113}
114
115#[repr(C)]
117#[derive(FromBytes, Immutable, KnownLayout)]
118pub struct Inode<Header: InodeHeader> {
119 pub header: Header,
121 pub data: [u8],
123}
124
125#[repr(C)]
127#[derive(Debug, FromBytes, Immutable, KnownLayout)]
128pub struct InodeXAttrs {
129 pub header: InodeXAttrHeader,
131 pub data: [u8],
133}
134
135impl XAttrHeader {
136 pub fn calculate_n_elems(&self) -> usize {
138 round_up(self.name_len as usize + self.value_size.get() as usize, 4)
139 }
140}
141
142impl XAttr {
143 pub fn from_prefix(data: &[u8]) -> (&XAttr, &[u8]) {
145 let header = XAttrHeader::ref_from_bytes(&data[..4]).unwrap();
146 Self::ref_from_prefix_with_elems(data, header.calculate_n_elems()).unwrap()
147 }
148
149 pub fn suffix(&self) -> &[u8] {
151 &self.data[..self.header.name_len as usize]
152 }
153
154 pub fn value(&self) -> &[u8] {
156 &self.data[self.header.name_len as usize..][..self.header.value_size.get() as usize]
157 }
158
159 pub fn padding(&self) -> &[u8] {
161 &self.data[self.header.name_len as usize + self.header.value_size.get() as usize..]
162 }
163}
164
165pub trait InodeOps {
167 fn xattrs(&self) -> Option<&InodeXAttrs>;
169 fn inline(&self) -> Option<&[u8]>;
171 fn blocks(&self, blkszbits: u8) -> Range<u64>;
173}
174
175impl<Header: InodeHeader> InodeHeader for &Inode<Header> {
176 fn data_layout(&self) -> DataLayout {
177 self.header.data_layout()
178 }
179
180 fn xattr_icount(&self) -> u16 {
181 self.header.xattr_icount()
182 }
183
184 fn mode(&self) -> ModeField {
185 self.header.mode()
186 }
187
188 fn size(&self) -> u64 {
189 self.header.size()
190 }
191
192 fn u(&self) -> u32 {
193 self.header.u()
194 }
195}
196
197impl<Header: InodeHeader> InodeOps for &Inode<Header> {
198 fn xattrs(&self) -> Option<&InodeXAttrs> {
199 match self.header.xattr_size() {
200 0 => None,
201 n => Some(InodeXAttrs::ref_from_bytes(&self.data[..n]).unwrap()),
202 }
203 }
204
205 fn inline(&self) -> Option<&[u8]> {
206 let data = &self.data[self.header.xattr_size()..];
207
208 if data.is_empty() {
209 return None;
210 }
211
212 Some(data)
213 }
214
215 fn blocks(&self, blkszbits: u8) -> Range<u64> {
216 let size = self.header.size();
217 let block_size = 1 << blkszbits;
218 let start = self.header.u() as u64;
219
220 match self.header.data_layout() {
221 DataLayout::FlatPlain => Range {
222 start,
223 end: start + size.div_ceil(block_size),
224 },
225 DataLayout::FlatInline => Range {
226 start,
227 end: start + size / block_size,
228 },
229 DataLayout::ChunkBased => Range { start, end: start },
230 }
231 }
232}
233
234#[derive(Debug)]
238pub enum InodeType<'img> {
239 Compact(&'img Inode<CompactInodeHeader>),
241 Extended(&'img Inode<ExtendedInodeHeader>),
243}
244
245impl InodeHeader for InodeType<'_> {
246 fn u(&self) -> u32 {
247 match self {
248 Self::Compact(inode) => inode.u(),
249 Self::Extended(inode) => inode.u(),
250 }
251 }
252
253 fn size(&self) -> u64 {
254 match self {
255 Self::Compact(inode) => inode.size(),
256 Self::Extended(inode) => inode.size(),
257 }
258 }
259
260 fn xattr_icount(&self) -> u16 {
261 match self {
262 Self::Compact(inode) => inode.xattr_icount(),
263 Self::Extended(inode) => inode.xattr_icount(),
264 }
265 }
266
267 fn data_layout(&self) -> DataLayout {
268 match self {
269 Self::Compact(inode) => inode.data_layout(),
270 Self::Extended(inode) => inode.data_layout(),
271 }
272 }
273
274 fn mode(&self) -> ModeField {
275 match self {
276 Self::Compact(inode) => inode.mode(),
277 Self::Extended(inode) => inode.mode(),
278 }
279 }
280}
281
282impl InodeOps for InodeType<'_> {
283 fn xattrs(&self) -> Option<&InodeXAttrs> {
284 match self {
285 Self::Compact(inode) => inode.xattrs(),
286 Self::Extended(inode) => inode.xattrs(),
287 }
288 }
289
290 fn inline(&self) -> Option<&[u8]> {
291 match self {
292 Self::Compact(inode) => inode.inline(),
293 Self::Extended(inode) => inode.inline(),
294 }
295 }
296
297 fn blocks(&self, blkszbits: u8) -> Range<u64> {
298 match self {
299 Self::Compact(inode) => inode.blocks(blkszbits),
300 Self::Extended(inode) => inode.blocks(blkszbits),
301 }
302 }
303}
304
305#[derive(Debug)]
307pub struct Image<'i> {
308 pub image: &'i [u8],
310 pub header: &'i ComposefsHeader,
312 pub blkszbits: u8,
314 pub block_size: usize,
316 pub sb: &'i Superblock,
318 pub inodes: &'i [u8],
320 pub xattrs: &'i [u8],
322}
323
324impl<'img> Image<'img> {
325 pub fn open(image: &'img [u8]) -> Self {
327 let header = ComposefsHeader::ref_from_prefix(image)
328 .expect("header err")
329 .0;
330 let sb = Superblock::ref_from_prefix(&image[1024..])
331 .expect("superblock err")
332 .0;
333 let blkszbits = sb.blkszbits;
334 let block_size = 1usize << blkszbits;
335 assert!(block_size != 0);
336 let inodes = &image[sb.meta_blkaddr.get() as usize * block_size..];
337 let xattrs = &image[sb.xattr_blkaddr.get() as usize * block_size..];
338 Image {
339 image,
340 header,
341 blkszbits,
342 block_size,
343 sb,
344 inodes,
345 xattrs,
346 }
347 }
348
349 pub fn inode(&self, id: u64) -> InodeType<'_> {
351 let inode_data = &self.inodes[id as usize * 32..];
352 if inode_data[0] & 1 != 0 {
353 let header = ExtendedInodeHeader::ref_from_bytes(&inode_data[..64]).unwrap();
354 InodeType::Extended(
355 Inode::<ExtendedInodeHeader>::ref_from_prefix_with_elems(
356 inode_data,
357 header.additional_bytes(self.blkszbits),
358 )
359 .unwrap()
360 .0,
361 )
362 } else {
363 let header = CompactInodeHeader::ref_from_bytes(&inode_data[..32]).unwrap();
364 InodeType::Compact(
365 Inode::<CompactInodeHeader>::ref_from_prefix_with_elems(
366 inode_data,
367 header.additional_bytes(self.blkszbits),
368 )
369 .unwrap()
370 .0,
371 )
372 }
373 }
374
375 pub fn shared_xattr(&self, id: u32) -> &XAttr {
377 let xattr_data = &self.xattrs[id as usize * 4..];
378 let header = XAttrHeader::ref_from_bytes(&xattr_data[..4]).unwrap();
379 XAttr::ref_from_prefix_with_elems(xattr_data, header.calculate_n_elems())
380 .unwrap()
381 .0
382 }
383
384 pub fn block(&self, id: u64) -> &[u8] {
386 &self.image[id as usize * self.block_size..][..self.block_size]
387 }
388
389 pub fn data_block(&self, id: u64) -> &DataBlock {
391 DataBlock::ref_from_bytes(self.block(id)).unwrap()
392 }
393
394 pub fn directory_block(&self, id: u64) -> &DirectoryBlock {
396 DirectoryBlock::ref_from_bytes(self.block(id)).unwrap()
397 }
398
399 pub fn root(&self) -> InodeType<'_> {
401 self.inode(self.sb.root_nid.get() as u64)
402 }
403}
404
405#[derive(FromBytes, Immutable, KnownLayout)]
407#[repr(C)]
408struct Array<T>([T]);
409
410impl InodeXAttrs {
411 pub fn shared(&self) -> &[U32] {
413 &Array::ref_from_prefix_with_elems(&self.data, self.header.shared_count as usize)
414 .unwrap()
415 .0
416 .0
417 }
418
419 pub fn local(&self) -> XAttrIter<'_> {
421 XAttrIter {
422 data: &self.data[self.header.shared_count as usize * 4..],
423 }
424 }
425}
426
427#[derive(Debug)]
429pub struct XAttrIter<'img> {
430 data: &'img [u8],
431}
432
433impl<'img> Iterator for XAttrIter<'img> {
434 type Item = &'img XAttr;
435
436 fn next(&mut self) -> Option<Self::Item> {
437 if !self.data.is_empty() {
438 let (result, rest) = XAttr::from_prefix(self.data);
439 self.data = rest;
440 Some(result)
441 } else {
442 None
443 }
444 }
445}
446
447#[repr(C)]
449#[derive(FromBytes, Immutable, KnownLayout)]
450pub struct DataBlock(pub [u8]);
451
452#[repr(C)]
454#[derive(FromBytes, Immutable, KnownLayout)]
455pub struct DirectoryBlock(pub [u8]);
456
457impl DirectoryBlock {
458 pub fn get_entry_header(&self, n: usize) -> &DirectoryEntryHeader {
460 let entry_data = &self.0
461 [n * size_of::<DirectoryEntryHeader>()..(n + 1) * size_of::<DirectoryEntryHeader>()];
462 DirectoryEntryHeader::ref_from_bytes(entry_data).unwrap()
463 }
464
465 pub fn get_entry_headers(&self) -> &[DirectoryEntryHeader] {
467 &Array::ref_from_prefix_with_elems(&self.0, self.n_entries())
468 .unwrap()
469 .0
470 .0
471 }
472
473 pub fn n_entries(&self) -> usize {
475 let first = self.get_entry_header(0);
476 let offset = first.name_offset.get();
477 assert!(offset != 0);
478 assert!(offset.is_multiple_of(12));
479 offset as usize / 12
480 }
481
482 pub fn entries(&self) -> DirectoryEntries<'_> {
484 DirectoryEntries {
485 block: self,
486 length: self.n_entries(),
487 position: 0,
488 }
489 }
490}
491
492#[derive(Debug)]
495pub struct DirectoryEntry<'a> {
496 pub header: &'a DirectoryEntryHeader,
498 pub name: &'a [u8],
500}
501
502impl DirectoryEntry<'_> {
503 fn nid(&self) -> u64 {
504 self.header.inode_offset.get()
505 }
506}
507
508#[derive(Debug)]
510pub struct DirectoryEntries<'d> {
511 block: &'d DirectoryBlock,
512 length: usize,
513 position: usize,
514}
515
516impl<'d> Iterator for DirectoryEntries<'d> {
517 type Item = DirectoryEntry<'d>;
518
519 fn next(&mut self) -> Option<Self::Item> {
520 if self.position < self.length {
521 let header = self.block.get_entry_header(self.position);
522 let name_start = header.name_offset.get() as usize;
523 self.position += 1;
524
525 let name = if self.position == self.length {
526 let with_padding = &self.block.0[name_start..];
527 let end = with_padding.partition_point(|c| *c != 0);
528 &with_padding[..end]
529 } else {
530 let next = self.block.get_entry_header(self.position);
531 let name_end = next.name_offset.get() as usize;
532 &self.block.0[name_start..name_end]
533 };
534
535 Some(DirectoryEntry { header, name })
536 } else {
537 None
538 }
539 }
540}
541
542#[derive(Error, Debug)]
544pub enum ErofsReaderError {
545 #[error("Hardlinked directories detected")]
547 DirectoryHardlinks,
548 #[error("Maximum directory depth exceeded")]
550 DepthExceeded,
551 #[error("Invalid '.' entry in directory")]
553 InvalidSelfReference,
554 #[error("Invalid '..' entry in directory")]
556 InvalidParentReference,
557 #[error("File type in dirent doesn't match type in inode")]
559 FileTypeMismatch,
560}
561
562type ReadResult<T> = Result<T, ErofsReaderError>;
563
564#[derive(Debug)]
566pub struct ObjectCollector<ObjectID: FsVerityHashValue> {
567 visited_nids: HashSet<u64>,
568 nids_to_visit: BTreeSet<u64>,
569 objects: HashSet<ObjectID>,
570}
571
572impl<ObjectID: FsVerityHashValue> ObjectCollector<ObjectID> {
573 fn visit_xattr(&mut self, attr: &XAttr) {
574 if attr.header.name_index != 4 {
576 return;
577 }
578 if attr.suffix() != b"overlay.metacopy" {
579 return;
580 }
581 if let Ok(value) = OverlayMetacopy::read_from_bytes(attr.value()) {
582 if value.valid() {
583 self.objects.insert(value.digest);
584 }
585 }
586 }
587
588 fn visit_xattrs(&mut self, img: &Image, xattrs: &InodeXAttrs) -> ReadResult<()> {
589 for id in xattrs.shared() {
590 self.visit_xattr(img.shared_xattr(id.get()));
591 }
592 for attr in xattrs.local() {
593 self.visit_xattr(attr);
594 }
595 Ok(())
596 }
597
598 fn visit_directory_block(&mut self, block: &DirectoryBlock) {
599 for entry in block.entries() {
600 if entry.name != b"." && entry.name != b".." {
601 let nid = entry.nid();
602 if !self.visited_nids.contains(&nid) {
603 self.nids_to_visit.insert(nid);
604 }
605 }
606 }
607 }
608
609 fn visit_nid(&mut self, img: &Image, nid: u64) -> ReadResult<()> {
610 let first_time = self.visited_nids.insert(nid);
611 assert!(first_time); let inode = img.inode(nid);
614
615 if let Some(xattrs) = inode.xattrs() {
616 self.visit_xattrs(img, xattrs)?;
617 }
618
619 if inode.mode().is_dir() {
620 for blkid in inode.blocks(img.sb.blkszbits) {
621 self.visit_directory_block(img.directory_block(blkid));
622 }
623
624 if let Some(inline) = inode.inline() {
625 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
626 self.visit_directory_block(inline_block);
627 }
628 }
629
630 Ok(())
631 }
632}
633
634pub fn collect_objects<ObjectID: FsVerityHashValue>(image: &[u8]) -> ReadResult<HashSet<ObjectID>> {
641 let img = Image::open(image);
642 let mut this = ObjectCollector {
643 visited_nids: HashSet::new(),
644 nids_to_visit: BTreeSet::new(),
645 objects: HashSet::new(),
646 };
647
648 this.nids_to_visit.insert(img.sb.root_nid.get() as u64);
651 while let Some(nid) = this.nids_to_visit.pop_first() {
652 this.visit_nid(&img, nid)?;
653 }
654 Ok(this.objects)
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660 use crate::{
661 dumpfile::dumpfile_to_filesystem, erofs::writer::mkfs_erofs, fsverity::Sha256HashValue,
662 };
663 use std::collections::HashMap;
664
665 fn validate_directory_entries(img: &Image, nid: u64, expected_names: &[&str]) {
667 let inode = img.inode(nid);
668 assert!(inode.mode().is_dir(), "Expected directory inode");
669
670 let mut found_names = Vec::new();
671
672 if let Some(inline) = inode.inline() {
674 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
675 for entry in inline_block.entries() {
676 let name = std::str::from_utf8(entry.name).unwrap();
677 found_names.push(name.to_string());
678 }
679 }
680
681 for blkid in inode.blocks(img.blkszbits) {
683 let block = img.directory_block(blkid);
684 for entry in block.entries() {
685 let name = std::str::from_utf8(entry.name).unwrap();
686 found_names.push(name.to_string());
687 }
688 }
689
690 found_names.sort();
692 let mut expected_sorted: Vec<_> = expected_names.iter().map(|s| s.to_string()).collect();
693 expected_sorted.sort();
694
695 assert_eq!(
696 found_names, expected_sorted,
697 "Directory entries mismatch for nid {}",
698 nid
699 );
700 }
701
702 #[test]
703 fn test_empty_directory() {
704 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
706/empty_dir 4096 40755 2 0 0 0 1000.0 - - -
707"#;
708
709 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
710 let image = mkfs_erofs(&fs);
711 let img = Image::open(&image);
712
713 let root_nid = img.sb.root_nid.get() as u64;
715 validate_directory_entries(&img, root_nid, &[".", "..", "empty_dir"]);
716
717 let root_inode = img.root();
719 let mut empty_dir_nid = None;
720 if let Some(inline) = root_inode.inline() {
721 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
722 for entry in inline_block.entries() {
723 if entry.name == b"empty_dir" {
724 empty_dir_nid = Some(entry.nid());
725 break;
726 }
727 }
728 }
729 for blkid in root_inode.blocks(img.blkszbits) {
730 let block = img.directory_block(blkid);
731 for entry in block.entries() {
732 if entry.name == b"empty_dir" {
733 empty_dir_nid = Some(entry.nid());
734 break;
735 }
736 }
737 }
738
739 let empty_dir_nid = empty_dir_nid.expect("empty_dir not found");
740 validate_directory_entries(&img, empty_dir_nid, &[".", ".."]);
741 }
742
743 #[test]
744 fn test_directory_with_inline_entries() {
745 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
747/dir1 4096 40755 2 0 0 0 1000.0 - - -
748/dir1/file1 5 100644 1 0 0 0 1000.0 - hello -
749/dir1/file2 5 100644 1 0 0 0 1000.0 - world -
750"#;
751
752 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
753 let image = mkfs_erofs(&fs);
754 let img = Image::open(&image);
755
756 let root_inode = img.root();
758 let mut dir1_nid = None;
759 if let Some(inline) = root_inode.inline() {
760 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
761 for entry in inline_block.entries() {
762 if entry.name == b"dir1" {
763 dir1_nid = Some(entry.nid());
764 break;
765 }
766 }
767 }
768 for blkid in root_inode.blocks(img.blkszbits) {
769 let block = img.directory_block(blkid);
770 for entry in block.entries() {
771 if entry.name == b"dir1" {
772 dir1_nid = Some(entry.nid());
773 break;
774 }
775 }
776 }
777
778 let dir1_nid = dir1_nid.expect("dir1 not found");
779 validate_directory_entries(&img, dir1_nid, &[".", "..", "file1", "file2"]);
780 }
781
782 #[test]
783 fn test_directory_with_many_entries() {
784 let mut dumpfile = String::from("/ 4096 40755 2 0 0 0 1000.0 - - -\n");
786 dumpfile.push_str("/bigdir 4096 40755 2 0 0 0 1000.0 - - -\n");
787
788 for i in 0..100 {
790 dumpfile.push_str(&format!(
791 "/bigdir/file{:03} 5 100644 1 0 0 0 1000.0 - hello -\n",
792 i
793 ));
794 }
795
796 let fs = dumpfile_to_filesystem::<Sha256HashValue>(&dumpfile).unwrap();
797 let image = mkfs_erofs(&fs);
798 let img = Image::open(&image);
799
800 let root_inode = img.root();
802 let mut bigdir_nid = None;
803 if let Some(inline) = root_inode.inline() {
804 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
805 for entry in inline_block.entries() {
806 if entry.name == b"bigdir" {
807 bigdir_nid = Some(entry.nid());
808 break;
809 }
810 }
811 }
812 for blkid in root_inode.blocks(img.blkszbits) {
813 let block = img.directory_block(blkid);
814 for entry in block.entries() {
815 if entry.name == b"bigdir" {
816 bigdir_nid = Some(entry.nid());
817 break;
818 }
819 }
820 }
821
822 let bigdir_nid = bigdir_nid.expect("bigdir not found");
823
824 let mut expected: Vec<String> = vec![".".to_string(), "..".to_string()];
826 for i in 0..100 {
827 expected.push(format!("file{:03}", i));
828 }
829 let expected_refs: Vec<&str> = expected.iter().map(|s| s.as_str()).collect();
830
831 validate_directory_entries(&img, bigdir_nid, &expected_refs);
832 }
833
834 #[test]
835 fn test_nested_directories() {
836 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
838/a 4096 40755 2 0 0 0 1000.0 - - -
839/a/b 4096 40755 2 0 0 0 1000.0 - - -
840/a/b/c 4096 40755 2 0 0 0 1000.0 - - -
841/a/b/c/file.txt 5 100644 1 0 0 0 1000.0 - hello -
842"#;
843
844 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
845 let image = mkfs_erofs(&fs);
846 let img = Image::open(&image);
847
848 let root_nid = img.sb.root_nid.get() as u64;
850 validate_directory_entries(&img, root_nid, &[".", "..", "a"]);
851
852 let find_entry = |parent_nid: u64, name: &[u8]| -> u64 {
854 let inode = img.inode(parent_nid);
855
856 if let Some(inline) = inode.inline() {
857 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
858 for entry in inline_block.entries() {
859 if entry.name == name {
860 return entry.nid();
861 }
862 }
863 }
864
865 for blkid in inode.blocks(img.blkszbits) {
866 let block = img.directory_block(blkid);
867 for entry in block.entries() {
868 if entry.name == name {
869 return entry.nid();
870 }
871 }
872 }
873 panic!("Entry not found: {:?}", std::str::from_utf8(name));
874 };
875
876 let a_nid = find_entry(root_nid, b"a");
877 validate_directory_entries(&img, a_nid, &[".", "..", "b"]);
878
879 let b_nid = find_entry(a_nid, b"b");
880 validate_directory_entries(&img, b_nid, &[".", "..", "c"]);
881
882 let c_nid = find_entry(b_nid, b"c");
883 validate_directory_entries(&img, c_nid, &[".", "..", "file.txt"]);
884 }
885
886 #[test]
887 fn test_mixed_entry_types() {
888 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
890/mixed 4096 40755 2 0 0 0 1000.0 - - -
891/mixed/regular 10 100644 1 0 0 0 1000.0 - content123 -
892/mixed/symlink 7 120777 1 0 0 0 1000.0 /target - -
893/mixed/fifo 0 10644 1 0 0 0 1000.0 - - -
894/mixed/subdir 4096 40755 2 0 0 0 1000.0 - - -
895"#;
896
897 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
898 let image = mkfs_erofs(&fs);
899 let img = Image::open(&image);
900
901 let root_inode = img.root();
902 let mut mixed_nid = None;
903 if let Some(inline) = root_inode.inline() {
904 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
905 for entry in inline_block.entries() {
906 if entry.name == b"mixed" {
907 mixed_nid = Some(entry.nid());
908 break;
909 }
910 }
911 }
912 for blkid in root_inode.blocks(img.blkszbits) {
913 let block = img.directory_block(blkid);
914 for entry in block.entries() {
915 if entry.name == b"mixed" {
916 mixed_nid = Some(entry.nid());
917 break;
918 }
919 }
920 }
921
922 let mixed_nid = mixed_nid.expect("mixed not found");
923 validate_directory_entries(
924 &img,
925 mixed_nid,
926 &[".", "..", "regular", "symlink", "fifo", "subdir"],
927 );
928 }
929
930 #[test]
931 fn test_collect_objects_traversal() {
932 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
934/dir1 4096 40755 2 0 0 0 1000.0 - - -
935/dir1/file1 5 100644 1 0 0 0 1000.0 - hello -
936/dir2 4096 40755 2 0 0 0 1000.0 - - -
937/dir2/subdir 4096 40755 2 0 0 0 1000.0 - - -
938/dir2/subdir/file2 5 100644 1 0 0 0 1000.0 - world -
939"#;
940
941 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
942 let image = mkfs_erofs(&fs);
943
944 let result = collect_objects::<Sha256HashValue>(&image);
946 assert!(
947 result.is_ok(),
948 "Failed to collect objects: {:?}",
949 result.err()
950 );
951 }
952
953 #[test]
954 fn test_pr188_empty_inline_directory() -> anyhow::Result<()> {
955 let dumpfile_content = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
971/empty_dir 4096 40755 2 0 0 0 1000.0 - - -
972"#;
973
974 let temp_dir = tempfile::TempDir::new()?;
976 let temp_dir = temp_dir.path();
977 let dumpfile_path = temp_dir.join("pr188_test.dump");
978 let erofs_path = temp_dir.join("pr188_test.erofs");
979
980 std::fs::write(&dumpfile_path, dumpfile_content).expect("Failed to write test dumpfile");
982
983 let output = std::process::Command::new("mkcomposefs")
985 .arg("--from-file")
986 .arg(&dumpfile_path)
987 .arg(&erofs_path)
988 .output()
989 .expect("Failed to run mkcomposefs - is it installed?");
990
991 assert!(
992 output.status.success(),
993 "mkcomposefs failed: {}",
994 String::from_utf8_lossy(&output.stderr)
995 );
996
997 let image = std::fs::read(&erofs_path).expect("Failed to read generated erofs");
999
1000 let r = collect_objects::<Sha256HashValue>(&image).unwrap();
1002 assert_eq!(r.len(), 0);
1003
1004 Ok(())
1005 }
1006
1007 #[test]
1008 fn test_round_trip_basic() {
1009 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
1011/file1 5 100644 1 0 0 0 1000.0 - hello -
1012/file2 6 100644 1 0 0 0 1000.0 - world! -
1013/dir1 4096 40755 2 0 0 0 1000.0 - - -
1014/dir1/nested 8 100644 1 0 0 0 1000.0 - content1 -
1015"#;
1016
1017 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
1018 let image = mkfs_erofs(&fs);
1019 let img = Image::open(&image);
1020
1021 let root_nid = img.sb.root_nid.get() as u64;
1023 validate_directory_entries(&img, root_nid, &[".", "..", "file1", "file2", "dir1"]);
1024
1025 let mut entries_map: HashMap<Vec<u8>, u64> = HashMap::new();
1027 let root_inode = img.root();
1028
1029 if let Some(inline) = root_inode.inline() {
1030 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
1031 for entry in inline_block.entries() {
1032 entries_map.insert(entry.name.to_vec(), entry.nid());
1033 }
1034 }
1035
1036 for blkid in root_inode.blocks(img.blkszbits) {
1037 let block = img.directory_block(blkid);
1038 for entry in block.entries() {
1039 entries_map.insert(entry.name.to_vec(), entry.nid());
1040 }
1041 }
1042
1043 let file1_nid = entries_map
1045 .get(b"file1".as_slice())
1046 .expect("file1 not found");
1047 let file1_inode = img.inode(*file1_nid);
1048 assert!(!file1_inode.mode().is_dir());
1049 assert_eq!(file1_inode.size(), 5);
1050
1051 let inline_data = file1_inode.inline();
1052 assert_eq!(inline_data, Some(b"hello".as_slice()));
1053 }
1054}