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