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, EncryptionLayer, EncryptionOpen,
19 EncryptionScheme, FileId, FileSystem, FileSystemOpen, FsKind, FsMeta, Layer, NodeAddr,
20 NodeKind, Openers, PathSpec, SmallHex, SnapshotRef, SniffWindow, VfsError, VfsResult,
21 VolumeDesc, VolumeKind, VolumeScheme, 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]
233pub fn default_openers() -> Openers {
234 Openers::new()
235 .filesystem(NtfsProbe)
236 .filesystem(Ext4Probe)
237 .filesystem(XfsProbe)
238 .filesystem(Iso9660Probe)
239 .filesystem(ApfsProbe)
240 .filesystem(HfsPlusProbe)
241 .filesystem(ExFatProbe)
242 .filesystem(FatProbe)
243 .filesystem(UdfProbe)
244 .filesystem(UfsProbe)
245 .filesystem(BtrfsProbe)
246 .volume_system(GptProbe)
247 .volume_system(MbrProbe)
248 .volume_system(ApmProbe)
249 .container(VhdDecoder)
250 .container(Qcow2Decoder)
251 .container(VmdkDecoder)
252 .container(VhdxDecoder)
253 .container(DmgDecoder)
254 .container(Aff4Decoder)
255 .archive(archive_core::ArchiveOpener)
256 .encryption(BitLockerProbe)
257 .encryption(LuksProbe)
258 .encryption(FileVaultProbe)
259 .encryption(VeraCryptProbe)
260}
261
262fn open_base(path: &Path) -> VfsResult<DynSource> {
266 if is_ewf(path) {
267 let reader = ewf::EwfReader::open(path).map_err(|e| VfsError::Bootstrap {
268 stage: "ewf::open",
269 detail: e.to_string(),
270 })?;
271 Ok(Arc::new(reader))
272 } else {
273 Ok(Arc::new(FileSource::open(path)?))
274 }
275}
276
277fn is_ewf(path: &Path) -> bool {
278 path.extension()
279 .and_then(|e| e.to_str())
280 .is_some_and(|e| e.eq_ignore_ascii_case("e01") || e.eq_ignore_ascii_case("ex01"))
281}
282
283struct NtfsProbe;
285
286impl FileSystemOpen for NtfsProbe {
287 fn kind(&self) -> FsKind {
288 FsKind::NTFS
289 }
290
291 fn probe(&self, w: &SniffWindow) -> Confidence {
292 if w.has_magic(3, b"NTFS ") {
294 Confidence::Yes { how: "NTFS OEM id" }
295 } else {
296 Confidence::No
297 }
298 }
299
300 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
301 let len = src.len();
302 let cursor = SourceCursor::new(src, 0, len);
303 let fs = ntfs_core::NtfsFs::open(cursor).map_err(|e| VfsError::Decode {
304 layer: "ntfs",
305 offset: 0,
306 detail: e.to_string(),
307 bytes: SmallHex::new(&[]),
308 })?;
309 Ok(Arc::new(fs))
310 }
311}
312
313struct Ext4Probe;
316
317impl FileSystemOpen for Ext4Probe {
318 fn kind(&self) -> FsKind {
319 FsKind::EXT
320 }
321
322 fn probe(&self, w: &SniffWindow) -> Confidence {
323 if w.has_magic(1080, &[0x53, 0xEF]) {
326 Confidence::Yes {
327 how: "ext2/3/4 superblock magic",
328 }
329 } else {
330 Confidence::No
331 }
332 }
333
334 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
335 let len = src.len();
336 let cursor = SourceCursor::new(src, 0, len);
337 let fs = ext4fs::Ext4Fs::open(cursor).map_err(|e| VfsError::Decode {
338 layer: "ext4",
339 offset: 0,
340 detail: e.to_string(),
341 bytes: SmallHex::new(&[]),
342 })?;
343 Ok(Arc::new(fs))
344 }
345}
346
347struct XfsProbe;
351
352impl FileSystemOpen for XfsProbe {
353 fn kind(&self) -> FsKind {
354 FsKind::XFS
355 }
356
357 fn probe(&self, w: &SniffWindow) -> Confidence {
358 xfs::vfs::xfs_probe(w)
359 }
360
361 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
362 Ok(Arc::new(xfs::vfs::XfsFs::open(&src)?))
363 }
364}
365
366struct Iso9660Probe;
370
371impl FileSystemOpen for Iso9660Probe {
372 fn kind(&self) -> FsKind {
373 FsKind::ISO9660
374 }
375
376 fn probe(&self, w: &SniffWindow) -> Confidence {
377 if w.has_magic(32769, b"CD001") {
380 Confidence::Yes {
381 how: "ISO 9660 CD001 volume descriptor",
382 }
383 } else {
384 Confidence::No
385 }
386 }
387
388 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
389 let len = src.len();
390 let cursor = SourceCursor::new(src, 0, len);
391 let fs = iso::vfs::IsoVfs::open(cursor).map_err(|e| VfsError::Decode {
392 layer: "iso9660",
393 offset: 0,
394 detail: e.to_string(),
395 bytes: SmallHex::new(&[]),
396 })?;
397 Ok(Arc::new(fs))
398 }
399}
400
401struct ApfsProbe;
404
405impl FileSystemOpen for ApfsProbe {
406 fn kind(&self) -> FsKind {
407 FsKind::APFS
408 }
409
410 fn probe(&self, w: &SniffWindow) -> Confidence {
411 if w.has_magic(32, b"NXSB") {
412 Confidence::Yes {
413 how: "APFS NXSB container superblock",
414 }
415 } else {
416 Confidence::No
417 }
418 }
419
420 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
421 let len = src.len();
422 let cursor = SourceCursor::new(src, 0, len);
423 let fs = apfs_core::vfs::ApfsFs::open(cursor).map_err(|e| VfsError::Decode {
424 layer: "apfs",
425 offset: 0,
426 detail: e.to_string(),
427 bytes: SmallHex::new(&[]),
428 })?;
429 Ok(Arc::new(fs))
430 }
431}
432
433struct HfsPlusProbe;
436
437impl FileSystemOpen for HfsPlusProbe {
438 fn kind(&self) -> FsKind {
439 FsKind::HFS_PLUS
440 }
441
442 fn probe(&self, w: &SniffWindow) -> Confidence {
443 match w.at(1024, 2) {
444 Some([0x48, 0x2B | 0x58]) => Confidence::Yes {
445 how: "HFS+/HFSX volume header",
446 },
447 _ => Confidence::No,
448 }
449 }
450
451 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
452 let len = src.len();
455 let mut volume = vec![0u8; usize::try_from(len).unwrap_or(usize::MAX)];
456 let n = src.read_at(0, &mut volume)?;
457 volume.truncate(n);
458 let fs = hfsplus::vfs::HfsFs::new(volume)?;
459 Ok(Arc::new(fs))
460 }
461}
462
463struct ExFatProbe;
467
468impl FileSystemOpen for ExFatProbe {
469 fn kind(&self) -> FsKind {
470 FsKind::EXFAT
471 }
472
473 fn probe(&self, w: &SniffWindow) -> Confidence {
474 if w.at(510, 2) == Some(&[0x55, 0xaa]) && w.has_magic(3, b"EXFAT ") {
475 Confidence::Yes {
476 how: "exFAT boot signature",
477 }
478 } else {
479 Confidence::No
480 }
481 }
482
483 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
484 let len = src.len();
485 let cursor = SourceCursor::new(src, 0, len);
486 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
487 layer: "exfat",
488 offset: 0,
489 detail: e.to_string(),
490 bytes: SmallHex::new(&[]),
491 })?;
492 Ok(Arc::new(fs))
493 }
494}
495
496struct FatProbe;
499
500impl FileSystemOpen for FatProbe {
501 fn kind(&self) -> FsKind {
502 FsKind::FAT
503 }
504
505 fn probe(&self, w: &SniffWindow) -> Confidence {
506 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
507 return Confidence::No;
508 }
509 let jump = w.at(0, 1).and_then(|s| s.first().copied());
510 let jump_ok = matches!(jump, Some(0xEB | 0xE9));
511 let bps = w
512 .at(11, 2)
513 .and_then(|b| <[u8; 2]>::try_from(b).ok())
514 .map_or(0, u16::from_le_bytes);
515 if jump_ok && bps.is_power_of_two() && (512..=4096).contains(&bps) {
516 Confidence::Yes { how: "FAT BPB" }
517 } else {
518 Confidence::No
519 }
520 }
521
522 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
523 let len = src.len();
524 let cursor = SourceCursor::new(src, 0, len);
525 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
526 layer: "fat",
527 offset: 0,
528 detail: e.to_string(),
529 bytes: SmallHex::new(&[]),
530 })?;
531 Ok(Arc::new(fs))
532 }
533}
534
535struct UdfProbe;
542
543impl FileSystemOpen for UdfProbe {
544 fn kind(&self) -> FsKind {
545 FsKind::UDF
546 }
547
548 fn probe(&self, w: &SniffWindow) -> Confidence {
549 if udf_forensic::detect_udf(&mut Cursor::new(w.bytes())) {
550 Confidence::Yes {
551 how: "UDF NSR02/NSR03 volume recognition sequence",
552 }
553 } else {
554 Confidence::No
555 }
556 }
557
558 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
559 let len = src.len();
560 let cursor = SourceCursor::new(src, 0, len);
561 let fs = udf_forensic::vfs::UdfVfs::open(cursor)?;
562 Ok(Arc::new(fs))
563 }
564}
565
566struct UfsProbe;
573
574impl FileSystemOpen for UfsProbe {
575 fn kind(&self) -> FsKind {
576 FsKind::UFS
577 }
578
579 fn probe(&self, w: &SniffWindow) -> Confidence {
580 ufs::vfs::ufs_probe(w)
581 }
582
583 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
584 let fs = ufs::vfs::UfsFs::open(&src)?;
585 Ok(Arc::new(fs))
586 }
587}
588
589const BTRFS_MAGIC_OFFSET: usize = btrfs_core::BTRFS_SUPER_INFO_OFFSET as usize + 0x40;
593
594struct BtrfsProbe;
607
608impl FileSystemOpen for BtrfsProbe {
609 fn kind(&self) -> FsKind {
610 FsKind::BTRFS
611 }
612
613 fn probe(&self, w: &SniffWindow) -> Confidence {
614 if w.has_magic(BTRFS_MAGIC_OFFSET, &btrfs_core::BTRFS_MAGIC) {
615 Confidence::Yes {
616 how: "btrfs _BHRfS_M superblock magic",
617 }
618 } else {
619 Confidence::No
620 }
621 }
622
623 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
624 let fs = btrfs_core::vfs::BtrfsFs::open(&*src)?;
625 Ok(Arc::new(fs))
626 }
627}
628
629struct MbrProbe;
632
633impl VolumeSystemOpen for MbrProbe {
634 fn scheme(&self) -> VolumeScheme {
635 VolumeScheme::Mbr
636 }
637
638 fn probe(&self, w: &SniffWindow) -> Confidence {
639 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
641 return Confidence::No;
642 }
643 let data = w.bytes();
644 for i in 0..4usize {
645 let base = 446 + i * 16;
646 let ptype = data.get(base + 4).copied().unwrap_or(0);
647 let size = le_u32(data, base + 12);
648 if ptype != 0 && ptype != 0xEE && size != 0 {
649 return Confidence::Yes {
650 how: "MBR partition table",
651 };
652 }
653 }
654 Confidence::No
655 }
656
657 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
658 Ok(Box::new(Mbr::parse(src)?))
659 }
660}
661
662struct Mbr {
664 parent: DynSource,
665 volumes: Vec<VolumeDesc>,
666}
667
668impl Mbr {
669 fn parse(src: DynSource) -> VfsResult<Self> {
670 let mut sector = [0u8; 512];
671 src.read_at(0, &mut sector)?;
672 let mut volumes = Vec::new();
673 for i in 0..4usize {
674 let base = 446 + i * 16;
675 let ptype = sector.get(base + 4).copied().unwrap_or(0);
676 let start_lba = le_u32(§or, base + 8);
677 let size = le_u32(§or, base + 12);
678 if ptype == 0 || ptype == 0xEE || size == 0 {
679 continue;
680 }
681 volumes.push(VolumeDesc {
682 index: i,
683 kind: VolumeKind::Partition,
684 start: u64::from(start_lba) * 512,
685 len: u64::from(size) * 512,
686 type_hint: Some(format!("0x{ptype:02x}")),
687 label: None,
688 });
689 }
690 Ok(Self {
691 parent: src,
692 volumes,
693 })
694 }
695}
696
697impl VolumeSystem for Mbr {
698 fn scheme(&self) -> VolumeScheme {
699 VolumeScheme::Mbr
700 }
701
702 fn volumes(&self) -> &[VolumeDesc] {
703 &self.volumes
704 }
705
706 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
707 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
708 what: "mbr volume index",
709 offset: index as u64,
710 len: 1,
711 bound: self.volumes.len() as u64,
712 })?;
713 Ok(Arc::new(SubRange::new(
714 self.parent.clone(),
715 desc.start,
716 desc.len,
717 )))
718 }
719}
720
721struct GptProbe;
725
726impl VolumeSystemOpen for GptProbe {
727 fn scheme(&self) -> VolumeScheme {
728 VolumeScheme::Gpt
729 }
730
731 fn probe(&self, w: &SniffWindow) -> Confidence {
732 if w.has_magic(512, b"EFI PART") {
734 Confidence::Yes {
735 how: "GPT EFI PART header",
736 }
737 } else {
738 Confidence::No
739 }
740 }
741
742 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
743 Ok(Box::new(Gpt::parse(src)?))
744 }
745}
746
747struct Gpt {
749 parent: DynSource,
750 volumes: Vec<VolumeDesc>,
751}
752
753impl Gpt {
754 fn parse(src: DynSource) -> VfsResult<Self> {
755 let mut header = [0u8; 512];
757 src.read_at(512, &mut header)?;
758 if header.get(0..8) != Some(b"EFI PART".as_slice()) {
759 return Err(VfsError::Decode {
760 layer: "gpt",
761 offset: 512,
762 detail: "missing EFI PART signature".to_string(),
763 bytes: SmallHex::new(header.get(0..8).unwrap_or(&[])),
764 });
765 }
766 let entries_lba = le_u64(&header, 72);
767 let num_entries = le_u32(&header, 80).min(256) as usize;
769 let entry_size = le_u32(&header, 84).clamp(128, 512) as usize;
770 let array_len = num_entries.checked_mul(entry_size).unwrap_or(0);
771 let mut arr = vec![0u8; array_len];
772 src.read_at(entries_lba.saturating_mul(512), &mut arr)?;
773
774 let mut volumes = Vec::new();
775 for i in 0..num_entries {
776 let Some(base) = i.checked_mul(entry_size) else {
777 break; };
779 let Some(entry) = arr.get(base..base.saturating_add(entry_size)) else {
780 break; };
782 let type_guid = entry.get(0..16).unwrap_or(&[]);
784 if type_guid.iter().all(|&b| b == 0) {
785 continue;
786 }
787 let first = le_u64(entry, 32);
788 let last = le_u64(entry, 40);
789 if last < first {
790 continue;
791 }
792 let sectors = last - first + 1;
793 volumes.push(VolumeDesc {
794 index: i,
795 kind: VolumeKind::Partition,
796 start: first.saturating_mul(512),
797 len: sectors.saturating_mul(512),
798 type_hint: Some(guid_hint(type_guid)),
799 label: None,
800 });
801 }
802 Ok(Self {
803 parent: src,
804 volumes,
805 })
806 }
807}
808
809impl VolumeSystem for Gpt {
810 fn scheme(&self) -> VolumeScheme {
811 VolumeScheme::Gpt
812 }
813
814 fn volumes(&self) -> &[VolumeDesc] {
815 &self.volumes
816 }
817
818 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
819 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
820 what: "gpt volume index",
821 offset: index as u64,
822 len: 1,
823 bound: self.volumes.len() as u64,
824 })?;
825 Ok(Arc::new(SubRange::new(
826 self.parent.clone(),
827 desc.start,
828 desc.len,
829 )))
830 }
831}
832
833struct ApmProbe;
836
837impl VolumeSystemOpen for ApmProbe {
838 fn scheme(&self) -> VolumeScheme {
839 VolumeScheme::Apm
840 }
841
842 fn probe(&self, w: &SniffWindow) -> Confidence {
843 if w.has_magic(0, b"ER") && w.has_magic(512, b"PM") {
846 Confidence::Yes {
847 how: "Apple Partition Map",
848 }
849 } else {
850 Confidence::No
851 }
852 }
853
854 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
855 Ok(Box::new(Apm::parse(src)?))
856 }
857}
858
859struct Apm {
860 parent: DynSource,
861 volumes: Vec<VolumeDesc>,
862}
863
864const APM_MAP_CAP: u64 = 256 * 1024;
867
868impl Apm {
869 fn parse(src: DynSource) -> VfsResult<Self> {
870 let cap = src.len().clamp(1, APM_MAP_CAP) as usize;
873 let mut head = vec![0u8; cap];
874 let n = src.read_at(0, &mut head)?;
875 let map = apm::parse(head.get(..n).unwrap_or(&[])).ok_or_else(|| VfsError::Decode {
876 layer: "apm",
877 offset: 0,
878 detail: "not an Apple Partition Map".to_string(),
879 bytes: SmallHex::new(head.get(..2).unwrap_or(&[])),
880 })?;
881
882 let block_size = u64::from(map.block_size.max(1));
883 let mut volumes = Vec::new();
884 for (i, part) in map.partitions.iter().enumerate() {
885 if part.type_name.eq_ignore_ascii_case("Apple_partition_map")
887 || part.type_name.eq_ignore_ascii_case("Apple_Free")
888 || part.type_name.eq_ignore_ascii_case("Apple_Void")
889 {
890 continue;
891 }
892 volumes.push(VolumeDesc {
893 index: i,
894 kind: VolumeKind::Partition,
895 start: u64::from(part.start_block) * block_size,
896 len: u64::from(part.block_count) * block_size,
897 type_hint: Some(part.type_name.clone()),
898 label: (!part.name.is_empty()).then(|| part.name.clone()),
899 });
900 }
901
902 Ok(Self {
903 parent: src,
904 volumes,
905 })
906 }
907}
908
909impl VolumeSystem for Apm {
910 fn scheme(&self) -> VolumeScheme {
911 VolumeScheme::Apm
912 }
913
914 fn volumes(&self) -> &[VolumeDesc] {
915 &self.volumes
916 }
917
918 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
919 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
920 what: "apm volume index",
921 offset: index as u64,
922 len: 1,
923 bound: self.volumes.len() as u64,
924 })?;
925 Ok(Arc::new(SubRange::new(
926 self.parent.clone(),
927 desc.start,
928 desc.len,
929 )))
930 }
931}
932
933fn guid_hint(bytes: &[u8]) -> String {
934 use std::fmt::Write as _;
935 let mut s = String::with_capacity(bytes.len() * 2);
936 for b in bytes {
937 let _ = write!(s, "{b:02x}");
938 }
939 s
940}
941
942struct VhdDecoder;
945
946impl ContainerOpen for VhdDecoder {
947 fn format(&self) -> ContainerFormat {
948 ContainerFormat::Vhd
949 }
950
951 fn probe(&self, w: &SniffWindow) -> Confidence {
952 if w.has_magic(0, b"conectix") {
956 Confidence::Yes {
957 how: "VHD conectix footer",
958 }
959 } else {
960 Confidence::No
961 }
962 }
963
964 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
965 let len = src.len();
966 let cursor = SourceCursor::new(src, 0, len);
967 let reader =
968 vhd::VhdReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
969 layer: "vhd",
970 offset: 0,
971 detail: e.to_string(),
972 bytes: SmallHex::new(&[]),
973 })?;
974 let vsize = reader.virtual_disk_size();
975 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
976 }
977}
978
979struct Qcow2Decoder;
982
983impl ContainerOpen for Qcow2Decoder {
984 fn format(&self) -> ContainerFormat {
985 ContainerFormat::Qcow2
986 }
987
988 fn probe(&self, w: &SniffWindow) -> Confidence {
989 if w.has_magic(0, &[0x51, 0x46, 0x49, 0xfb]) {
990 Confidence::Yes { how: "QCOW2 magic" }
991 } else {
992 Confidence::No
993 }
994 }
995
996 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
997 let len = src.len();
998 let cursor = SourceCursor::new(src, 0, len);
999 let reader =
1000 qcow2::Qcow2Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1001 layer: "qcow2",
1002 offset: 0,
1003 detail: e.to_string(),
1004 bytes: SmallHex::new(&[]),
1005 })?;
1006 let vsize = reader.virtual_disk_size();
1007 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1008 }
1009}
1010
1011struct VmdkDecoder;
1015
1016impl ContainerOpen for VmdkDecoder {
1017 fn format(&self) -> ContainerFormat {
1018 ContainerFormat::Vmdk
1019 }
1020
1021 fn probe(&self, w: &SniffWindow) -> Confidence {
1022 if w.has_magic(0, b"KDMV") {
1024 Confidence::Yes {
1025 how: "VMDK KDMV magic",
1026 }
1027 } else {
1028 Confidence::No
1029 }
1030 }
1031
1032 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1033 let len = src.len();
1034 let cursor = SourceCursor::new(src, 0, len);
1035 let boxed: Box<dyn vmdk::ReadSeek + Send> = Box::new(cursor);
1036 let reader = vmdk::VmdkReader::open(boxed).map_err(|e| VfsError::Decode {
1037 layer: "vmdk",
1038 offset: 0,
1039 detail: e.to_string(),
1040 bytes: SmallHex::new(&[]),
1041 })?;
1042 let vsize = reader.virtual_disk_size();
1043 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1044 }
1045}
1046
1047struct VhdxDecoder;
1049
1050impl ContainerOpen for VhdxDecoder {
1051 fn format(&self) -> ContainerFormat {
1052 ContainerFormat::Vhdx
1053 }
1054
1055 fn probe(&self, w: &SniffWindow) -> Confidence {
1056 if w.has_magic(0, vhdx::FILE_MAGIC) {
1057 Confidence::Yes {
1058 how: "VHDX file magic",
1059 }
1060 } else {
1061 Confidence::No
1062 }
1063 }
1064
1065 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1066 let len = src.len();
1067 let cursor = SourceCursor::new(src, 0, len);
1068 let reader =
1069 vhdx::VhdxReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1070 layer: "vhdx",
1071 offset: 0,
1072 detail: e.to_string(),
1073 bytes: SmallHex::new(&[]),
1074 })?;
1075 let vsize = reader.virtual_disk_size();
1076 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1077 }
1078}
1079
1080struct DmgDecoder;
1084
1085impl ContainerOpen for DmgDecoder {
1086 fn format(&self) -> ContainerFormat {
1087 ContainerFormat::Dmg
1088 }
1089
1090 fn probe(&self, w: &SniffWindow) -> Confidence {
1091 if w.has_magic_from_end(512, b"koly") {
1093 Confidence::Yes {
1094 how: "DMG koly trailer",
1095 }
1096 } else {
1097 Confidence::No
1098 }
1099 }
1100
1101 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1102 let len = src.len();
1103 let cursor = SourceCursor::new(src, 0, len);
1104 let reader = dmg::DmgReader::open(cursor).map_err(|e| VfsError::Decode {
1105 layer: "dmg",
1106 offset: 0,
1107 detail: e.to_string(),
1108 bytes: SmallHex::new(&[]),
1109 })?;
1110 let vsize = reader.virtual_disk_size();
1111 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1112 }
1113}
1114
1115struct Aff4Decoder;
1119
1120impl ContainerOpen for Aff4Decoder {
1121 fn format(&self) -> ContainerFormat {
1122 ContainerFormat::Aff4
1123 }
1124
1125 fn probe(&self, w: &SniffWindow) -> Confidence {
1126 if w.has_magic(0, &[0x50, 0x4b, 0x03, 0x04]) {
1127 Confidence::Maybe
1128 } else {
1129 Confidence::No
1130 }
1131 }
1132
1133 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1134 let len = src.len();
1135 let cursor = SourceCursor::new(src, 0, len);
1136 let reader =
1137 aff4::Aff4Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1138 layer: "aff4",
1139 offset: 0,
1140 detail: e.to_string(),
1141 bytes: SmallHex::new(&[]),
1142 })?;
1143 let vsize = reader.virtual_disk_size();
1144 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1145 }
1146}
1147
1148struct BitLockerProbe;
1153
1154impl EncryptionOpen for BitLockerProbe {
1155 fn scheme(&self) -> EncryptionScheme {
1156 EncryptionScheme::Bitlocker
1157 }
1158
1159 fn probe(&self, w: &SniffWindow) -> Confidence {
1160 if w.has_magic(3, b"-FVE-FS-") {
1161 Confidence::Yes {
1162 how: "BitLocker -FVE-FS- signature",
1163 }
1164 } else {
1165 Confidence::No
1166 }
1167 }
1168
1169 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1170 Ok(Box::new(bitlocker::vfs::BitlockerLayer::new(src)))
1171 }
1172}
1173
1174struct LuksProbe;
1180
1181impl EncryptionOpen for LuksProbe {
1182 fn scheme(&self) -> EncryptionScheme {
1183 EncryptionScheme::Luks2
1186 }
1187
1188 fn probe(&self, w: &SniffWindow) -> Confidence {
1189 if w.has_magic(0, &[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]) {
1191 Confidence::Yes { how: "LUKS magic" }
1192 } else {
1193 Confidence::No
1194 }
1195 }
1196
1197 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1198 Ok(Box::new(luks::vfs::LuksLayer::new(src)))
1199 }
1200}
1201
1202struct FileVaultProbe;
1207
1208impl EncryptionOpen for FileVaultProbe {
1209 fn scheme(&self) -> EncryptionScheme {
1210 EncryptionScheme::FileVault
1211 }
1212
1213 fn probe(&self, w: &SniffWindow) -> Confidence {
1214 if w.has_magic(88, b"CS") {
1217 Confidence::Yes {
1218 how: "CoreStorage CS volume header",
1219 }
1220 } else {
1221 Confidence::No
1222 }
1223 }
1224
1225 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1226 Ok(Box::new(filevault::vfs::FileVaultLayer::new(src)))
1227 }
1228}
1229
1230struct VeraCryptProbe;
1236
1237impl EncryptionOpen for VeraCryptProbe {
1238 fn scheme(&self) -> EncryptionScheme {
1239 EncryptionScheme::VeraCrypt
1240 }
1241
1242 fn probe(&self, _w: &SniffWindow) -> Confidence {
1243 Confidence::Maybe
1246 }
1247
1248 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1249 Ok(Box::new(veracrypt::vfs::VeraCryptLayer::new(src)))
1250 }
1251}
1252
1253const WALK_MAX_DEPTH: usize = 256;
1255
1256pub struct WalkEntry {
1259 pub path: Vec<Vec<u8>>,
1260 pub id: FileId,
1261 pub meta: FsMeta,
1262}
1263
1264pub fn walk(fs: &dyn FileSystem) -> VfsResult<Vec<WalkEntry>> {
1269 let mut out = Vec::new();
1270 let mut visited: HashSet<FileId> = HashSet::new();
1271 let mut stack: Vec<(Vec<Vec<u8>>, FileId, usize)> = vec![(Vec::new(), fs.root(), 0)];
1272 while let Some((prefix, dir_id, depth)) = stack.pop() {
1273 if depth > WALK_MAX_DEPTH || !visited.insert(dir_id) {
1274 continue;
1275 }
1276 for entry in fs.read_dir(dir_id)? {
1277 let entry = entry?;
1278 if matches!(entry.name.as_slice(), b"." | b"..") {
1279 continue;
1280 }
1281 let mut path = prefix.clone();
1282 path.push(entry.name);
1283 let meta = fs.meta(entry.id)?;
1284 let is_dir = matches!(meta.kind, NodeKind::Dir);
1285 out.push(WalkEntry {
1286 path: path.clone(),
1287 id: entry.id,
1288 meta,
1289 });
1290 if is_dir {
1291 stack.push((path, entry.id, depth + 1));
1292 }
1293 }
1294 }
1295 Ok(out)
1296}
1297
1298#[cfg(test)]
1299mod tests {
1300 use super::*;
1301 use forensic_vfs::ImageSource;
1302 use std::io::Write;
1303
1304 struct Mem(Vec<u8>);
1305 impl ImageSource for Mem {
1306 fn len(&self) -> u64 {
1307 self.0.len() as u64
1308 }
1309 fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
1310 let off = usize::try_from(offset).unwrap_or(usize::MAX);
1311 let Some(s) = self.0.get(off..) else {
1312 return Ok(0);
1313 };
1314 let n = s.len().min(buf.len());
1315 buf[..n].copy_from_slice(&s[..n]);
1316 Ok(n)
1317 }
1318 }
1319 fn mem(b: Vec<u8>) -> DynSource {
1320 Arc::new(Mem(b))
1321 }
1322 fn window(b: &[u8]) -> SniffWindow<'_> {
1323 SniffWindow::new(0, b)
1324 }
1325
1326 #[test]
1327 fn default_openers_registers_btrfs_ufs_udf() {
1328 let kinds: Vec<FsKind> = default_openers()
1331 .filesystems()
1332 .iter()
1333 .map(|p| p.kind())
1334 .collect();
1335 assert!(kinds.contains(&FsKind::BTRFS), "btrfs prober registered");
1336 assert!(kinds.contains(&FsKind::UFS), "ufs prober registered");
1337 assert!(kinds.contains(&FsKind::UDF), "udf prober registered");
1338 assert_eq!(kinds.len(), 11, "8 existing + 3 new filesystem probers");
1339 }
1340
1341 #[test]
1342 fn default_openers_registers_the_archive_opener() {
1343 assert_eq!(
1347 default_openers().archives().len(),
1348 1,
1349 "the archive opener is registered"
1350 );
1351 }
1352
1353 #[test]
1354 fn default_openers_registers_the_four_encryption_layers() {
1355 let openers = default_openers();
1359 let layers = openers.encryption_layers();
1360 assert_eq!(layers.len(), 4, "4 FDE probers registered");
1361 let schemes: Vec<EncryptionScheme> = layers.iter().map(|p| p.scheme()).collect();
1362 assert!(
1363 schemes.contains(&EncryptionScheme::Bitlocker),
1364 "bitlocker prober registered: {schemes:?}"
1365 );
1366 assert!(
1367 schemes
1368 .iter()
1369 .any(|s| matches!(s, EncryptionScheme::Luks1 | EncryptionScheme::Luks2)),
1370 "luks prober registered: {schemes:?}"
1371 );
1372 assert!(
1373 schemes.contains(&EncryptionScheme::FileVault),
1374 "filevault prober registered: {schemes:?}"
1375 );
1376 assert!(
1377 schemes.contains(&EncryptionScheme::VeraCrypt),
1378 "veracrypt prober registered: {schemes:?}"
1379 );
1380 }
1381
1382 #[test]
1383 fn encryption_probes_detect_their_signatures() {
1384 let mut bde = vec![0u8; 64];
1386 bde[3..11].copy_from_slice(b"-FVE-FS-");
1387 assert!(matches!(
1388 BitLockerProbe.probe(&window(&bde)),
1389 Confidence::Yes { .. }
1390 ));
1391 assert_eq!(BitLockerProbe.probe(&window(&[0u8; 64])), Confidence::No);
1392 assert_eq!(BitLockerProbe.scheme(), EncryptionScheme::Bitlocker);
1393
1394 let mut luks = vec![0u8; 64];
1396 luks[0..6].copy_from_slice(&[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]);
1397 assert!(matches!(
1398 LuksProbe.probe(&window(&luks)),
1399 Confidence::Yes { .. }
1400 ));
1401 assert_eq!(LuksProbe.probe(&window(&[0u8; 64])), Confidence::No);
1402
1403 let mut fv = vec![0u8; 128];
1405 fv[88..90].copy_from_slice(b"CS");
1406 assert!(matches!(
1407 FileVaultProbe.probe(&window(&fv)),
1408 Confidence::Yes { .. }
1409 ));
1410 assert_eq!(FileVaultProbe.probe(&window(&[0u8; 128])), Confidence::No);
1411
1412 assert_eq!(VeraCryptProbe.probe(&window(&[])), Confidence::Maybe);
1414 assert_eq!(
1415 VeraCryptProbe.probe(&window(&[0xffu8; 512])),
1416 Confidence::Maybe
1417 );
1418 assert_eq!(VeraCryptProbe.scheme(), EncryptionScheme::VeraCrypt);
1419 }
1420
1421 #[test]
1422 fn default_is_new_and_probers_report_their_kinds() {
1423 let _ = Vfs::default().open_source(mem(vec![0u8; 64])).unwrap();
1424 assert_eq!(NtfsProbe.kind(), FsKind::NTFS);
1425 assert_eq!(MbrProbe.scheme(), VolumeScheme::Mbr);
1426 assert_eq!(GptProbe.scheme(), VolumeScheme::Gpt);
1427 }
1428
1429 #[test]
1430 fn probers_say_no_on_unrecognized_bytes() {
1431 let empty = window(&[]);
1432 assert_eq!(NtfsProbe.probe(&empty), Confidence::No);
1433 assert_eq!(MbrProbe.probe(&empty), Confidence::No);
1434 assert_eq!(GptProbe.probe(&empty), Confidence::No);
1435 let mut prot = vec![0u8; 512];
1437 prot[446 + 4] = 0xEE;
1438 prot[446 + 12] = 1; prot[510] = 0x55;
1440 prot[511] = 0xaa;
1441 assert_eq!(MbrProbe.probe(&window(&prot)), Confidence::No);
1442 }
1443
1444 #[test]
1445 fn ntfs_magic_but_invalid_boot_is_a_loud_error() {
1446 let mut v = vec![0u8; 4096];
1449 v[3..11].copy_from_slice(b"NTFS ");
1450 assert!(Vfs::new().open_source(mem(v)).is_err());
1451 }
1452
1453 #[test]
1454 fn a_garbage_e01_path_fails_loud() {
1455 let mut f = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
1456 f.write_all(b"not really an EWF image").unwrap();
1457 f.flush().unwrap();
1458 assert!(Vfs::new().open(f.path()).is_err());
1459 }
1460
1461 #[test]
1462 fn gpt_parse_without_signature_errors_and_mbr_volume_index_is_bounded() {
1463 assert!(Gpt::parse(mem(vec![0u8; 1024])).is_err());
1465 let mut d = vec![0u8; 512];
1467 d[446 + 4] = 0x07;
1468 d[446 + 8] = 1; d[446 + 12] = 4; d[510] = 0x55;
1471 d[511] = 0xaa;
1472 let m = Mbr::parse(mem(d)).unwrap();
1473 assert_eq!(m.scheme(), VolumeScheme::Mbr);
1474 assert_eq!(m.volumes().len(), 1);
1475 assert!(m.open_volume(0).is_ok());
1476 assert!(m.open_volume(9).is_err());
1477 }
1478
1479 #[test]
1480 fn apm_maps_partitions_and_errors_on_non_apm() {
1481 let mut img = vec![0u8; 512];
1483 img[0..2].copy_from_slice(b"ER");
1484 img[2..4].copy_from_slice(&512u16.to_be_bytes()); let pm = |map_cnt: u32, pstart: u32, pcnt: u32, ptype: &str| {
1487 let mut e = vec![0u8; 512];
1488 e[0..2].copy_from_slice(b"PM");
1489 e[4..8].copy_from_slice(&map_cnt.to_be_bytes());
1490 e[8..12].copy_from_slice(&pstart.to_be_bytes());
1491 e[0x0c..0x10].copy_from_slice(&pcnt.to_be_bytes());
1492 e[0x30..0x30 + ptype.len()].copy_from_slice(ptype.as_bytes());
1493 e
1494 };
1495 img.extend(pm(2, 1, 63, "Apple_partition_map"));
1497 img.extend(pm(2, 4, 2, "Apple_HFS"));
1498 img.extend(vec![0u8; 4 * 512]);
1499
1500 let apm = Apm::parse(mem(img)).unwrap();
1501 assert_eq!(apm.scheme(), VolumeScheme::Apm);
1502 assert_eq!(apm.volumes().len(), 1); assert_eq!(apm.volumes()[0].start, 4 * 512); assert!(apm.open_volume(0).is_ok());
1505 assert!(apm.open_volume(9).is_err());
1506
1507 assert!(Apm::parse(mem(vec![0u8; 2048])).is_err());
1509 }
1510
1511 #[test]
1512 fn recursion_is_depth_capped_on_a_self_referential_mbr() {
1513 let mut d = vec![0u8; 1024];
1516 d[446 + 4] = 0x83; d[446 + 12] = 2;
1519 d[510] = 0x55;
1520 d[511] = 0xaa;
1521 assert!(Vfs::new().open_source(mem(d)).unwrap().is_none());
1522 }
1523
1524 #[test]
1525 fn container_decoders_report_format_and_error_on_bad_content() {
1526 assert_eq!(VhdDecoder.format(), ContainerFormat::Vhd);
1527 assert_eq!(Qcow2Decoder.format(), ContainerFormat::Qcow2);
1528 assert_eq!(VmdkDecoder.format(), ContainerFormat::Vmdk);
1529 assert_eq!(VhdxDecoder.format(), ContainerFormat::Vhdx);
1530 let mut vhd = vec![0u8; 4096];
1533 vhd[0..8].copy_from_slice(b"conectix");
1534 assert!(Vfs::new().open_source(mem(vhd)).is_err());
1535 let mut q = vec![0u8; 4096];
1536 q[0..4].copy_from_slice(&[0x51, 0x46, 0x49, 0xfb]);
1537 assert!(Vfs::new().open_source(mem(q)).is_err());
1538 let mut v = vec![0u8; 4096];
1539 v[0..4].copy_from_slice(b"KDMV");
1540 assert!(Vfs::new().open_source(mem(v)).is_err());
1541 let mut x = vec![0u8; 4096];
1542 x[0..8].copy_from_slice(vhdx::FILE_MAGIC);
1543 assert!(Vfs::new().open_source(mem(x)).is_err());
1544 }
1545
1546 #[test]
1547 fn dmg_decoder_format_probe_and_open_error() {
1548 assert_eq!(DmgDecoder.format(), ContainerFormat::Dmg);
1549 assert_eq!(
1551 DmgDecoder.probe(&SniffWindow::with_tail(0, &[], 1024, &[0u8; 512])),
1552 Confidence::No
1553 );
1554 let mut v = vec![0u8; 1024];
1559 v[512..516].copy_from_slice(b"koly");
1560 v[512 + 224..512 + 232].copy_from_slice(&u64::MAX.to_be_bytes());
1561 assert!(Vfs::new().open_source(mem(v)).is_err());
1562 }
1563
1564 #[test]
1565 fn aff4_decoder_format_probe_and_open_error() {
1566 assert_eq!(Aff4Decoder.format(), ContainerFormat::Aff4);
1567 assert_eq!(Aff4Decoder.probe(&window(&[])), Confidence::No);
1569 assert_eq!(
1570 Aff4Decoder.probe(&window(&[0x50, 0x4b, 0x03, 0x04])),
1571 Confidence::Maybe
1572 );
1573 let mut v = vec![0u8; 256];
1576 v[0..4].copy_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
1577 assert!(Vfs::new().open_source(mem(v)).is_err());
1578 }
1579
1580 #[test]
1581 fn a_valid_container_holding_no_filesystem_resolves_to_none() {
1582 let vhd = include_bytes!("../tests/data/empty.vhd").to_vec();
1585 assert!(Vfs::new().open_source(mem(vhd)).unwrap().is_none());
1586 }
1587
1588 #[test]
1589 fn ext4_probe_kind_and_open_error() {
1590 assert_eq!(Ext4Probe.kind(), FsKind::EXT);
1591 let mut v = vec![0u8; 4096];
1594 v[1080] = 0x53;
1595 v[1081] = 0xef;
1596 v[1048..1052].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1597 assert!(Vfs::new().open_source(mem(v)).is_err());
1598 }
1599
1600 #[test]
1601 fn iso9660_probe_kind_and_open_error() {
1602 assert_eq!(Iso9660Probe.kind(), FsKind::ISO9660);
1603 assert_eq!(Iso9660Probe.probe(&window(&[])), Confidence::No);
1604 let mut v = vec![0u8; 40 * 1024];
1607 v[32769..32774].copy_from_slice(b"CD001");
1608 assert_eq!(
1609 Iso9660Probe.probe(&window(&v)),
1610 Confidence::Yes {
1611 how: "ISO 9660 CD001 volume descriptor"
1612 }
1613 );
1614 assert!(Vfs::new().open_source(mem(v)).is_err());
1615 }
1616
1617 #[test]
1618 fn apfs_probe_kind_and_open_error() {
1619 assert_eq!(ApfsProbe.kind(), FsKind::APFS);
1620 assert_eq!(ApfsProbe.probe(&window(&[])), Confidence::No);
1621 let mut v = vec![0u8; 40 * 1024];
1624 v[32..36].copy_from_slice(b"NXSB");
1625 assert_eq!(
1626 ApfsProbe.probe(&window(&v)),
1627 Confidence::Yes {
1628 how: "APFS NXSB container superblock"
1629 }
1630 );
1631 assert!(Vfs::new().open_source(mem(v)).is_err());
1632 }
1633
1634 #[test]
1635 fn hfsplus_probe_kind_and_no_on_short_window() {
1636 assert_eq!(HfsPlusProbe.kind(), FsKind::HFS_PLUS);
1637 assert_eq!(HfsPlusProbe.probe(&window(&[])), Confidence::No);
1639 let mut v = vec![0u8; 40 * 1024];
1641 v[1024..1026].copy_from_slice(&[0x48, 0x58]);
1642 assert_eq!(
1643 HfsPlusProbe.probe(&window(&v)),
1644 Confidence::Yes {
1645 how: "HFS+/HFSX volume header"
1646 }
1647 );
1648 }
1649
1650 #[test]
1651 fn fat_and_exfat_magic_but_garbage_are_loud_errors() {
1652 let mut x = vec![0u8; 4096];
1654 x[3..11].copy_from_slice(b"EXFAT ");
1655 x[510] = 0x55;
1656 x[511] = 0xaa;
1657 assert!(Vfs::new().open_source(mem(x)).is_err());
1658
1659 let mut f = vec![0u8; 4096];
1662 f[0] = 0xEB;
1663 f[11..13].copy_from_slice(&512u16.to_le_bytes());
1664 f[510] = 0x55;
1665 f[511] = 0xaa;
1666 assert!(Vfs::new().open_source(mem(f)).is_err());
1667 }
1668
1669 #[test]
1670 fn guid_hint_is_lowercase_hex() {
1671 assert_eq!(guid_hint(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
1672 }
1673
1674 #[test]
1675 fn gpt_parse_skips_unused_and_reversed_entries() {
1676 let mut d = vec![0u8; 1280];
1677 d[512..520].copy_from_slice(b"EFI PART");
1678 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());
1684 d[1024 + 40..1024 + 48].copy_from_slice(&200u64.to_le_bytes());
1685 d[1152] = 0xa2;
1687 d[1152 + 32..1152 + 40].copy_from_slice(&500u64.to_le_bytes());
1688 d[1152 + 40..1152 + 48].copy_from_slice(&400u64.to_le_bytes());
1689 let g = Gpt::parse(mem(d)).unwrap();
1690 assert_eq!(g.scheme(), VolumeScheme::Gpt);
1691 assert_eq!(g.volumes().len(), 1, "reversed entry 1 is skipped");
1692 assert_eq!(g.volumes()[0].start, 100 * 512);
1693 assert!(g.open_volume(0).is_ok());
1694 assert!(g.open_volume(7).is_err());
1695
1696 assert_eq!(Mem(vec![1, 2, 3]).read_at(99, &mut [0u8; 4]).unwrap(), 0);
1698 }
1699
1700 const APFS_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/apfs_volume.bin");
1703 const EXT4_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/ext4.img");
1704
1705 fn apfs_live_xid() -> u64 {
1709 use std::io::{Read, Seek, SeekFrom};
1710 let bytes = std::fs::read(APFS_FIXTURE).unwrap();
1711 let mut c = apfs_core::ApfsContainer::open(std::io::Cursor::new(bytes)).unwrap();
1712 let bs = u64::from(c.superblock().block_size);
1713 let vaddr = c.volume_superblock_addrs().unwrap()[0];
1714 let mut r = c.into_reader();
1715 r.seek(SeekFrom::Start(vaddr * bs)).unwrap();
1716 let mut buf = vec![0u8; bs as usize];
1717 r.read_exact(&mut buf).unwrap();
1718 apfs_core::volume::ApfsVolume::parse(&buf).unwrap().xid()
1719 }
1720
1721 fn zeros_file() -> tempfile::NamedTempFile {
1722 let mut f = tempfile::NamedTempFile::new().unwrap();
1723 f.write_all(&[0u8; 4096]).unwrap();
1724 f.flush().unwrap();
1725 f
1726 }
1727
1728 #[test]
1729 fn epoch_from_create_time_round_trips_and_orders() {
1730 let t = 0x0123_4567_89ab_cdefu64;
1731 let tag = epoch_from_create_time(t);
1732 assert_eq!(&tag.0[0..24], &[0u8; 24], "high 24 bytes are zero");
1733 assert_eq!(
1734 u64::from_be_bytes(tag.0[24..32].try_into().unwrap()),
1735 t,
1736 "create_time round-trips out of the low 8 bytes"
1737 );
1738 assert!(
1739 epoch_from_create_time(t + 1).0 > tag.0,
1740 "a later create_time yields a greater tag"
1741 );
1742 }
1743
1744 #[test]
1745 fn snapshot_view_carries_epoch_and_snapshot_locator() {
1746 let base = PathSpec::os("/ev.dmg");
1747 let v = snapshot_view(&base, 42, "daily".to_string(), 1000);
1748 assert_eq!(v.xid, 42);
1749 assert_eq!(v.name, "daily");
1750 assert_eq!(v.epoch, epoch_from_create_time(1000));
1751 assert!(matches!(
1752 v.locator.layer,
1753 Layer::Snapshot {
1754 store: SnapshotRef::ApfsXid(42)
1755 }
1756 ));
1757 }
1758
1759 #[test]
1760 fn snapshots_on_unrecognized_source_is_empty() {
1761 let f = zeros_file();
1762 assert!(Vfs::new().snapshots(f.path()).unwrap().is_empty());
1763 }
1764
1765 #[test]
1766 fn snapshots_on_non_apfs_filesystem_is_empty() {
1767 assert!(Vfs::new()
1769 .snapshots(Path::new(EXT4_FIXTURE))
1770 .unwrap()
1771 .is_empty());
1772 }
1773
1774 #[test]
1775 fn open_snapshot_without_filesystem_is_bootstrap_error() {
1776 let f = zeros_file();
1777 assert!(matches!(
1778 Vfs::new().open_snapshot(f.path(), 1),
1779 Err(VfsError::Bootstrap { .. })
1780 ));
1781 }
1782
1783 #[test]
1784 fn open_snapshot_on_non_apfs_is_unsupported() {
1785 assert!(matches!(
1786 Vfs::new().open_snapshot(Path::new(EXT4_FIXTURE), 1),
1787 Err(VfsError::Unsupported { .. })
1788 ));
1789 }
1790
1791 #[test]
1792 fn open_snapshot_unknown_xid_is_a_loud_decode_error() {
1793 let bogus = apfs_live_xid().wrapping_add(0xDEAD_BEEF);
1796 assert!(matches!(
1797 Vfs::new().open_snapshot(Path::new(APFS_FIXTURE), bogus),
1798 Err(VfsError::Decode { .. })
1799 ));
1800 }
1801
1802 #[test]
1803 fn open_snapshot_at_live_xid_mounts_and_walks() {
1804 let ev = Vfs::new()
1805 .open_snapshot(Path::new(APFS_FIXTURE), apfs_live_xid())
1806 .expect("open live-xid snapshot");
1807 let uri = ev.root.to_uri();
1808 assert!(
1809 uri.contains("snapshot:apfs") && uri.contains("fs:apfs"),
1810 "locator names the snapshot + APFS layers: {uri}"
1811 );
1812 let fs = ev.fs.expect("snapshot mounts a filesystem");
1813 let names: Vec<String> = walk(fs.as_ref())
1814 .unwrap()
1815 .into_iter()
1816 .filter_map(|e| {
1817 e.path
1818 .last()
1819 .map(|n| String::from_utf8_lossy(n).to_string())
1820 })
1821 .collect();
1822 assert!(names.iter().any(|n| n == "plain.txt"), "walk: {names:?}");
1823 }
1824
1825 #[test]
1828 fn engine_resolution_matches_openers_open_directly() {
1829 let bytes = std::fs::read(EXT4_FIXTURE).unwrap();
1835 let len = bytes.len() as u64;
1836
1837 let via_engine = Vfs::new()
1839 .open_source(mem(bytes.clone()))
1840 .unwrap()
1841 .expect("engine resolves the ext4 fixture");
1842
1843 let base = PathSpec::root(Layer::Range { start: 0, len });
1845 let resolved = default_openers()
1846 .open(mem(bytes), base, 0)
1847 .unwrap()
1848 .expect("Openers::open resolves the ext4 fixture");
1849
1850 let names = |fs: &dyn FileSystem| {
1852 let mut v: Vec<Vec<Vec<u8>>> = walk(fs).unwrap().into_iter().map(|e| e.path).collect();
1853 v.sort();
1854 v
1855 };
1856 assert_eq!(
1857 names(via_engine.as_ref()),
1858 names(resolved.fs.as_ref()),
1859 "engine and Openers::open mount the same filesystem"
1860 );
1861 assert!(
1863 matches!(
1864 resolved.spec.layer,
1865 Layer::Fs {
1866 kind: FsKind::EXT,
1867 ..
1868 }
1869 ),
1870 "registry resolved spec tops with fs:ext: {}",
1871 resolved.spec.to_uri()
1872 );
1873 }
1874}