1#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
9
10use std::collections::HashSet;
11use std::io::Cursor;
12use std::path::Path;
13use std::sync::Arc;
14
15use forensic_vfs::adapters::{FileSource, SeekPoolSource, SourceCursor, SubRange};
16use forensic_vfs::read::{le_u32, le_u64};
17use forensic_vfs::{
18 Confidence, ContainerFormat, ContainerOpen, DynFs, DynSource, FileId, FileSystem,
19 FileSystemOpen, FsKind, FsMeta, Layer, NodeAddr, NodeKind, Openers, PathSpec, SmallHex,
20 SnapshotRef, SniffWindow, VfsError, VfsResult, VolumeDesc, VolumeKind, VolumeScheme,
21 VolumeSystem, VolumeSystemOpen,
22};
23use forensic_vfs_resolver::SourceOpen;
24use state_history_forensic::epoch::EpochTag;
25
26pub struct Evidence {
29 pub root: PathSpec,
31 pub fs: Option<DynFs>,
33}
34
35pub struct Vfs {
37 openers: Openers,
38}
39
40impl Default for Vfs {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl Vfs {
47 #[must_use]
49 pub fn new() -> Self {
50 Self {
51 openers: default_openers(),
52 }
53 }
54
55 pub fn open(&self, path: &Path) -> VfsResult<Evidence> {
60 let base = open_base(path)?;
61 let base_spec = PathSpec::os(path);
62 match self.openers.open(base, base_spec.clone(), 0)? {
63 Some(r) => Ok(Evidence {
64 root: r.spec,
65 fs: Some(r.fs),
66 }),
67 None => Ok(Evidence {
68 root: base_spec,
69 fs: None,
70 }),
71 }
72 }
73
74 pub fn open_source(&self, source: DynSource) -> VfsResult<Option<DynFs>> {
77 let base = PathSpec::root(Layer::Range {
78 start: 0,
79 len: source.len(),
80 });
81 Ok(self.openers.open(source, base, 0)?.map(|r| r.fs))
82 }
83
84 pub fn snapshots(&self, path: &Path) -> VfsResult<Vec<SnapshotView>> {
99 let base = open_base(path)?;
100 let base_spec = PathSpec::os(path);
101 let Some(resolved) = self.openers.open(base, base_spec, 0)? else {
102 return Ok(Vec::new());
103 };
104 if !is_apfs(&resolved.spec) {
105 return Ok(Vec::new());
106 }
107 let source_spec = resolved.source_spec;
108 let len = resolved.source.len();
109 let cursor = SourceCursor::new(resolved.source, 0, len);
110 let snaps = apfs_core::vfs::ApfsFs::snapshots(cursor).map_err(map_apfs_err)?;
111 Ok(snaps
112 .into_iter()
113 .map(|s| snapshot_view(&source_spec, s.xid, s.name, s.create_time))
114 .collect())
115 }
116
117 pub fn open_snapshot(&self, path: &Path, xid: u64) -> VfsResult<Evidence> {
130 let base = open_base(path)?;
131 let base_spec = PathSpec::os(path);
132 let resolved = self
133 .openers
134 .open(base, base_spec, 0)?
135 .ok_or(VfsError::Bootstrap {
136 stage: "apfs snapshot",
137 detail: "no filesystem detected in evidence".to_string(),
138 })?;
139 if !is_apfs(&resolved.spec) {
140 return Err(VfsError::Unsupported {
141 layer: "snapshot",
142 scheme: "non-APFS filesystem has no APFS snapshot".to_string(),
143 });
144 }
145 let source_spec = resolved.source_spec;
146 let len = resolved.source.len();
147 let cursor = SourceCursor::new(resolved.source, 0, len);
148 let fs = apfs_core::vfs::ApfsFs::open_snapshot(cursor, xid).map_err(map_apfs_err)?;
149 let root = source_spec
150 .push(Layer::Snapshot {
151 store: SnapshotRef::ApfsXid(xid),
152 })
153 .push(Layer::Fs {
154 kind: FsKind::APFS,
155 at: NodeAddr::Path(Vec::new()),
156 });
157 Ok(Evidence {
158 root,
159 fs: Some(Arc::new(fs)),
160 })
161 }
162}
163
164#[derive(Debug, Clone)]
168pub struct SnapshotView {
169 pub epoch: EpochTag,
171 pub xid: u64,
173 pub name: String,
175 pub locator: PathSpec,
177}
178
179fn is_apfs(spec: &PathSpec) -> bool {
181 matches!(
182 spec.layer,
183 Layer::Fs {
184 kind: FsKind::APFS,
185 ..
186 }
187 )
188}
189
190fn snapshot_view(source_spec: &PathSpec, xid: u64, name: String, create_time: u64) -> SnapshotView {
195 SnapshotView {
196 epoch: epoch_from_create_time(create_time),
197 xid,
198 name,
199 locator: source_spec.clone().push(Layer::Snapshot {
200 store: SnapshotRef::ApfsXid(xid),
201 }),
202 }
203}
204
205fn epoch_from_create_time(create_time_ns: u64) -> EpochTag {
211 let mut bytes = [0u8; 32];
212 bytes[24..32].copy_from_slice(&create_time_ns.to_be_bytes());
213 EpochTag::from_bytes(bytes)
214}
215
216#[allow(clippy::needless_pass_by_value)]
219fn map_apfs_err(e: apfs_core::ApfsError) -> VfsError {
220 VfsError::Decode {
221 layer: "apfs snapshot",
222 offset: 0,
223 detail: e.to_string(),
224 bytes: SmallHex::new(&[]),
225 }
226}
227
228#[must_use]
232pub fn default_openers() -> Openers {
233 Openers::new()
234 .filesystem(NtfsProbe)
235 .filesystem(Ext4Probe)
236 .filesystem(XfsProbe)
237 .filesystem(Iso9660Probe)
238 .filesystem(ApfsProbe)
239 .filesystem(HfsPlusProbe)
240 .filesystem(ExFatProbe)
241 .filesystem(FatProbe)
242 .filesystem(UdfProbe)
243 .filesystem(UfsProbe)
244 .filesystem(BtrfsProbe)
245 .volume_system(GptProbe)
246 .volume_system(MbrProbe)
247 .volume_system(ApmProbe)
248 .container(VhdDecoder)
249 .container(Qcow2Decoder)
250 .container(VmdkDecoder)
251 .container(VhdxDecoder)
252 .container(DmgDecoder)
253 .container(Aff4Decoder)
254}
255
256fn open_base(path: &Path) -> VfsResult<DynSource> {
260 if is_ewf(path) {
261 let reader = ewf::EwfReader::open(path).map_err(|e| VfsError::Bootstrap {
262 stage: "ewf::open",
263 detail: e.to_string(),
264 })?;
265 Ok(Arc::new(reader))
266 } else {
267 Ok(Arc::new(FileSource::open(path)?))
268 }
269}
270
271fn is_ewf(path: &Path) -> bool {
272 path.extension()
273 .and_then(|e| e.to_str())
274 .is_some_and(|e| e.eq_ignore_ascii_case("e01") || e.eq_ignore_ascii_case("ex01"))
275}
276
277struct NtfsProbe;
279
280impl FileSystemOpen for NtfsProbe {
281 fn kind(&self) -> FsKind {
282 FsKind::NTFS
283 }
284
285 fn probe(&self, w: &SniffWindow) -> Confidence {
286 if w.has_magic(3, b"NTFS ") {
288 Confidence::Yes { how: "NTFS OEM id" }
289 } else {
290 Confidence::No
291 }
292 }
293
294 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
295 let len = src.len();
296 let cursor = SourceCursor::new(src, 0, len);
297 let fs = ntfs_core::NtfsFs::open(cursor).map_err(|e| VfsError::Decode {
298 layer: "ntfs",
299 offset: 0,
300 detail: e.to_string(),
301 bytes: SmallHex::new(&[]),
302 })?;
303 Ok(Arc::new(fs))
304 }
305}
306
307struct Ext4Probe;
310
311impl FileSystemOpen for Ext4Probe {
312 fn kind(&self) -> FsKind {
313 FsKind::EXT
314 }
315
316 fn probe(&self, w: &SniffWindow) -> Confidence {
317 if w.has_magic(1080, &[0x53, 0xEF]) {
320 Confidence::Yes {
321 how: "ext2/3/4 superblock magic",
322 }
323 } else {
324 Confidence::No
325 }
326 }
327
328 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
329 let len = src.len();
330 let cursor = SourceCursor::new(src, 0, len);
331 let fs = ext4fs::Ext4Fs::open(cursor).map_err(|e| VfsError::Decode {
332 layer: "ext4",
333 offset: 0,
334 detail: e.to_string(),
335 bytes: SmallHex::new(&[]),
336 })?;
337 Ok(Arc::new(fs))
338 }
339}
340
341struct XfsProbe;
345
346impl FileSystemOpen for XfsProbe {
347 fn kind(&self) -> FsKind {
348 FsKind::XFS
349 }
350
351 fn probe(&self, w: &SniffWindow) -> Confidence {
352 xfs::vfs::xfs_probe(w)
353 }
354
355 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
356 Ok(Arc::new(xfs::vfs::XfsFs::open(&src)?))
357 }
358}
359
360struct Iso9660Probe;
364
365impl FileSystemOpen for Iso9660Probe {
366 fn kind(&self) -> FsKind {
367 FsKind::ISO9660
368 }
369
370 fn probe(&self, w: &SniffWindow) -> Confidence {
371 if w.has_magic(32769, b"CD001") {
374 Confidence::Yes {
375 how: "ISO 9660 CD001 volume descriptor",
376 }
377 } else {
378 Confidence::No
379 }
380 }
381
382 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
383 let len = src.len();
384 let cursor = SourceCursor::new(src, 0, len);
385 let fs = iso::vfs::IsoVfs::open(cursor).map_err(|e| VfsError::Decode {
386 layer: "iso9660",
387 offset: 0,
388 detail: e.to_string(),
389 bytes: SmallHex::new(&[]),
390 })?;
391 Ok(Arc::new(fs))
392 }
393}
394
395struct ApfsProbe;
398
399impl FileSystemOpen for ApfsProbe {
400 fn kind(&self) -> FsKind {
401 FsKind::APFS
402 }
403
404 fn probe(&self, w: &SniffWindow) -> Confidence {
405 if w.has_magic(32, b"NXSB") {
406 Confidence::Yes {
407 how: "APFS NXSB container superblock",
408 }
409 } else {
410 Confidence::No
411 }
412 }
413
414 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
415 let len = src.len();
416 let cursor = SourceCursor::new(src, 0, len);
417 let fs = apfs_core::vfs::ApfsFs::open(cursor).map_err(|e| VfsError::Decode {
418 layer: "apfs",
419 offset: 0,
420 detail: e.to_string(),
421 bytes: SmallHex::new(&[]),
422 })?;
423 Ok(Arc::new(fs))
424 }
425}
426
427struct HfsPlusProbe;
430
431impl FileSystemOpen for HfsPlusProbe {
432 fn kind(&self) -> FsKind {
433 FsKind::HFS_PLUS
434 }
435
436 fn probe(&self, w: &SniffWindow) -> Confidence {
437 match w.at(1024, 2) {
438 Some([0x48, 0x2B | 0x58]) => Confidence::Yes {
439 how: "HFS+/HFSX volume header",
440 },
441 _ => Confidence::No,
442 }
443 }
444
445 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
446 let len = src.len();
449 let mut volume = vec![0u8; usize::try_from(len).unwrap_or(usize::MAX)];
450 let n = src.read_at(0, &mut volume)?;
451 volume.truncate(n);
452 let fs = hfsplus::vfs::HfsFs::new(volume)?;
453 Ok(Arc::new(fs))
454 }
455}
456
457struct ExFatProbe;
461
462impl FileSystemOpen for ExFatProbe {
463 fn kind(&self) -> FsKind {
464 FsKind::EXFAT
465 }
466
467 fn probe(&self, w: &SniffWindow) -> Confidence {
468 if w.at(510, 2) == Some(&[0x55, 0xaa]) && w.has_magic(3, b"EXFAT ") {
469 Confidence::Yes {
470 how: "exFAT boot signature",
471 }
472 } else {
473 Confidence::No
474 }
475 }
476
477 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
478 let len = src.len();
479 let cursor = SourceCursor::new(src, 0, len);
480 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
481 layer: "exfat",
482 offset: 0,
483 detail: e.to_string(),
484 bytes: SmallHex::new(&[]),
485 })?;
486 Ok(Arc::new(fs))
487 }
488}
489
490struct FatProbe;
493
494impl FileSystemOpen for FatProbe {
495 fn kind(&self) -> FsKind {
496 FsKind::FAT
497 }
498
499 fn probe(&self, w: &SniffWindow) -> Confidence {
500 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
501 return Confidence::No;
502 }
503 let jump = w.at(0, 1).and_then(|s| s.first().copied());
504 let jump_ok = matches!(jump, Some(0xEB | 0xE9));
505 let bps = w
506 .at(11, 2)
507 .and_then(|b| <[u8; 2]>::try_from(b).ok())
508 .map_or(0, u16::from_le_bytes);
509 if jump_ok && bps.is_power_of_two() && (512..=4096).contains(&bps) {
510 Confidence::Yes { how: "FAT BPB" }
511 } else {
512 Confidence::No
513 }
514 }
515
516 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
517 let len = src.len();
518 let cursor = SourceCursor::new(src, 0, len);
519 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
520 layer: "fat",
521 offset: 0,
522 detail: e.to_string(),
523 bytes: SmallHex::new(&[]),
524 })?;
525 Ok(Arc::new(fs))
526 }
527}
528
529struct UdfProbe;
536
537impl FileSystemOpen for UdfProbe {
538 fn kind(&self) -> FsKind {
539 FsKind::UDF
540 }
541
542 fn probe(&self, w: &SniffWindow) -> Confidence {
543 if udf_forensic::detect_udf(&mut Cursor::new(w.bytes())) {
544 Confidence::Yes {
545 how: "UDF NSR02/NSR03 volume recognition sequence",
546 }
547 } else {
548 Confidence::No
549 }
550 }
551
552 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
553 let len = src.len();
554 let cursor = SourceCursor::new(src, 0, len);
555 let fs = udf_forensic::vfs::UdfVfs::open(cursor)?;
556 Ok(Arc::new(fs))
557 }
558}
559
560struct UfsProbe;
567
568impl FileSystemOpen for UfsProbe {
569 fn kind(&self) -> FsKind {
570 FsKind::UFS
571 }
572
573 fn probe(&self, w: &SniffWindow) -> Confidence {
574 ufs::vfs::ufs_probe(w)
575 }
576
577 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
578 let fs = ufs::vfs::UfsFs::open(&src)?;
579 Ok(Arc::new(fs))
580 }
581}
582
583const BTRFS_MAGIC_OFFSET: usize = btrfs_core::BTRFS_SUPER_INFO_OFFSET as usize + 0x40;
587
588struct BtrfsProbe;
601
602impl FileSystemOpen for BtrfsProbe {
603 fn kind(&self) -> FsKind {
604 FsKind::BTRFS
605 }
606
607 fn probe(&self, w: &SniffWindow) -> Confidence {
608 if w.has_magic(BTRFS_MAGIC_OFFSET, &btrfs_core::BTRFS_MAGIC) {
609 Confidence::Yes {
610 how: "btrfs _BHRfS_M superblock magic",
611 }
612 } else {
613 Confidence::No
614 }
615 }
616
617 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
618 let fs = btrfs_core::vfs::BtrfsFs::open(&*src)?;
619 Ok(Arc::new(fs))
620 }
621}
622
623struct MbrProbe;
626
627impl VolumeSystemOpen for MbrProbe {
628 fn scheme(&self) -> VolumeScheme {
629 VolumeScheme::Mbr
630 }
631
632 fn probe(&self, w: &SniffWindow) -> Confidence {
633 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
635 return Confidence::No;
636 }
637 let data = w.bytes();
638 for i in 0..4usize {
639 let base = 446 + i * 16;
640 let ptype = data.get(base + 4).copied().unwrap_or(0);
641 let size = le_u32(data, base + 12);
642 if ptype != 0 && ptype != 0xEE && size != 0 {
643 return Confidence::Yes {
644 how: "MBR partition table",
645 };
646 }
647 }
648 Confidence::No
649 }
650
651 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
652 Ok(Box::new(Mbr::parse(src)?))
653 }
654}
655
656struct Mbr {
658 parent: DynSource,
659 volumes: Vec<VolumeDesc>,
660}
661
662impl Mbr {
663 fn parse(src: DynSource) -> VfsResult<Self> {
664 let mut sector = [0u8; 512];
665 src.read_at(0, &mut sector)?;
666 let mut volumes = Vec::new();
667 for i in 0..4usize {
668 let base = 446 + i * 16;
669 let ptype = sector.get(base + 4).copied().unwrap_or(0);
670 let start_lba = le_u32(§or, base + 8);
671 let size = le_u32(§or, base + 12);
672 if ptype == 0 || ptype == 0xEE || size == 0 {
673 continue;
674 }
675 volumes.push(VolumeDesc {
676 index: i,
677 kind: VolumeKind::Partition,
678 start: u64::from(start_lba) * 512,
679 len: u64::from(size) * 512,
680 type_hint: Some(format!("0x{ptype:02x}")),
681 label: None,
682 });
683 }
684 Ok(Self {
685 parent: src,
686 volumes,
687 })
688 }
689}
690
691impl VolumeSystem for Mbr {
692 fn scheme(&self) -> VolumeScheme {
693 VolumeScheme::Mbr
694 }
695
696 fn volumes(&self) -> &[VolumeDesc] {
697 &self.volumes
698 }
699
700 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
701 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
702 what: "mbr volume index",
703 offset: index as u64,
704 len: 1,
705 bound: self.volumes.len() as u64,
706 })?;
707 Ok(Arc::new(SubRange::new(
708 self.parent.clone(),
709 desc.start,
710 desc.len,
711 )))
712 }
713}
714
715struct GptProbe;
719
720impl VolumeSystemOpen for GptProbe {
721 fn scheme(&self) -> VolumeScheme {
722 VolumeScheme::Gpt
723 }
724
725 fn probe(&self, w: &SniffWindow) -> Confidence {
726 if w.has_magic(512, b"EFI PART") {
728 Confidence::Yes {
729 how: "GPT EFI PART header",
730 }
731 } else {
732 Confidence::No
733 }
734 }
735
736 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
737 Ok(Box::new(Gpt::parse(src)?))
738 }
739}
740
741struct Gpt {
743 parent: DynSource,
744 volumes: Vec<VolumeDesc>,
745}
746
747impl Gpt {
748 fn parse(src: DynSource) -> VfsResult<Self> {
749 let mut header = [0u8; 512];
751 src.read_at(512, &mut header)?;
752 if header.get(0..8) != Some(b"EFI PART".as_slice()) {
753 return Err(VfsError::Decode {
754 layer: "gpt",
755 offset: 512,
756 detail: "missing EFI PART signature".to_string(),
757 bytes: SmallHex::new(header.get(0..8).unwrap_or(&[])),
758 });
759 }
760 let entries_lba = le_u64(&header, 72);
761 let num_entries = le_u32(&header, 80).min(256) as usize;
763 let entry_size = le_u32(&header, 84).clamp(128, 512) as usize;
764 let array_len = num_entries.checked_mul(entry_size).unwrap_or(0);
765 let mut arr = vec![0u8; array_len];
766 src.read_at(entries_lba.saturating_mul(512), &mut arr)?;
767
768 let mut volumes = Vec::new();
769 for i in 0..num_entries {
770 let Some(base) = i.checked_mul(entry_size) else {
771 break; };
773 let Some(entry) = arr.get(base..base.saturating_add(entry_size)) else {
774 break; };
776 let type_guid = entry.get(0..16).unwrap_or(&[]);
778 if type_guid.iter().all(|&b| b == 0) {
779 continue;
780 }
781 let first = le_u64(entry, 32);
782 let last = le_u64(entry, 40);
783 if last < first {
784 continue;
785 }
786 let sectors = last - first + 1;
787 volumes.push(VolumeDesc {
788 index: i,
789 kind: VolumeKind::Partition,
790 start: first.saturating_mul(512),
791 len: sectors.saturating_mul(512),
792 type_hint: Some(guid_hint(type_guid)),
793 label: None,
794 });
795 }
796 Ok(Self {
797 parent: src,
798 volumes,
799 })
800 }
801}
802
803impl VolumeSystem for Gpt {
804 fn scheme(&self) -> VolumeScheme {
805 VolumeScheme::Gpt
806 }
807
808 fn volumes(&self) -> &[VolumeDesc] {
809 &self.volumes
810 }
811
812 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
813 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
814 what: "gpt volume index",
815 offset: index as u64,
816 len: 1,
817 bound: self.volumes.len() as u64,
818 })?;
819 Ok(Arc::new(SubRange::new(
820 self.parent.clone(),
821 desc.start,
822 desc.len,
823 )))
824 }
825}
826
827struct ApmProbe;
830
831impl VolumeSystemOpen for ApmProbe {
832 fn scheme(&self) -> VolumeScheme {
833 VolumeScheme::Apm
834 }
835
836 fn probe(&self, w: &SniffWindow) -> Confidence {
837 if w.has_magic(0, b"ER") && w.has_magic(512, b"PM") {
840 Confidence::Yes {
841 how: "Apple Partition Map",
842 }
843 } else {
844 Confidence::No
845 }
846 }
847
848 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
849 Ok(Box::new(Apm::parse(src)?))
850 }
851}
852
853struct Apm {
854 parent: DynSource,
855 volumes: Vec<VolumeDesc>,
856}
857
858const APM_MAP_CAP: u64 = 256 * 1024;
861
862impl Apm {
863 fn parse(src: DynSource) -> VfsResult<Self> {
864 let cap = src.len().clamp(1, APM_MAP_CAP) as usize;
867 let mut head = vec![0u8; cap];
868 let n = src.read_at(0, &mut head)?;
869 let map = apm::parse(head.get(..n).unwrap_or(&[])).ok_or_else(|| VfsError::Decode {
870 layer: "apm",
871 offset: 0,
872 detail: "not an Apple Partition Map".to_string(),
873 bytes: SmallHex::new(head.get(..2).unwrap_or(&[])),
874 })?;
875
876 let block_size = u64::from(map.block_size.max(1));
877 let mut volumes = Vec::new();
878 for (i, part) in map.partitions.iter().enumerate() {
879 if part.type_name.eq_ignore_ascii_case("Apple_partition_map")
881 || part.type_name.eq_ignore_ascii_case("Apple_Free")
882 || part.type_name.eq_ignore_ascii_case("Apple_Void")
883 {
884 continue;
885 }
886 volumes.push(VolumeDesc {
887 index: i,
888 kind: VolumeKind::Partition,
889 start: u64::from(part.start_block) * block_size,
890 len: u64::from(part.block_count) * block_size,
891 type_hint: Some(part.type_name.clone()),
892 label: (!part.name.is_empty()).then(|| part.name.clone()),
893 });
894 }
895
896 Ok(Self {
897 parent: src,
898 volumes,
899 })
900 }
901}
902
903impl VolumeSystem for Apm {
904 fn scheme(&self) -> VolumeScheme {
905 VolumeScheme::Apm
906 }
907
908 fn volumes(&self) -> &[VolumeDesc] {
909 &self.volumes
910 }
911
912 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
913 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
914 what: "apm volume index",
915 offset: index as u64,
916 len: 1,
917 bound: self.volumes.len() as u64,
918 })?;
919 Ok(Arc::new(SubRange::new(
920 self.parent.clone(),
921 desc.start,
922 desc.len,
923 )))
924 }
925}
926
927fn guid_hint(bytes: &[u8]) -> String {
928 use std::fmt::Write as _;
929 let mut s = String::with_capacity(bytes.len() * 2);
930 for b in bytes {
931 let _ = write!(s, "{b:02x}");
932 }
933 s
934}
935
936struct VhdDecoder;
939
940impl ContainerOpen for VhdDecoder {
941 fn format(&self) -> ContainerFormat {
942 ContainerFormat::Vhd
943 }
944
945 fn probe(&self, w: &SniffWindow) -> Confidence {
946 if w.has_magic(0, b"conectix") {
950 Confidence::Yes {
951 how: "VHD conectix footer",
952 }
953 } else {
954 Confidence::No
955 }
956 }
957
958 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
959 let len = src.len();
960 let cursor = SourceCursor::new(src, 0, len);
961 let reader =
962 vhd::VhdReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
963 layer: "vhd",
964 offset: 0,
965 detail: e.to_string(),
966 bytes: SmallHex::new(&[]),
967 })?;
968 let vsize = reader.virtual_disk_size();
969 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
970 }
971}
972
973struct Qcow2Decoder;
976
977impl ContainerOpen for Qcow2Decoder {
978 fn format(&self) -> ContainerFormat {
979 ContainerFormat::Qcow2
980 }
981
982 fn probe(&self, w: &SniffWindow) -> Confidence {
983 if w.has_magic(0, &[0x51, 0x46, 0x49, 0xfb]) {
984 Confidence::Yes { how: "QCOW2 magic" }
985 } else {
986 Confidence::No
987 }
988 }
989
990 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
991 let len = src.len();
992 let cursor = SourceCursor::new(src, 0, len);
993 let reader =
994 qcow2::Qcow2Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
995 layer: "qcow2",
996 offset: 0,
997 detail: e.to_string(),
998 bytes: SmallHex::new(&[]),
999 })?;
1000 let vsize = reader.virtual_disk_size();
1001 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1002 }
1003}
1004
1005struct VmdkDecoder;
1009
1010impl ContainerOpen for VmdkDecoder {
1011 fn format(&self) -> ContainerFormat {
1012 ContainerFormat::Vmdk
1013 }
1014
1015 fn probe(&self, w: &SniffWindow) -> Confidence {
1016 if w.has_magic(0, b"KDMV") {
1018 Confidence::Yes {
1019 how: "VMDK KDMV magic",
1020 }
1021 } else {
1022 Confidence::No
1023 }
1024 }
1025
1026 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1027 let len = src.len();
1028 let cursor = SourceCursor::new(src, 0, len);
1029 let boxed: Box<dyn vmdk::ReadSeek + Send> = Box::new(cursor);
1030 let reader = vmdk::VmdkReader::open(boxed).map_err(|e| VfsError::Decode {
1031 layer: "vmdk",
1032 offset: 0,
1033 detail: e.to_string(),
1034 bytes: SmallHex::new(&[]),
1035 })?;
1036 let vsize = reader.virtual_disk_size();
1037 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1038 }
1039}
1040
1041struct VhdxDecoder;
1043
1044impl ContainerOpen for VhdxDecoder {
1045 fn format(&self) -> ContainerFormat {
1046 ContainerFormat::Vhdx
1047 }
1048
1049 fn probe(&self, w: &SniffWindow) -> Confidence {
1050 if w.has_magic(0, vhdx::FILE_MAGIC) {
1051 Confidence::Yes {
1052 how: "VHDX file magic",
1053 }
1054 } else {
1055 Confidence::No
1056 }
1057 }
1058
1059 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1060 let len = src.len();
1061 let cursor = SourceCursor::new(src, 0, len);
1062 let reader =
1063 vhdx::VhdxReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1064 layer: "vhdx",
1065 offset: 0,
1066 detail: e.to_string(),
1067 bytes: SmallHex::new(&[]),
1068 })?;
1069 let vsize = reader.virtual_disk_size();
1070 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1071 }
1072}
1073
1074struct DmgDecoder;
1078
1079impl ContainerOpen for DmgDecoder {
1080 fn format(&self) -> ContainerFormat {
1081 ContainerFormat::Dmg
1082 }
1083
1084 fn probe(&self, w: &SniffWindow) -> Confidence {
1085 if w.has_magic_from_end(512, b"koly") {
1087 Confidence::Yes {
1088 how: "DMG koly trailer",
1089 }
1090 } else {
1091 Confidence::No
1092 }
1093 }
1094
1095 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1096 let len = src.len();
1097 let cursor = SourceCursor::new(src, 0, len);
1098 let reader = dmg::DmgReader::open(cursor).map_err(|e| VfsError::Decode {
1099 layer: "dmg",
1100 offset: 0,
1101 detail: e.to_string(),
1102 bytes: SmallHex::new(&[]),
1103 })?;
1104 let vsize = reader.virtual_disk_size();
1105 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1106 }
1107}
1108
1109struct Aff4Decoder;
1113
1114impl ContainerOpen for Aff4Decoder {
1115 fn format(&self) -> ContainerFormat {
1116 ContainerFormat::Aff4
1117 }
1118
1119 fn probe(&self, w: &SniffWindow) -> Confidence {
1120 if w.has_magic(0, &[0x50, 0x4b, 0x03, 0x04]) {
1121 Confidence::Maybe
1122 } else {
1123 Confidence::No
1124 }
1125 }
1126
1127 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1128 let len = src.len();
1129 let cursor = SourceCursor::new(src, 0, len);
1130 let reader =
1131 aff4::Aff4Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1132 layer: "aff4",
1133 offset: 0,
1134 detail: e.to_string(),
1135 bytes: SmallHex::new(&[]),
1136 })?;
1137 let vsize = reader.virtual_disk_size();
1138 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1139 }
1140}
1141
1142const WALK_MAX_DEPTH: usize = 256;
1144
1145pub struct WalkEntry {
1148 pub path: Vec<Vec<u8>>,
1149 pub id: FileId,
1150 pub meta: FsMeta,
1151}
1152
1153pub fn walk(fs: &dyn FileSystem) -> VfsResult<Vec<WalkEntry>> {
1158 let mut out = Vec::new();
1159 let mut visited: HashSet<FileId> = HashSet::new();
1160 let mut stack: Vec<(Vec<Vec<u8>>, FileId, usize)> = vec![(Vec::new(), fs.root(), 0)];
1161 while let Some((prefix, dir_id, depth)) = stack.pop() {
1162 if depth > WALK_MAX_DEPTH || !visited.insert(dir_id) {
1163 continue;
1164 }
1165 for entry in fs.read_dir(dir_id)? {
1166 let entry = entry?;
1167 if matches!(entry.name.as_slice(), b"." | b"..") {
1168 continue;
1169 }
1170 let mut path = prefix.clone();
1171 path.push(entry.name);
1172 let meta = fs.meta(entry.id)?;
1173 let is_dir = matches!(meta.kind, NodeKind::Dir);
1174 out.push(WalkEntry {
1175 path: path.clone(),
1176 id: entry.id,
1177 meta,
1178 });
1179 if is_dir {
1180 stack.push((path, entry.id, depth + 1));
1181 }
1182 }
1183 }
1184 Ok(out)
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189 use super::*;
1190 use forensic_vfs::ImageSource;
1191 use std::io::Write;
1192
1193 struct Mem(Vec<u8>);
1194 impl ImageSource for Mem {
1195 fn len(&self) -> u64 {
1196 self.0.len() as u64
1197 }
1198 fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
1199 let off = usize::try_from(offset).unwrap_or(usize::MAX);
1200 let Some(s) = self.0.get(off..) else {
1201 return Ok(0);
1202 };
1203 let n = s.len().min(buf.len());
1204 buf[..n].copy_from_slice(&s[..n]);
1205 Ok(n)
1206 }
1207 }
1208 fn mem(b: Vec<u8>) -> DynSource {
1209 Arc::new(Mem(b))
1210 }
1211 fn window(b: &[u8]) -> SniffWindow<'_> {
1212 SniffWindow::new(0, b)
1213 }
1214
1215 #[test]
1216 fn default_openers_registers_btrfs_ufs_udf() {
1217 let kinds: Vec<FsKind> = default_openers()
1220 .filesystems()
1221 .iter()
1222 .map(|p| p.kind())
1223 .collect();
1224 assert!(kinds.contains(&FsKind::BTRFS), "btrfs prober registered");
1225 assert!(kinds.contains(&FsKind::UFS), "ufs prober registered");
1226 assert!(kinds.contains(&FsKind::UDF), "udf prober registered");
1227 assert_eq!(kinds.len(), 11, "8 existing + 3 new filesystem probers");
1228 }
1229
1230 #[test]
1231 fn default_is_new_and_probers_report_their_kinds() {
1232 let _ = Vfs::default().open_source(mem(vec![0u8; 64])).unwrap();
1233 assert_eq!(NtfsProbe.kind(), FsKind::NTFS);
1234 assert_eq!(MbrProbe.scheme(), VolumeScheme::Mbr);
1235 assert_eq!(GptProbe.scheme(), VolumeScheme::Gpt);
1236 }
1237
1238 #[test]
1239 fn probers_say_no_on_unrecognized_bytes() {
1240 let empty = window(&[]);
1241 assert_eq!(NtfsProbe.probe(&empty), Confidence::No);
1242 assert_eq!(MbrProbe.probe(&empty), Confidence::No);
1243 assert_eq!(GptProbe.probe(&empty), Confidence::No);
1244 let mut prot = vec![0u8; 512];
1246 prot[446 + 4] = 0xEE;
1247 prot[446 + 12] = 1; prot[510] = 0x55;
1249 prot[511] = 0xaa;
1250 assert_eq!(MbrProbe.probe(&window(&prot)), Confidence::No);
1251 }
1252
1253 #[test]
1254 fn ntfs_magic_but_invalid_boot_is_a_loud_error() {
1255 let mut v = vec![0u8; 4096];
1258 v[3..11].copy_from_slice(b"NTFS ");
1259 assert!(Vfs::new().open_source(mem(v)).is_err());
1260 }
1261
1262 #[test]
1263 fn a_garbage_e01_path_fails_loud() {
1264 let mut f = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
1265 f.write_all(b"not really an EWF image").unwrap();
1266 f.flush().unwrap();
1267 assert!(Vfs::new().open(f.path()).is_err());
1268 }
1269
1270 #[test]
1271 fn gpt_parse_without_signature_errors_and_mbr_volume_index_is_bounded() {
1272 assert!(Gpt::parse(mem(vec![0u8; 1024])).is_err());
1274 let mut d = vec![0u8; 512];
1276 d[446 + 4] = 0x07;
1277 d[446 + 8] = 1; d[446 + 12] = 4; d[510] = 0x55;
1280 d[511] = 0xaa;
1281 let m = Mbr::parse(mem(d)).unwrap();
1282 assert_eq!(m.scheme(), VolumeScheme::Mbr);
1283 assert_eq!(m.volumes().len(), 1);
1284 assert!(m.open_volume(0).is_ok());
1285 assert!(m.open_volume(9).is_err());
1286 }
1287
1288 #[test]
1289 fn apm_maps_partitions_and_errors_on_non_apm() {
1290 let mut img = vec![0u8; 512];
1292 img[0..2].copy_from_slice(b"ER");
1293 img[2..4].copy_from_slice(&512u16.to_be_bytes()); let pm = |map_cnt: u32, pstart: u32, pcnt: u32, ptype: &str| {
1296 let mut e = vec![0u8; 512];
1297 e[0..2].copy_from_slice(b"PM");
1298 e[4..8].copy_from_slice(&map_cnt.to_be_bytes());
1299 e[8..12].copy_from_slice(&pstart.to_be_bytes());
1300 e[0x0c..0x10].copy_from_slice(&pcnt.to_be_bytes());
1301 e[0x30..0x30 + ptype.len()].copy_from_slice(ptype.as_bytes());
1302 e
1303 };
1304 img.extend(pm(2, 1, 63, "Apple_partition_map"));
1306 img.extend(pm(2, 4, 2, "Apple_HFS"));
1307 img.extend(vec![0u8; 4 * 512]);
1308
1309 let apm = Apm::parse(mem(img)).unwrap();
1310 assert_eq!(apm.scheme(), VolumeScheme::Apm);
1311 assert_eq!(apm.volumes().len(), 1); assert_eq!(apm.volumes()[0].start, 4 * 512); assert!(apm.open_volume(0).is_ok());
1314 assert!(apm.open_volume(9).is_err());
1315
1316 assert!(Apm::parse(mem(vec![0u8; 2048])).is_err());
1318 }
1319
1320 #[test]
1321 fn recursion_is_depth_capped_on_a_self_referential_mbr() {
1322 let mut d = vec![0u8; 1024];
1325 d[446 + 4] = 0x83; d[446 + 12] = 2;
1328 d[510] = 0x55;
1329 d[511] = 0xaa;
1330 assert!(Vfs::new().open_source(mem(d)).unwrap().is_none());
1331 }
1332
1333 #[test]
1334 fn container_decoders_report_format_and_error_on_bad_content() {
1335 assert_eq!(VhdDecoder.format(), ContainerFormat::Vhd);
1336 assert_eq!(Qcow2Decoder.format(), ContainerFormat::Qcow2);
1337 assert_eq!(VmdkDecoder.format(), ContainerFormat::Vmdk);
1338 assert_eq!(VhdxDecoder.format(), ContainerFormat::Vhdx);
1339 let mut vhd = vec![0u8; 4096];
1342 vhd[0..8].copy_from_slice(b"conectix");
1343 assert!(Vfs::new().open_source(mem(vhd)).is_err());
1344 let mut q = vec![0u8; 4096];
1345 q[0..4].copy_from_slice(&[0x51, 0x46, 0x49, 0xfb]);
1346 assert!(Vfs::new().open_source(mem(q)).is_err());
1347 let mut v = vec![0u8; 4096];
1348 v[0..4].copy_from_slice(b"KDMV");
1349 assert!(Vfs::new().open_source(mem(v)).is_err());
1350 let mut x = vec![0u8; 4096];
1351 x[0..8].copy_from_slice(vhdx::FILE_MAGIC);
1352 assert!(Vfs::new().open_source(mem(x)).is_err());
1353 }
1354
1355 #[test]
1356 fn dmg_decoder_format_probe_and_open_error() {
1357 assert_eq!(DmgDecoder.format(), ContainerFormat::Dmg);
1358 assert_eq!(
1360 DmgDecoder.probe(&SniffWindow::with_tail(0, &[], 1024, &[0u8; 512])),
1361 Confidence::No
1362 );
1363 let mut v = vec![0u8; 1024];
1368 v[512..516].copy_from_slice(b"koly");
1369 v[512 + 224..512 + 232].copy_from_slice(&u64::MAX.to_be_bytes());
1370 assert!(Vfs::new().open_source(mem(v)).is_err());
1371 }
1372
1373 #[test]
1374 fn aff4_decoder_format_probe_and_open_error() {
1375 assert_eq!(Aff4Decoder.format(), ContainerFormat::Aff4);
1376 assert_eq!(Aff4Decoder.probe(&window(&[])), Confidence::No);
1378 assert_eq!(
1379 Aff4Decoder.probe(&window(&[0x50, 0x4b, 0x03, 0x04])),
1380 Confidence::Maybe
1381 );
1382 let mut v = vec![0u8; 256];
1385 v[0..4].copy_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
1386 assert!(Vfs::new().open_source(mem(v)).is_err());
1387 }
1388
1389 #[test]
1390 fn a_valid_container_holding_no_filesystem_resolves_to_none() {
1391 let vhd = include_bytes!("../tests/data/empty.vhd").to_vec();
1394 assert!(Vfs::new().open_source(mem(vhd)).unwrap().is_none());
1395 }
1396
1397 #[test]
1398 fn ext4_probe_kind_and_open_error() {
1399 assert_eq!(Ext4Probe.kind(), FsKind::EXT);
1400 let mut v = vec![0u8; 4096];
1403 v[1080] = 0x53;
1404 v[1081] = 0xef;
1405 v[1048..1052].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1406 assert!(Vfs::new().open_source(mem(v)).is_err());
1407 }
1408
1409 #[test]
1410 fn iso9660_probe_kind_and_open_error() {
1411 assert_eq!(Iso9660Probe.kind(), FsKind::ISO9660);
1412 assert_eq!(Iso9660Probe.probe(&window(&[])), Confidence::No);
1413 let mut v = vec![0u8; 40 * 1024];
1416 v[32769..32774].copy_from_slice(b"CD001");
1417 assert_eq!(
1418 Iso9660Probe.probe(&window(&v)),
1419 Confidence::Yes {
1420 how: "ISO 9660 CD001 volume descriptor"
1421 }
1422 );
1423 assert!(Vfs::new().open_source(mem(v)).is_err());
1424 }
1425
1426 #[test]
1427 fn apfs_probe_kind_and_open_error() {
1428 assert_eq!(ApfsProbe.kind(), FsKind::APFS);
1429 assert_eq!(ApfsProbe.probe(&window(&[])), Confidence::No);
1430 let mut v = vec![0u8; 40 * 1024];
1433 v[32..36].copy_from_slice(b"NXSB");
1434 assert_eq!(
1435 ApfsProbe.probe(&window(&v)),
1436 Confidence::Yes {
1437 how: "APFS NXSB container superblock"
1438 }
1439 );
1440 assert!(Vfs::new().open_source(mem(v)).is_err());
1441 }
1442
1443 #[test]
1444 fn hfsplus_probe_kind_and_no_on_short_window() {
1445 assert_eq!(HfsPlusProbe.kind(), FsKind::HFS_PLUS);
1446 assert_eq!(HfsPlusProbe.probe(&window(&[])), Confidence::No);
1448 let mut v = vec![0u8; 40 * 1024];
1450 v[1024..1026].copy_from_slice(&[0x48, 0x58]);
1451 assert_eq!(
1452 HfsPlusProbe.probe(&window(&v)),
1453 Confidence::Yes {
1454 how: "HFS+/HFSX volume header"
1455 }
1456 );
1457 }
1458
1459 #[test]
1460 fn fat_and_exfat_magic_but_garbage_are_loud_errors() {
1461 let mut x = vec![0u8; 4096];
1463 x[3..11].copy_from_slice(b"EXFAT ");
1464 x[510] = 0x55;
1465 x[511] = 0xaa;
1466 assert!(Vfs::new().open_source(mem(x)).is_err());
1467
1468 let mut f = vec![0u8; 4096];
1471 f[0] = 0xEB;
1472 f[11..13].copy_from_slice(&512u16.to_le_bytes());
1473 f[510] = 0x55;
1474 f[511] = 0xaa;
1475 assert!(Vfs::new().open_source(mem(f)).is_err());
1476 }
1477
1478 #[test]
1479 fn guid_hint_is_lowercase_hex() {
1480 assert_eq!(guid_hint(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
1481 }
1482
1483 #[test]
1484 fn gpt_parse_skips_unused_and_reversed_entries() {
1485 let mut d = vec![0u8; 1280];
1486 d[512..520].copy_from_slice(b"EFI PART");
1487 d[512 + 72..512 + 80].copy_from_slice(&2u64.to_le_bytes()); d[512 + 80..512 + 84].copy_from_slice(&2u32.to_le_bytes()); d[512 + 84..512 + 88].copy_from_slice(&128u32.to_le_bytes()); d[1024] = 0xa2; d[1024 + 32..1024 + 40].copy_from_slice(&100u64.to_le_bytes());
1493 d[1024 + 40..1024 + 48].copy_from_slice(&200u64.to_le_bytes());
1494 d[1152] = 0xa2;
1496 d[1152 + 32..1152 + 40].copy_from_slice(&500u64.to_le_bytes());
1497 d[1152 + 40..1152 + 48].copy_from_slice(&400u64.to_le_bytes());
1498 let g = Gpt::parse(mem(d)).unwrap();
1499 assert_eq!(g.scheme(), VolumeScheme::Gpt);
1500 assert_eq!(g.volumes().len(), 1, "reversed entry 1 is skipped");
1501 assert_eq!(g.volumes()[0].start, 100 * 512);
1502 assert!(g.open_volume(0).is_ok());
1503 assert!(g.open_volume(7).is_err());
1504
1505 assert_eq!(Mem(vec![1, 2, 3]).read_at(99, &mut [0u8; 4]).unwrap(), 0);
1507 }
1508
1509 const APFS_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/apfs_volume.bin");
1512 const EXT4_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/ext4.img");
1513
1514 fn apfs_live_xid() -> u64 {
1518 use std::io::{Read, Seek, SeekFrom};
1519 let bytes = std::fs::read(APFS_FIXTURE).unwrap();
1520 let mut c = apfs_core::ApfsContainer::open(std::io::Cursor::new(bytes)).unwrap();
1521 let bs = u64::from(c.superblock().block_size);
1522 let vaddr = c.volume_superblock_addrs().unwrap()[0];
1523 let mut r = c.into_reader();
1524 r.seek(SeekFrom::Start(vaddr * bs)).unwrap();
1525 let mut buf = vec![0u8; bs as usize];
1526 r.read_exact(&mut buf).unwrap();
1527 apfs_core::volume::ApfsVolume::parse(&buf).unwrap().xid()
1528 }
1529
1530 fn zeros_file() -> tempfile::NamedTempFile {
1531 let mut f = tempfile::NamedTempFile::new().unwrap();
1532 f.write_all(&[0u8; 4096]).unwrap();
1533 f.flush().unwrap();
1534 f
1535 }
1536
1537 #[test]
1538 fn epoch_from_create_time_round_trips_and_orders() {
1539 let t = 0x0123_4567_89ab_cdefu64;
1540 let tag = epoch_from_create_time(t);
1541 assert_eq!(&tag.0[0..24], &[0u8; 24], "high 24 bytes are zero");
1542 assert_eq!(
1543 u64::from_be_bytes(tag.0[24..32].try_into().unwrap()),
1544 t,
1545 "create_time round-trips out of the low 8 bytes"
1546 );
1547 assert!(
1548 epoch_from_create_time(t + 1).0 > tag.0,
1549 "a later create_time yields a greater tag"
1550 );
1551 }
1552
1553 #[test]
1554 fn snapshot_view_carries_epoch_and_snapshot_locator() {
1555 let base = PathSpec::os("/ev.dmg");
1556 let v = snapshot_view(&base, 42, "daily".to_string(), 1000);
1557 assert_eq!(v.xid, 42);
1558 assert_eq!(v.name, "daily");
1559 assert_eq!(v.epoch, epoch_from_create_time(1000));
1560 assert!(matches!(
1561 v.locator.layer,
1562 Layer::Snapshot {
1563 store: SnapshotRef::ApfsXid(42)
1564 }
1565 ));
1566 }
1567
1568 #[test]
1569 fn snapshots_on_unrecognized_source_is_empty() {
1570 let f = zeros_file();
1571 assert!(Vfs::new().snapshots(f.path()).unwrap().is_empty());
1572 }
1573
1574 #[test]
1575 fn snapshots_on_non_apfs_filesystem_is_empty() {
1576 assert!(Vfs::new()
1578 .snapshots(Path::new(EXT4_FIXTURE))
1579 .unwrap()
1580 .is_empty());
1581 }
1582
1583 #[test]
1584 fn open_snapshot_without_filesystem_is_bootstrap_error() {
1585 let f = zeros_file();
1586 assert!(matches!(
1587 Vfs::new().open_snapshot(f.path(), 1),
1588 Err(VfsError::Bootstrap { .. })
1589 ));
1590 }
1591
1592 #[test]
1593 fn open_snapshot_on_non_apfs_is_unsupported() {
1594 assert!(matches!(
1595 Vfs::new().open_snapshot(Path::new(EXT4_FIXTURE), 1),
1596 Err(VfsError::Unsupported { .. })
1597 ));
1598 }
1599
1600 #[test]
1601 fn open_snapshot_unknown_xid_is_a_loud_decode_error() {
1602 let bogus = apfs_live_xid().wrapping_add(0xDEAD_BEEF);
1605 assert!(matches!(
1606 Vfs::new().open_snapshot(Path::new(APFS_FIXTURE), bogus),
1607 Err(VfsError::Decode { .. })
1608 ));
1609 }
1610
1611 #[test]
1612 fn open_snapshot_at_live_xid_mounts_and_walks() {
1613 let ev = Vfs::new()
1614 .open_snapshot(Path::new(APFS_FIXTURE), apfs_live_xid())
1615 .expect("open live-xid snapshot");
1616 let uri = ev.root.to_uri();
1617 assert!(
1618 uri.contains("snapshot:apfs") && uri.contains("fs:apfs"),
1619 "locator names the snapshot + APFS layers: {uri}"
1620 );
1621 let fs = ev.fs.expect("snapshot mounts a filesystem");
1622 let names: Vec<String> = walk(fs.as_ref())
1623 .unwrap()
1624 .into_iter()
1625 .filter_map(|e| {
1626 e.path
1627 .last()
1628 .map(|n| String::from_utf8_lossy(n).to_string())
1629 })
1630 .collect();
1631 assert!(names.iter().any(|n| n == "plain.txt"), "walk: {names:?}");
1632 }
1633
1634 #[test]
1637 fn engine_resolution_matches_openers_open_directly() {
1638 let bytes = std::fs::read(EXT4_FIXTURE).unwrap();
1644 let len = bytes.len() as u64;
1645
1646 let via_engine = Vfs::new()
1648 .open_source(mem(bytes.clone()))
1649 .unwrap()
1650 .expect("engine resolves the ext4 fixture");
1651
1652 let base = PathSpec::root(Layer::Range { start: 0, len });
1654 let resolved = default_openers()
1655 .open(mem(bytes), base, 0)
1656 .unwrap()
1657 .expect("Openers::open resolves the ext4 fixture");
1658
1659 let names = |fs: &dyn FileSystem| {
1661 let mut v: Vec<Vec<Vec<u8>>> = walk(fs).unwrap().into_iter().map(|e| e.path).collect();
1662 v.sort();
1663 v
1664 };
1665 assert_eq!(
1666 names(via_engine.as_ref()),
1667 names(resolved.fs.as_ref()),
1668 "engine and Openers::open mount the same filesystem"
1669 );
1670 assert!(
1672 matches!(
1673 resolved.spec.layer,
1674 Layer::Fs {
1675 kind: FsKind::EXT,
1676 ..
1677 }
1678 ),
1679 "registry resolved spec tops with fs:ext: {}",
1680 resolved.spec.to_uri()
1681 );
1682 }
1683}