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 {nid}"
698 );
699 }
700
701 #[test]
702 fn test_empty_directory() {
703 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
705/empty_dir 4096 40755 2 0 0 0 1000.0 - - -
706"#;
707
708 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
709 let image = mkfs_erofs(&fs);
710 let img = Image::open(&image);
711
712 let root_nid = img.sb.root_nid.get() as u64;
714 validate_directory_entries(&img, root_nid, &[".", "..", "empty_dir"]);
715
716 let root_inode = img.root();
718 let mut empty_dir_nid = None;
719 if let Some(inline) = root_inode.inline() {
720 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
721 for entry in inline_block.entries() {
722 if entry.name == b"empty_dir" {
723 empty_dir_nid = Some(entry.nid());
724 break;
725 }
726 }
727 }
728 for blkid in root_inode.blocks(img.blkszbits) {
729 let block = img.directory_block(blkid);
730 for entry in block.entries() {
731 if entry.name == b"empty_dir" {
732 empty_dir_nid = Some(entry.nid());
733 break;
734 }
735 }
736 }
737
738 let empty_dir_nid = empty_dir_nid.expect("empty_dir not found");
739 validate_directory_entries(&img, empty_dir_nid, &[".", ".."]);
740 }
741
742 #[test]
743 fn test_directory_with_inline_entries() {
744 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
746/dir1 4096 40755 2 0 0 0 1000.0 - - -
747/dir1/file1 5 100644 1 0 0 0 1000.0 - hello -
748/dir1/file2 5 100644 1 0 0 0 1000.0 - world -
749"#;
750
751 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
752 let image = mkfs_erofs(&fs);
753 let img = Image::open(&image);
754
755 let root_inode = img.root();
757 let mut dir1_nid = None;
758 if let Some(inline) = root_inode.inline() {
759 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
760 for entry in inline_block.entries() {
761 if entry.name == b"dir1" {
762 dir1_nid = Some(entry.nid());
763 break;
764 }
765 }
766 }
767 for blkid in root_inode.blocks(img.blkszbits) {
768 let block = img.directory_block(blkid);
769 for entry in block.entries() {
770 if entry.name == b"dir1" {
771 dir1_nid = Some(entry.nid());
772 break;
773 }
774 }
775 }
776
777 let dir1_nid = dir1_nid.expect("dir1 not found");
778 validate_directory_entries(&img, dir1_nid, &[".", "..", "file1", "file2"]);
779 }
780
781 #[test]
782 fn test_directory_with_many_entries() {
783 let mut dumpfile = String::from("/ 4096 40755 2 0 0 0 1000.0 - - -\n");
785 dumpfile.push_str("/bigdir 4096 40755 2 0 0 0 1000.0 - - -\n");
786
787 for i in 0..100 {
789 dumpfile.push_str(&format!(
790 "/bigdir/file{i:03} 5 100644 1 0 0 0 1000.0 - hello -\n"
791 ));
792 }
793
794 let fs = dumpfile_to_filesystem::<Sha256HashValue>(&dumpfile).unwrap();
795 let image = mkfs_erofs(&fs);
796 let img = Image::open(&image);
797
798 let root_inode = img.root();
800 let mut bigdir_nid = None;
801 if let Some(inline) = root_inode.inline() {
802 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
803 for entry in inline_block.entries() {
804 if entry.name == b"bigdir" {
805 bigdir_nid = Some(entry.nid());
806 break;
807 }
808 }
809 }
810 for blkid in root_inode.blocks(img.blkszbits) {
811 let block = img.directory_block(blkid);
812 for entry in block.entries() {
813 if entry.name == b"bigdir" {
814 bigdir_nid = Some(entry.nid());
815 break;
816 }
817 }
818 }
819
820 let bigdir_nid = bigdir_nid.expect("bigdir not found");
821
822 let mut expected: Vec<String> = vec![".".to_string(), "..".to_string()];
824 for i in 0..100 {
825 expected.push(format!("file{i:03}"));
826 }
827 let expected_refs: Vec<&str> = expected.iter().map(|s| s.as_str()).collect();
828
829 validate_directory_entries(&img, bigdir_nid, &expected_refs);
830 }
831
832 #[test]
833 fn test_nested_directories() {
834 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
836/a 4096 40755 2 0 0 0 1000.0 - - -
837/a/b 4096 40755 2 0 0 0 1000.0 - - -
838/a/b/c 4096 40755 2 0 0 0 1000.0 - - -
839/a/b/c/file.txt 5 100644 1 0 0 0 1000.0 - hello -
840"#;
841
842 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
843 let image = mkfs_erofs(&fs);
844 let img = Image::open(&image);
845
846 let root_nid = img.sb.root_nid.get() as u64;
848 validate_directory_entries(&img, root_nid, &[".", "..", "a"]);
849
850 let find_entry = |parent_nid: u64, name: &[u8]| -> u64 {
852 let inode = img.inode(parent_nid);
853
854 if let Some(inline) = inode.inline() {
855 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
856 for entry in inline_block.entries() {
857 if entry.name == name {
858 return entry.nid();
859 }
860 }
861 }
862
863 for blkid in inode.blocks(img.blkszbits) {
864 let block = img.directory_block(blkid);
865 for entry in block.entries() {
866 if entry.name == name {
867 return entry.nid();
868 }
869 }
870 }
871 panic!("Entry not found: {:?}", std::str::from_utf8(name));
872 };
873
874 let a_nid = find_entry(root_nid, b"a");
875 validate_directory_entries(&img, a_nid, &[".", "..", "b"]);
876
877 let b_nid = find_entry(a_nid, b"b");
878 validate_directory_entries(&img, b_nid, &[".", "..", "c"]);
879
880 let c_nid = find_entry(b_nid, b"c");
881 validate_directory_entries(&img, c_nid, &[".", "..", "file.txt"]);
882 }
883
884 #[test]
885 fn test_mixed_entry_types() {
886 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
888/mixed 4096 40755 2 0 0 0 1000.0 - - -
889/mixed/regular 10 100644 1 0 0 0 1000.0 - content123 -
890/mixed/symlink 7 120777 1 0 0 0 1000.0 /target - -
891/mixed/fifo 0 10644 1 0 0 0 1000.0 - - -
892/mixed/subdir 4096 40755 2 0 0 0 1000.0 - - -
893"#;
894
895 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
896 let image = mkfs_erofs(&fs);
897 let img = Image::open(&image);
898
899 let root_inode = img.root();
900 let mut mixed_nid = None;
901 if let Some(inline) = root_inode.inline() {
902 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
903 for entry in inline_block.entries() {
904 if entry.name == b"mixed" {
905 mixed_nid = Some(entry.nid());
906 break;
907 }
908 }
909 }
910 for blkid in root_inode.blocks(img.blkszbits) {
911 let block = img.directory_block(blkid);
912 for entry in block.entries() {
913 if entry.name == b"mixed" {
914 mixed_nid = Some(entry.nid());
915 break;
916 }
917 }
918 }
919
920 let mixed_nid = mixed_nid.expect("mixed not found");
921 validate_directory_entries(
922 &img,
923 mixed_nid,
924 &[".", "..", "regular", "symlink", "fifo", "subdir"],
925 );
926 }
927
928 #[test]
929 fn test_collect_objects_traversal() {
930 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
932/dir1 4096 40755 2 0 0 0 1000.0 - - -
933/dir1/file1 5 100644 1 0 0 0 1000.0 - hello -
934/dir2 4096 40755 2 0 0 0 1000.0 - - -
935/dir2/subdir 4096 40755 2 0 0 0 1000.0 - - -
936/dir2/subdir/file2 5 100644 1 0 0 0 1000.0 - world -
937"#;
938
939 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
940 let image = mkfs_erofs(&fs);
941
942 let result = collect_objects::<Sha256HashValue>(&image);
944 assert!(
945 result.is_ok(),
946 "Failed to collect objects: {:?}",
947 result.err()
948 );
949 }
950
951 #[test]
952 fn test_pr188_empty_inline_directory() -> anyhow::Result<()> {
953 let dumpfile_content = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
969/empty_dir 4096 40755 2 0 0 0 1000.0 - - -
970"#;
971
972 let temp_dir = tempfile::TempDir::new()?;
974 let temp_dir = temp_dir.path();
975 let dumpfile_path = temp_dir.join("pr188_test.dump");
976 let erofs_path = temp_dir.join("pr188_test.erofs");
977
978 std::fs::write(&dumpfile_path, dumpfile_content).expect("Failed to write test dumpfile");
980
981 let output = std::process::Command::new("mkcomposefs")
983 .arg("--from-file")
984 .arg(&dumpfile_path)
985 .arg(&erofs_path)
986 .output()
987 .expect("Failed to run mkcomposefs - is it installed?");
988
989 assert!(
990 output.status.success(),
991 "mkcomposefs failed: {}",
992 String::from_utf8_lossy(&output.stderr)
993 );
994
995 let image = std::fs::read(&erofs_path).expect("Failed to read generated erofs");
997
998 let r = collect_objects::<Sha256HashValue>(&image).unwrap();
1000 assert_eq!(r.len(), 0);
1001
1002 Ok(())
1003 }
1004
1005 #[test]
1006 fn test_round_trip_basic() {
1007 let dumpfile = r#"/ 4096 40755 2 0 0 0 1000.0 - - -
1009/file1 5 100644 1 0 0 0 1000.0 - hello -
1010/file2 6 100644 1 0 0 0 1000.0 - world! -
1011/dir1 4096 40755 2 0 0 0 1000.0 - - -
1012/dir1/nested 8 100644 1 0 0 0 1000.0 - content1 -
1013"#;
1014
1015 let fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
1016 let image = mkfs_erofs(&fs);
1017 let img = Image::open(&image);
1018
1019 let root_nid = img.sb.root_nid.get() as u64;
1021 validate_directory_entries(&img, root_nid, &[".", "..", "file1", "file2", "dir1"]);
1022
1023 let mut entries_map: HashMap<Vec<u8>, u64> = HashMap::new();
1025 let root_inode = img.root();
1026
1027 if let Some(inline) = root_inode.inline() {
1028 let inline_block = DirectoryBlock::ref_from_bytes(inline).unwrap();
1029 for entry in inline_block.entries() {
1030 entries_map.insert(entry.name.to_vec(), entry.nid());
1031 }
1032 }
1033
1034 for blkid in root_inode.blocks(img.blkszbits) {
1035 let block = img.directory_block(blkid);
1036 for entry in block.entries() {
1037 entries_map.insert(entry.name.to_vec(), entry.nid());
1038 }
1039 }
1040
1041 let file1_nid = entries_map
1043 .get(b"file1".as_slice())
1044 .expect("file1 not found");
1045 let file1_inode = img.inode(*file1_nid);
1046 assert!(!file1_inode.mode().is_dir());
1047 assert_eq!(file1_inode.size(), 5);
1048
1049 let inline_data = file1_inode.inline();
1050 assert_eq!(inline_data, Some(b"hello".as_slice()));
1051 }
1052}