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, Locator, NodeAddr,
20 NodeKind, Openers, SmallHex, SnapshotRef, SniffWindow, VfsError, VfsResult, VolumeDesc,
21 VolumeKind, VolumeScheme, VolumeSystem, VolumeSystemOpen,
22};
23use forensic_vfs_resolver::SourceOpen;
24use state_history_forensic::epoch::EpochTag;
25
26pub struct Evidence {
29 pub root: Locator,
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 = Locator::file(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_all(&self, path: &Path) -> VfsResult<Vec<Evidence>> {
94 const HEAD_CAP: u64 = 128 * 1024;
98 const TAIL_CAP: u64 = 4096;
99
100 let base = open_base(path)?;
101 let base_spec = Locator::file(path);
102
103 let total = base.len();
104 let head_len = total.clamp(1, HEAD_CAP) as usize;
105 let mut head = vec![0u8; head_len];
106 let hn = base.read_at(0, &mut head)?;
107 let tail_len = total.min(TAIL_CAP);
108 let mut tail = vec![0u8; tail_len as usize];
109 let tn = base.read_at(total - tail_len, &mut tail)?;
110 let window = SniffWindow::with_tail(
111 0,
112 head.get(..hn).unwrap_or(&[]),
113 total,
114 tail.get(..tn).unwrap_or(&[]),
115 );
116
117 for vsp in self.openers.volume_systems() {
118 if !vsp.probe(&window).is_candidate() {
119 continue;
120 }
121 let vs = vsp.open(base.clone())?;
122 let mut out = Vec::new();
123 for index in 0..vs.volumes().len() {
124 let sub = vs.open_volume(index)?;
125 let child = base_spec.clone().push(Layer::Volume {
126 scheme: vsp.scheme(),
127 index,
128 guid: None,
129 });
130 if let Some(r) = self.openers.open(sub, child, 1)? {
131 out.push(Evidence {
132 root: r.spec,
133 fs: Some(r.fs),
134 });
135 }
136 }
137 return Ok(out);
138 }
139
140 Ok(vec![self.open(path)?])
143 }
144
145 pub fn open_source(&self, source: DynSource) -> VfsResult<Option<DynFs>> {
148 let base = Locator::root(Layer::Range {
149 start: 0,
150 len: source.len(),
151 });
152 Ok(self.openers.open(source, base, 0)?.map(|r| r.fs))
153 }
154
155 pub fn snapshots(&self, path: &Path) -> VfsResult<Vec<SnapshotView>> {
170 let base = open_base(path)?;
171 let base_spec = Locator::file(path);
172 let Some(resolved) = self.openers.open(base, base_spec, 0)? else {
173 return Ok(Vec::new());
174 };
175 if !is_apfs(&resolved.spec) {
176 return Ok(Vec::new());
177 }
178 let source_spec = resolved.source_spec;
179 let len = resolved.source.len();
180 let cursor = SourceCursor::new(resolved.source, 0, len);
181 let snaps = apfs_core::vfs::ApfsFs::snapshots(cursor).map_err(map_apfs_err)?;
182 Ok(snaps
183 .into_iter()
184 .map(|s| snapshot_view(&source_spec, s.xid, s.name, s.create_time))
185 .collect())
186 }
187
188 pub fn open_snapshot(&self, path: &Path, xid: u64) -> VfsResult<Evidence> {
201 let base = open_base(path)?;
202 let base_spec = Locator::file(path);
203 let resolved = self
204 .openers
205 .open(base, base_spec, 0)?
206 .ok_or(VfsError::Bootstrap {
207 stage: "apfs snapshot",
208 detail: "no filesystem detected in evidence".to_string(),
209 })?;
210 if !is_apfs(&resolved.spec) {
211 return Err(VfsError::Unsupported {
212 layer: "snapshot",
213 scheme: "non-APFS filesystem has no APFS snapshot".to_string(),
214 });
215 }
216 let source_spec = resolved.source_spec;
217 let len = resolved.source.len();
218 let cursor = SourceCursor::new(resolved.source, 0, len);
219 let fs = apfs_core::vfs::ApfsFs::open_snapshot(cursor, xid).map_err(map_apfs_err)?;
220 let root = source_spec
221 .push(Layer::Snapshot {
222 store: SnapshotRef::ApfsXid(xid),
223 })
224 .push(Layer::Fs {
225 kind: FsKind::APFS,
226 at: NodeAddr::Path(Vec::new()),
227 });
228 Ok(Evidence {
229 root,
230 fs: Some(Arc::new(fs)),
231 })
232 }
233}
234
235#[derive(Debug, Clone)]
239pub struct SnapshotView {
240 pub epoch: EpochTag,
242 pub xid: u64,
244 pub name: String,
246 pub locator: Locator,
248}
249
250fn is_apfs(spec: &Locator) -> bool {
252 matches!(
253 spec.layer,
254 Layer::Fs {
255 kind: FsKind::APFS,
256 ..
257 }
258 )
259}
260
261fn snapshot_view(source_spec: &Locator, xid: u64, name: String, create_time: u64) -> SnapshotView {
266 SnapshotView {
267 epoch: epoch_from_create_time(create_time),
268 xid,
269 name,
270 locator: source_spec.clone().push(Layer::Snapshot {
271 store: SnapshotRef::ApfsXid(xid),
272 }),
273 }
274}
275
276fn epoch_from_create_time(create_time_ns: u64) -> EpochTag {
282 let mut bytes = [0u8; 32];
283 bytes[24..32].copy_from_slice(&create_time_ns.to_be_bytes());
284 EpochTag::from_bytes(bytes)
285}
286
287#[allow(clippy::needless_pass_by_value)]
290fn map_apfs_err(e: apfs_core::ApfsError) -> VfsError {
291 VfsError::Decode {
292 layer: "apfs snapshot",
293 offset: 0,
294 detail: e.to_string(),
295 bytes: SmallHex::new(&[]),
296 }
297}
298
299#[must_use]
304pub fn default_openers() -> Openers {
305 Openers::new()
306 .filesystem(NtfsProbe)
307 .filesystem(Ext4Probe)
308 .filesystem(XfsProbe)
309 .filesystem(Iso9660Probe)
310 .filesystem(ApfsProbe)
311 .filesystem(HfsPlusProbe)
312 .filesystem(ExFatProbe)
313 .filesystem(FatProbe)
314 .filesystem(UdfProbe)
315 .filesystem(UfsProbe)
316 .filesystem(BtrfsProbe)
317 .volume_system(GptProbe)
318 .volume_system(MbrProbe)
319 .volume_system(ApmProbe)
320 .container(VhdDecoder)
321 .container(Qcow2Decoder)
322 .container(VmdkDecoder)
323 .container(VhdxDecoder)
324 .container(DmgDecoder)
325 .container(Aff4Decoder)
326 .archive(archive_core::ArchiveOpener)
327 .encryption(BitLockerProbe)
328 .encryption(LuksProbe)
329 .encryption(FileVaultProbe)
330 .encryption(VeraCryptProbe)
331}
332
333fn open_base(path: &Path) -> VfsResult<DynSource> {
337 if is_ewf(path) {
338 let reader = ewf::EwfReader::open(path).map_err(|e| VfsError::Bootstrap {
339 stage: "ewf::open",
340 detail: e.to_string(),
341 })?;
342 Ok(Arc::new(reader))
343 } else {
344 Ok(Arc::new(FileSource::open(path)?))
345 }
346}
347
348fn is_ewf(path: &Path) -> bool {
349 path.extension()
350 .and_then(|e| e.to_str())
351 .is_some_and(|e| e.eq_ignore_ascii_case("e01") || e.eq_ignore_ascii_case("ex01"))
352}
353
354struct NtfsProbe;
356
357impl FileSystemOpen for NtfsProbe {
358 fn kind(&self) -> FsKind {
359 FsKind::NTFS
360 }
361
362 fn probe(&self, w: &SniffWindow) -> Confidence {
363 if w.has_magic(3, b"NTFS ") {
365 Confidence::Yes { how: "NTFS OEM id" }
366 } else {
367 Confidence::No
368 }
369 }
370
371 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
372 let len = src.len();
373 let cursor = SourceCursor::new(src, 0, len);
374 let fs = ntfs_core::NtfsFs::open(cursor).map_err(|e| VfsError::Decode {
375 layer: "ntfs",
376 offset: 0,
377 detail: e.to_string(),
378 bytes: SmallHex::new(&[]),
379 })?;
380 Ok(Arc::new(fs))
381 }
382}
383
384struct Ext4Probe;
387
388impl FileSystemOpen for Ext4Probe {
389 fn kind(&self) -> FsKind {
390 FsKind::EXT
391 }
392
393 fn probe(&self, w: &SniffWindow) -> Confidence {
394 if w.has_magic(1080, &[0x53, 0xEF]) {
397 Confidence::Yes {
398 how: "ext2/3/4 superblock magic",
399 }
400 } else {
401 Confidence::No
402 }
403 }
404
405 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
406 let len = src.len();
407 let cursor = SourceCursor::new(src, 0, len);
408 let fs = ext4fs::Ext4Fs::open(cursor).map_err(|e| VfsError::Decode {
409 layer: "ext4",
410 offset: 0,
411 detail: e.to_string(),
412 bytes: SmallHex::new(&[]),
413 })?;
414 Ok(Arc::new(fs))
415 }
416}
417
418struct XfsProbe;
422
423impl FileSystemOpen for XfsProbe {
424 fn kind(&self) -> FsKind {
425 FsKind::XFS
426 }
427
428 fn probe(&self, w: &SniffWindow) -> Confidence {
429 xfs::vfs::xfs_probe(w)
430 }
431
432 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
433 Ok(Arc::new(xfs::vfs::XfsFs::open(&src)?))
434 }
435}
436
437struct Iso9660Probe;
441
442impl FileSystemOpen for Iso9660Probe {
443 fn kind(&self) -> FsKind {
444 FsKind::ISO9660
445 }
446
447 fn probe(&self, w: &SniffWindow) -> Confidence {
448 if w.has_magic(32769, b"CD001") {
451 Confidence::Yes {
452 how: "ISO 9660 CD001 volume descriptor",
453 }
454 } else {
455 Confidence::No
456 }
457 }
458
459 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
460 let len = src.len();
461 let cursor = SourceCursor::new(src, 0, len);
462 let fs = iso::vfs::IsoVfs::open(cursor).map_err(|e| VfsError::Decode {
463 layer: "iso9660",
464 offset: 0,
465 detail: e.to_string(),
466 bytes: SmallHex::new(&[]),
467 })?;
468 Ok(Arc::new(fs))
469 }
470}
471
472struct ApfsProbe;
475
476impl FileSystemOpen for ApfsProbe {
477 fn kind(&self) -> FsKind {
478 FsKind::APFS
479 }
480
481 fn probe(&self, w: &SniffWindow) -> Confidence {
482 if w.has_magic(32, b"NXSB") {
483 Confidence::Yes {
484 how: "APFS NXSB container superblock",
485 }
486 } else {
487 Confidence::No
488 }
489 }
490
491 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
492 let len = src.len();
493 let cursor = SourceCursor::new(src, 0, len);
494 let fs = apfs_core::vfs::ApfsFs::open(cursor).map_err(|e| VfsError::Decode {
495 layer: "apfs",
496 offset: 0,
497 detail: e.to_string(),
498 bytes: SmallHex::new(&[]),
499 })?;
500 Ok(Arc::new(fs))
501 }
502}
503
504struct HfsPlusProbe;
507
508impl FileSystemOpen for HfsPlusProbe {
509 fn kind(&self) -> FsKind {
510 FsKind::HFS_PLUS
511 }
512
513 fn probe(&self, w: &SniffWindow) -> Confidence {
514 match w.at(1024, 2) {
515 Some([0x48, 0x2B | 0x58]) => Confidence::Yes {
516 how: "HFS+/HFSX volume header",
517 },
518 _ => Confidence::No,
519 }
520 }
521
522 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
523 let len = src.len();
526 let mut volume = vec![0u8; usize::try_from(len).unwrap_or(usize::MAX)];
527 let n = src.read_at(0, &mut volume)?;
528 volume.truncate(n);
529 let fs = hfsplus::vfs::HfsFs::new(volume)?;
530 Ok(Arc::new(fs))
531 }
532}
533
534struct ExFatProbe;
538
539impl FileSystemOpen for ExFatProbe {
540 fn kind(&self) -> FsKind {
541 FsKind::EXFAT
542 }
543
544 fn probe(&self, w: &SniffWindow) -> Confidence {
545 if w.at(510, 2) == Some(&[0x55, 0xaa]) && w.has_magic(3, b"EXFAT ") {
546 Confidence::Yes {
547 how: "exFAT boot signature",
548 }
549 } else {
550 Confidence::No
551 }
552 }
553
554 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
555 let len = src.len();
556 let cursor = SourceCursor::new(src, 0, len);
557 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
558 layer: "exfat",
559 offset: 0,
560 detail: e.to_string(),
561 bytes: SmallHex::new(&[]),
562 })?;
563 Ok(Arc::new(fs))
564 }
565}
566
567struct FatProbe;
570
571impl FileSystemOpen for FatProbe {
572 fn kind(&self) -> FsKind {
573 FsKind::FAT
574 }
575
576 fn probe(&self, w: &SniffWindow) -> Confidence {
577 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
578 return Confidence::No;
579 }
580 let jump = w.at(0, 1).and_then(|s| s.first().copied());
581 let jump_ok = matches!(jump, Some(0xEB | 0xE9));
582 let bps = w
583 .at(11, 2)
584 .and_then(|b| <[u8; 2]>::try_from(b).ok())
585 .map_or(0, u16::from_le_bytes);
586 if jump_ok && bps.is_power_of_two() && (512..=4096).contains(&bps) {
587 Confidence::Yes { how: "FAT BPB" }
588 } else {
589 Confidence::No
590 }
591 }
592
593 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
594 let len = src.len();
595 let cursor = SourceCursor::new(src, 0, len);
596 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
597 layer: "fat",
598 offset: 0,
599 detail: e.to_string(),
600 bytes: SmallHex::new(&[]),
601 })?;
602 Ok(Arc::new(fs))
603 }
604}
605
606struct UdfProbe;
613
614impl FileSystemOpen for UdfProbe {
615 fn kind(&self) -> FsKind {
616 FsKind::UDF
617 }
618
619 fn probe(&self, w: &SniffWindow) -> Confidence {
620 if udf_forensic::detect_udf(&mut Cursor::new(w.bytes())) {
621 Confidence::Yes {
622 how: "UDF NSR02/NSR03 volume recognition sequence",
623 }
624 } else {
625 Confidence::No
626 }
627 }
628
629 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
630 let len = src.len();
631 let cursor = SourceCursor::new(src, 0, len);
632 let fs = udf_forensic::vfs::UdfVfs::open(cursor)?;
633 Ok(Arc::new(fs))
634 }
635}
636
637struct UfsProbe;
644
645impl FileSystemOpen for UfsProbe {
646 fn kind(&self) -> FsKind {
647 FsKind::UFS
648 }
649
650 fn probe(&self, w: &SniffWindow) -> Confidence {
651 ufs::vfs::ufs_probe(w)
652 }
653
654 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
655 let fs = ufs::vfs::UfsFs::open(&src)?;
656 Ok(Arc::new(fs))
657 }
658}
659
660const BTRFS_MAGIC_OFFSET: usize = btrfs_core::BTRFS_SUPER_INFO_OFFSET as usize + 0x40;
664
665struct BtrfsProbe;
678
679impl FileSystemOpen for BtrfsProbe {
680 fn kind(&self) -> FsKind {
681 FsKind::BTRFS
682 }
683
684 fn probe(&self, w: &SniffWindow) -> Confidence {
685 if w.has_magic(BTRFS_MAGIC_OFFSET, &btrfs_core::BTRFS_MAGIC) {
686 Confidence::Yes {
687 how: "btrfs _BHRfS_M superblock magic",
688 }
689 } else {
690 Confidence::No
691 }
692 }
693
694 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
695 let fs = btrfs_core::vfs::BtrfsFs::open(&*src)?;
696 Ok(Arc::new(fs))
697 }
698}
699
700struct MbrProbe;
703
704impl VolumeSystemOpen for MbrProbe {
705 fn scheme(&self) -> VolumeScheme {
706 VolumeScheme::Mbr
707 }
708
709 fn probe(&self, w: &SniffWindow) -> Confidence {
710 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
716 return Confidence::No;
717 }
718 if w.has_magic(3, b"EXFAT ") || w.has_magic(3, b"NTFS ") {
723 return Confidence::No;
724 }
725 let jump = w.at(0, 1).and_then(|s| s.first().copied());
726 if matches!(jump, Some(0xEB | 0xE9)) {
727 return Confidence::No;
728 }
729 let data = w.bytes();
730 for i in 0..4usize {
731 let base = 446 + i * 16;
732 let boot_flag = data.get(base).copied().unwrap_or(0xFF);
735 let ptype = data.get(base + 4).copied().unwrap_or(0);
736 let size = le_u32(data, base + 12);
737 if matches!(boot_flag, 0x00 | 0x80) && ptype != 0 && ptype != 0xEE && size != 0 {
738 return Confidence::Yes {
739 how: "MBR partition table",
740 };
741 }
742 }
743 Confidence::No
744 }
745
746 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
747 Ok(Box::new(Mbr::parse(src)?))
748 }
749}
750
751struct Mbr {
753 parent: DynSource,
754 volumes: Vec<VolumeDesc>,
755}
756
757impl Mbr {
758 fn parse(src: DynSource) -> VfsResult<Self> {
759 let mut sector = [0u8; 512];
760 src.read_at(0, &mut sector)?;
761 let mut volumes = Vec::new();
762 for i in 0..4usize {
763 let base = 446 + i * 16;
764 let ptype = sector.get(base + 4).copied().unwrap_or(0);
765 let start_lba = le_u32(§or, base + 8);
766 let size = le_u32(§or, base + 12);
767 if ptype == 0 || ptype == 0xEE || size == 0 {
768 continue;
769 }
770 volumes.push(VolumeDesc {
771 index: i,
772 kind: VolumeKind::Partition,
773 start: u64::from(start_lba) * 512,
774 len: u64::from(size) * 512,
775 type_hint: Some(format!("0x{ptype:02x}")),
776 label: None,
777 });
778 }
779 Ok(Self {
780 parent: src,
781 volumes,
782 })
783 }
784}
785
786impl VolumeSystem for Mbr {
787 fn scheme(&self) -> VolumeScheme {
788 VolumeScheme::Mbr
789 }
790
791 fn volumes(&self) -> &[VolumeDesc] {
792 &self.volumes
793 }
794
795 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
796 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
797 what: "mbr volume index",
798 offset: index as u64,
799 len: 1,
800 bound: self.volumes.len() as u64,
801 })?;
802 Ok(Arc::new(SubRange::new(
803 self.parent.clone(),
804 desc.start,
805 desc.len,
806 )))
807 }
808}
809
810struct GptProbe;
814
815impl VolumeSystemOpen for GptProbe {
816 fn scheme(&self) -> VolumeScheme {
817 VolumeScheme::Gpt
818 }
819
820 fn probe(&self, w: &SniffWindow) -> Confidence {
821 if w.has_magic(512, b"EFI PART") {
823 Confidence::Yes {
824 how: "GPT EFI PART header",
825 }
826 } else {
827 Confidence::No
828 }
829 }
830
831 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
832 Ok(Box::new(Gpt::parse(src)?))
833 }
834}
835
836struct Gpt {
838 parent: DynSource,
839 volumes: Vec<VolumeDesc>,
840}
841
842impl Gpt {
843 fn parse(src: DynSource) -> VfsResult<Self> {
844 let mut header = [0u8; 512];
846 src.read_at(512, &mut header)?;
847 if header.get(0..8) != Some(b"EFI PART".as_slice()) {
848 return Err(VfsError::Decode {
849 layer: "gpt",
850 offset: 512,
851 detail: "missing EFI PART signature".to_string(),
852 bytes: SmallHex::new(header.get(0..8).unwrap_or(&[])),
853 });
854 }
855 let entries_lba = le_u64(&header, 72);
856 let num_entries = le_u32(&header, 80).min(256) as usize;
858 let entry_size = le_u32(&header, 84).clamp(128, 512) as usize;
859 let array_len = num_entries.checked_mul(entry_size).unwrap_or(0);
860 let mut arr = vec![0u8; array_len];
861 src.read_at(entries_lba.saturating_mul(512), &mut arr)?;
862
863 let mut volumes = Vec::new();
864 for i in 0..num_entries {
865 let Some(base) = i.checked_mul(entry_size) else {
866 break; };
868 let Some(entry) = arr.get(base..base.saturating_add(entry_size)) else {
869 break; };
871 let type_guid = entry.get(0..16).unwrap_or(&[]);
873 if type_guid.iter().all(|&b| b == 0) {
874 continue;
875 }
876 let first = le_u64(entry, 32);
877 let last = le_u64(entry, 40);
878 if last < first {
879 continue;
880 }
881 let sectors = last - first + 1;
882 volumes.push(VolumeDesc {
883 index: i,
884 kind: VolumeKind::Partition,
885 start: first.saturating_mul(512),
886 len: sectors.saturating_mul(512),
887 type_hint: Some(guid_hint(type_guid)),
888 label: None,
889 });
890 }
891 Ok(Self {
892 parent: src,
893 volumes,
894 })
895 }
896}
897
898impl VolumeSystem for Gpt {
899 fn scheme(&self) -> VolumeScheme {
900 VolumeScheme::Gpt
901 }
902
903 fn volumes(&self) -> &[VolumeDesc] {
904 &self.volumes
905 }
906
907 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
908 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
909 what: "gpt volume index",
910 offset: index as u64,
911 len: 1,
912 bound: self.volumes.len() as u64,
913 })?;
914 Ok(Arc::new(SubRange::new(
915 self.parent.clone(),
916 desc.start,
917 desc.len,
918 )))
919 }
920}
921
922struct ApmProbe;
925
926impl VolumeSystemOpen for ApmProbe {
927 fn scheme(&self) -> VolumeScheme {
928 VolumeScheme::Apm
929 }
930
931 fn probe(&self, w: &SniffWindow) -> Confidence {
932 if w.has_magic(0, b"ER") && w.has_magic(512, b"PM") {
935 Confidence::Yes {
936 how: "Apple Partition Map",
937 }
938 } else {
939 Confidence::No
940 }
941 }
942
943 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
944 Ok(Box::new(Apm::parse(src)?))
945 }
946}
947
948struct Apm {
949 parent: DynSource,
950 volumes: Vec<VolumeDesc>,
951}
952
953const APM_MAP_CAP: u64 = 256 * 1024;
956
957impl Apm {
958 fn parse(src: DynSource) -> VfsResult<Self> {
959 let cap = src.len().clamp(1, APM_MAP_CAP) as usize;
962 let mut head = vec![0u8; cap];
963 let n = src.read_at(0, &mut head)?;
964 let map = apm::parse(head.get(..n).unwrap_or(&[])).ok_or_else(|| VfsError::Decode {
965 layer: "apm",
966 offset: 0,
967 detail: "not an Apple Partition Map".to_string(),
968 bytes: SmallHex::new(head.get(..2).unwrap_or(&[])),
969 })?;
970
971 let block_size = u64::from(map.block_size.max(1));
972 let mut volumes = Vec::new();
973 for (i, part) in map.partitions.iter().enumerate() {
974 if part.type_name.eq_ignore_ascii_case("Apple_partition_map")
976 || part.type_name.eq_ignore_ascii_case("Apple_Free")
977 || part.type_name.eq_ignore_ascii_case("Apple_Void")
978 {
979 continue;
980 }
981 volumes.push(VolumeDesc {
982 index: i,
983 kind: VolumeKind::Partition,
984 start: u64::from(part.start_block) * block_size,
985 len: u64::from(part.block_count) * block_size,
986 type_hint: Some(part.type_name.clone()),
987 label: (!part.name.is_empty()).then(|| part.name.clone()),
988 });
989 }
990
991 Ok(Self {
992 parent: src,
993 volumes,
994 })
995 }
996}
997
998impl VolumeSystem for Apm {
999 fn scheme(&self) -> VolumeScheme {
1000 VolumeScheme::Apm
1001 }
1002
1003 fn volumes(&self) -> &[VolumeDesc] {
1004 &self.volumes
1005 }
1006
1007 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
1008 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
1009 what: "apm volume index",
1010 offset: index as u64,
1011 len: 1,
1012 bound: self.volumes.len() as u64,
1013 })?;
1014 Ok(Arc::new(SubRange::new(
1015 self.parent.clone(),
1016 desc.start,
1017 desc.len,
1018 )))
1019 }
1020}
1021
1022fn guid_hint(bytes: &[u8]) -> String {
1023 use std::fmt::Write as _;
1024 let mut s = String::with_capacity(bytes.len() * 2);
1025 for b in bytes {
1026 let _ = write!(s, "{b:02x}");
1027 }
1028 s
1029}
1030
1031struct VhdDecoder;
1034
1035impl ContainerOpen for VhdDecoder {
1036 fn format(&self) -> ContainerFormat {
1037 ContainerFormat::Vhd
1038 }
1039
1040 fn probe(&self, w: &SniffWindow) -> Confidence {
1041 if w.has_magic(0, b"conectix") {
1045 Confidence::Yes {
1046 how: "VHD conectix footer",
1047 }
1048 } else {
1049 Confidence::No
1050 }
1051 }
1052
1053 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1054 let len = src.len();
1055 let cursor = SourceCursor::new(src, 0, len);
1056 let reader =
1057 vhd::VhdReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1058 layer: "vhd",
1059 offset: 0,
1060 detail: e.to_string(),
1061 bytes: SmallHex::new(&[]),
1062 })?;
1063 let vsize = reader.virtual_disk_size();
1064 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1065 }
1066}
1067
1068struct Qcow2Decoder;
1071
1072impl ContainerOpen for Qcow2Decoder {
1073 fn format(&self) -> ContainerFormat {
1074 ContainerFormat::Qcow2
1075 }
1076
1077 fn probe(&self, w: &SniffWindow) -> Confidence {
1078 if w.has_magic(0, &[0x51, 0x46, 0x49, 0xfb]) {
1079 Confidence::Yes { how: "QCOW2 magic" }
1080 } else {
1081 Confidence::No
1082 }
1083 }
1084
1085 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1086 let len = src.len();
1087 let cursor = SourceCursor::new(src, 0, len);
1088 let reader =
1089 qcow2::Qcow2Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1090 layer: "qcow2",
1091 offset: 0,
1092 detail: e.to_string(),
1093 bytes: SmallHex::new(&[]),
1094 })?;
1095 let vsize = reader.virtual_disk_size();
1096 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1097 }
1098}
1099
1100struct VmdkDecoder;
1104
1105impl ContainerOpen for VmdkDecoder {
1106 fn format(&self) -> ContainerFormat {
1107 ContainerFormat::Vmdk
1108 }
1109
1110 fn probe(&self, w: &SniffWindow) -> Confidence {
1111 if w.has_magic(0, b"KDMV") {
1113 Confidence::Yes {
1114 how: "VMDK KDMV magic",
1115 }
1116 } else {
1117 Confidence::No
1118 }
1119 }
1120
1121 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1122 let len = src.len();
1123 let cursor = SourceCursor::new(src, 0, len);
1124 let boxed: Box<dyn vmdk::ReadSeek + Send> = Box::new(cursor);
1125 let reader = vmdk::VmdkReader::open(boxed).map_err(|e| VfsError::Decode {
1126 layer: "vmdk",
1127 offset: 0,
1128 detail: e.to_string(),
1129 bytes: SmallHex::new(&[]),
1130 })?;
1131 let vsize = reader.virtual_disk_size();
1132 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1133 }
1134}
1135
1136struct VhdxDecoder;
1138
1139impl ContainerOpen for VhdxDecoder {
1140 fn format(&self) -> ContainerFormat {
1141 ContainerFormat::Vhdx
1142 }
1143
1144 fn probe(&self, w: &SniffWindow) -> Confidence {
1145 if w.has_magic(0, vhdx::FILE_MAGIC) {
1146 Confidence::Yes {
1147 how: "VHDX file magic",
1148 }
1149 } else {
1150 Confidence::No
1151 }
1152 }
1153
1154 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1155 let len = src.len();
1156 let cursor = SourceCursor::new(src, 0, len);
1157 let reader =
1158 vhdx::VhdxReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1159 layer: "vhdx",
1160 offset: 0,
1161 detail: e.to_string(),
1162 bytes: SmallHex::new(&[]),
1163 })?;
1164 let vsize = reader.virtual_disk_size();
1165 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1166 }
1167}
1168
1169struct DmgDecoder;
1173
1174impl ContainerOpen for DmgDecoder {
1175 fn format(&self) -> ContainerFormat {
1176 ContainerFormat::Dmg
1177 }
1178
1179 fn probe(&self, w: &SniffWindow) -> Confidence {
1180 if w.has_magic_from_end(512, b"koly") {
1182 Confidence::Yes {
1183 how: "DMG koly trailer",
1184 }
1185 } else {
1186 Confidence::No
1187 }
1188 }
1189
1190 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1191 let len = src.len();
1192 let cursor = SourceCursor::new(src, 0, len);
1193 let reader = dmg::DmgReader::open(cursor).map_err(|e| VfsError::Decode {
1194 layer: "dmg",
1195 offset: 0,
1196 detail: e.to_string(),
1197 bytes: SmallHex::new(&[]),
1198 })?;
1199 let vsize = reader.virtual_disk_size();
1200 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1201 }
1202}
1203
1204struct Aff4Decoder;
1208
1209impl ContainerOpen for Aff4Decoder {
1210 fn format(&self) -> ContainerFormat {
1211 ContainerFormat::Aff4
1212 }
1213
1214 fn probe(&self, w: &SniffWindow) -> Confidence {
1215 if w.has_magic(0, &[0x50, 0x4b, 0x03, 0x04]) {
1216 Confidence::Maybe
1217 } else {
1218 Confidence::No
1219 }
1220 }
1221
1222 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1223 let len = src.len();
1224 let cursor = SourceCursor::new(src, 0, len);
1225 let reader =
1226 aff4::Aff4Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1227 layer: "aff4",
1228 offset: 0,
1229 detail: e.to_string(),
1230 bytes: SmallHex::new(&[]),
1231 })?;
1232 let vsize = reader.virtual_disk_size();
1233 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1234 }
1235}
1236
1237struct BitLockerProbe;
1242
1243impl EncryptionOpen for BitLockerProbe {
1244 fn scheme(&self) -> EncryptionScheme {
1245 EncryptionScheme::Bitlocker
1246 }
1247
1248 fn probe(&self, w: &SniffWindow) -> Confidence {
1249 if w.has_magic(3, b"-FVE-FS-") {
1250 Confidence::Yes {
1251 how: "BitLocker -FVE-FS- signature",
1252 }
1253 } else {
1254 Confidence::No
1255 }
1256 }
1257
1258 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1259 Ok(Box::new(bitlocker::vfs::BitlockerLayer::new(src)))
1260 }
1261}
1262
1263struct LuksProbe;
1269
1270impl EncryptionOpen for LuksProbe {
1271 fn scheme(&self) -> EncryptionScheme {
1272 EncryptionScheme::Luks2
1275 }
1276
1277 fn probe(&self, w: &SniffWindow) -> Confidence {
1278 if w.has_magic(0, &[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]) {
1280 Confidence::Yes { how: "LUKS magic" }
1281 } else {
1282 Confidence::No
1283 }
1284 }
1285
1286 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1287 Ok(Box::new(luks::vfs::LuksLayer::new(src)))
1288 }
1289}
1290
1291struct FileVaultProbe;
1296
1297impl EncryptionOpen for FileVaultProbe {
1298 fn scheme(&self) -> EncryptionScheme {
1299 EncryptionScheme::FileVault
1300 }
1301
1302 fn probe(&self, w: &SniffWindow) -> Confidence {
1303 if w.has_magic(88, b"CS") {
1306 Confidence::Yes {
1307 how: "CoreStorage CS volume header",
1308 }
1309 } else {
1310 Confidence::No
1311 }
1312 }
1313
1314 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1315 Ok(Box::new(filevault::vfs::FileVaultLayer::new(src)))
1316 }
1317}
1318
1319struct VeraCryptProbe;
1325
1326impl EncryptionOpen for VeraCryptProbe {
1327 fn scheme(&self) -> EncryptionScheme {
1328 EncryptionScheme::VeraCrypt
1329 }
1330
1331 fn probe(&self, _w: &SniffWindow) -> Confidence {
1332 Confidence::Maybe
1335 }
1336
1337 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1338 Ok(Box::new(veracrypt::vfs::VeraCryptLayer::new(src)))
1339 }
1340}
1341
1342const WALK_MAX_DEPTH: usize = 256;
1344
1345pub struct WalkEntry {
1348 pub path: Vec<Vec<u8>>,
1349 pub id: FileId,
1350 pub meta: FsMeta,
1351}
1352
1353pub fn walk(fs: &dyn FileSystem) -> VfsResult<Vec<WalkEntry>> {
1358 let mut out = Vec::new();
1359 let mut visited: HashSet<FileId> = HashSet::new();
1360 let mut stack: Vec<(Vec<Vec<u8>>, FileId, usize)> = vec![(Vec::new(), fs.root(), 0)];
1361 while let Some((prefix, dir_id, depth)) = stack.pop() {
1362 if depth > WALK_MAX_DEPTH || !visited.insert(dir_id) {
1363 continue;
1364 }
1365 for entry in fs.read_dir(dir_id)? {
1366 let entry = entry?;
1367 if matches!(entry.name.as_slice(), b"." | b"..") {
1368 continue;
1369 }
1370 let mut path = prefix.clone();
1371 path.push(entry.name);
1372 let meta = fs.meta(entry.id)?;
1373 let is_dir = matches!(meta.kind, NodeKind::Dir);
1374 out.push(WalkEntry {
1375 path: path.clone(),
1376 id: entry.id,
1377 meta,
1378 });
1379 if is_dir {
1380 stack.push((path, entry.id, depth + 1));
1381 }
1382 }
1383 }
1384 Ok(out)
1385}
1386
1387#[cfg(test)]
1388mod tests {
1389 use super::*;
1390 use forensic_vfs::ImageSource;
1391 use std::io::Write;
1392
1393 struct Mem(Vec<u8>);
1394 impl ImageSource for Mem {
1395 fn len(&self) -> u64 {
1396 self.0.len() as u64
1397 }
1398 fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
1399 let off = usize::try_from(offset).unwrap_or(usize::MAX);
1400 let Some(s) = self.0.get(off..) else {
1401 return Ok(0);
1402 };
1403 let n = s.len().min(buf.len());
1404 buf[..n].copy_from_slice(&s[..n]);
1405 Ok(n)
1406 }
1407 }
1408 fn mem(b: Vec<u8>) -> DynSource {
1409 Arc::new(Mem(b))
1410 }
1411 fn window(b: &[u8]) -> SniffWindow<'_> {
1412 SniffWindow::new(0, b)
1413 }
1414
1415 #[test]
1416 fn default_openers_registers_btrfs_ufs_udf() {
1417 let kinds: Vec<FsKind> = default_openers()
1420 .filesystems()
1421 .iter()
1422 .map(|p| p.kind())
1423 .collect();
1424 assert!(kinds.contains(&FsKind::BTRFS), "btrfs prober registered");
1425 assert!(kinds.contains(&FsKind::UFS), "ufs prober registered");
1426 assert!(kinds.contains(&FsKind::UDF), "udf prober registered");
1427 assert_eq!(kinds.len(), 11, "8 existing + 3 new filesystem probers");
1428 }
1429
1430 #[test]
1431 fn default_openers_registers_the_archive_opener() {
1432 assert_eq!(
1436 default_openers().archives().len(),
1437 1,
1438 "the archive opener is registered"
1439 );
1440 }
1441
1442 #[test]
1443 fn default_openers_registers_the_four_encryption_layers() {
1444 let openers = default_openers();
1448 let layers = openers.encryption_layers();
1449 assert_eq!(layers.len(), 4, "4 FDE probers registered");
1450 let schemes: Vec<EncryptionScheme> = layers.iter().map(|p| p.scheme()).collect();
1451 assert!(
1452 schemes.contains(&EncryptionScheme::Bitlocker),
1453 "bitlocker prober registered: {schemes:?}"
1454 );
1455 assert!(
1456 schemes
1457 .iter()
1458 .any(|s| matches!(s, EncryptionScheme::Luks1 | EncryptionScheme::Luks2)),
1459 "luks prober registered: {schemes:?}"
1460 );
1461 assert!(
1462 schemes.contains(&EncryptionScheme::FileVault),
1463 "filevault prober registered: {schemes:?}"
1464 );
1465 assert!(
1466 schemes.contains(&EncryptionScheme::VeraCrypt),
1467 "veracrypt prober registered: {schemes:?}"
1468 );
1469 }
1470
1471 #[test]
1472 fn encryption_probes_detect_their_signatures() {
1473 let mut bde = vec![0u8; 64];
1475 bde[3..11].copy_from_slice(b"-FVE-FS-");
1476 assert!(matches!(
1477 BitLockerProbe.probe(&window(&bde)),
1478 Confidence::Yes { .. }
1479 ));
1480 assert_eq!(BitLockerProbe.probe(&window(&[0u8; 64])), Confidence::No);
1481 assert_eq!(BitLockerProbe.scheme(), EncryptionScheme::Bitlocker);
1482
1483 let mut luks = vec![0u8; 64];
1485 luks[0..6].copy_from_slice(&[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]);
1486 assert!(matches!(
1487 LuksProbe.probe(&window(&luks)),
1488 Confidence::Yes { .. }
1489 ));
1490 assert_eq!(LuksProbe.probe(&window(&[0u8; 64])), Confidence::No);
1491
1492 let mut fv = vec![0u8; 128];
1494 fv[88..90].copy_from_slice(b"CS");
1495 assert!(matches!(
1496 FileVaultProbe.probe(&window(&fv)),
1497 Confidence::Yes { .. }
1498 ));
1499 assert_eq!(FileVaultProbe.probe(&window(&[0u8; 128])), Confidence::No);
1500
1501 assert_eq!(VeraCryptProbe.probe(&window(&[])), Confidence::Maybe);
1503 assert_eq!(
1504 VeraCryptProbe.probe(&window(&[0xffu8; 512])),
1505 Confidence::Maybe
1506 );
1507 assert_eq!(VeraCryptProbe.scheme(), EncryptionScheme::VeraCrypt);
1508 }
1509
1510 #[test]
1511 fn default_is_new_and_probers_report_their_kinds() {
1512 let _ = Vfs::default().open_source(mem(vec![0u8; 64])).unwrap();
1513 assert_eq!(NtfsProbe.kind(), FsKind::NTFS);
1514 assert_eq!(MbrProbe.scheme(), VolumeScheme::Mbr);
1515 assert_eq!(GptProbe.scheme(), VolumeScheme::Gpt);
1516 }
1517
1518 #[test]
1519 fn probers_say_no_on_unrecognized_bytes() {
1520 let empty = window(&[]);
1521 assert_eq!(NtfsProbe.probe(&empty), Confidence::No);
1522 assert_eq!(MbrProbe.probe(&empty), Confidence::No);
1523 assert_eq!(GptProbe.probe(&empty), Confidence::No);
1524 let mut prot = vec![0u8; 512];
1526 prot[446 + 4] = 0xEE;
1527 prot[446 + 12] = 1; prot[510] = 0x55;
1529 prot[511] = 0xaa;
1530 assert_eq!(MbrProbe.probe(&window(&prot)), Confidence::No);
1531 }
1532
1533 #[test]
1534 fn mbr_probe_rejects_filesystem_boot_sectors() {
1535 let mut exfat = vec![0u8; 512];
1543 exfat[3..11].copy_from_slice(b"EXFAT ");
1544 exfat[510] = 0x55;
1545 exfat[511] = 0xaa;
1546 exfat[446 + 4] = 0x07; exfat[446 + 12] = 0x20; assert_eq!(
1550 MbrProbe.probe(&window(&exfat)),
1551 Confidence::No,
1552 "exFAT boot sector must not be misread as an MBR partition table"
1553 );
1554
1555 let mut ntfs = vec![0u8; 512];
1557 ntfs[3..11].copy_from_slice(b"NTFS ");
1558 ntfs[510] = 0x55;
1559 ntfs[511] = 0xaa;
1560 ntfs[446 + 4] = 0x07;
1561 ntfs[446 + 12] = 0x20;
1562 assert_eq!(
1563 MbrProbe.probe(&window(&ntfs)),
1564 Confidence::No,
1565 "NTFS boot sector must not be misread as an MBR partition table"
1566 );
1567
1568 let mut fat = vec![0u8; 512];
1570 fat[0] = 0xEB; fat[510] = 0x55;
1572 fat[511] = 0xaa;
1573 fat[446 + 4] = 0x07;
1574 fat[446 + 12] = 0x20;
1575 assert_eq!(
1576 MbrProbe.probe(&window(&fat)),
1577 Confidence::No,
1578 "FAT boot sector must not be misread as an MBR partition table"
1579 );
1580
1581 let mut mbr = vec![0u8; 512];
1585 mbr[446] = 0x80; mbr[446 + 4] = 0x07; mbr[446 + 8] = 1; mbr[446 + 12] = 4; mbr[510] = 0x55;
1590 mbr[511] = 0xaa;
1591 assert!(
1592 matches!(MbrProbe.probe(&window(&mbr)), Confidence::Yes { .. }),
1593 "a genuine MBR partition table must still be detected"
1594 );
1595 }
1596
1597 #[test]
1598 fn ntfs_magic_but_invalid_boot_is_a_loud_error() {
1599 let mut v = vec![0u8; 4096];
1602 v[3..11].copy_from_slice(b"NTFS ");
1603 assert!(Vfs::new().open_source(mem(v)).is_err());
1604 }
1605
1606 #[test]
1607 fn a_garbage_e01_path_fails_loud() {
1608 let mut f = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
1609 f.write_all(b"not really an EWF image").unwrap();
1610 f.flush().unwrap();
1611 assert!(Vfs::new().open(f.path()).is_err());
1612 }
1613
1614 #[test]
1615 fn gpt_parse_without_signature_errors_and_mbr_volume_index_is_bounded() {
1616 assert!(Gpt::parse(mem(vec![0u8; 1024])).is_err());
1618 let mut d = vec![0u8; 512];
1620 d[446 + 4] = 0x07;
1621 d[446 + 8] = 1; d[446 + 12] = 4; d[510] = 0x55;
1624 d[511] = 0xaa;
1625 let m = Mbr::parse(mem(d)).unwrap();
1626 assert_eq!(m.scheme(), VolumeScheme::Mbr);
1627 assert_eq!(m.volumes().len(), 1);
1628 assert!(m.open_volume(0).is_ok());
1629 assert!(m.open_volume(9).is_err());
1630 }
1631
1632 #[test]
1633 fn apm_maps_partitions_and_errors_on_non_apm() {
1634 let mut img = vec![0u8; 512];
1636 img[0..2].copy_from_slice(b"ER");
1637 img[2..4].copy_from_slice(&512u16.to_be_bytes()); let pm = |map_cnt: u32, pstart: u32, pcnt: u32, ptype: &str| {
1640 let mut e = vec![0u8; 512];
1641 e[0..2].copy_from_slice(b"PM");
1642 e[4..8].copy_from_slice(&map_cnt.to_be_bytes());
1643 e[8..12].copy_from_slice(&pstart.to_be_bytes());
1644 e[0x0c..0x10].copy_from_slice(&pcnt.to_be_bytes());
1645 e[0x30..0x30 + ptype.len()].copy_from_slice(ptype.as_bytes());
1646 e
1647 };
1648 img.extend(pm(2, 1, 63, "Apple_partition_map"));
1650 img.extend(pm(2, 4, 2, "Apple_HFS"));
1651 img.extend(vec![0u8; 4 * 512]);
1652
1653 let apm = Apm::parse(mem(img)).unwrap();
1654 assert_eq!(apm.scheme(), VolumeScheme::Apm);
1655 assert_eq!(apm.volumes().len(), 1); assert_eq!(apm.volumes()[0].start, 4 * 512); assert!(apm.open_volume(0).is_ok());
1658 assert!(apm.open_volume(9).is_err());
1659
1660 assert!(Apm::parse(mem(vec![0u8; 2048])).is_err());
1662 }
1663
1664 #[test]
1665 fn recursion_is_depth_capped_on_a_self_referential_mbr() {
1666 let mut d = vec![0u8; 1024];
1669 d[446 + 4] = 0x83; d[446 + 12] = 2;
1672 d[510] = 0x55;
1673 d[511] = 0xaa;
1674 assert!(Vfs::new().open_source(mem(d)).unwrap().is_none());
1675 }
1676
1677 #[test]
1678 fn container_decoders_report_format_and_error_on_bad_content() {
1679 assert_eq!(VhdDecoder.format(), ContainerFormat::Vhd);
1680 assert_eq!(Qcow2Decoder.format(), ContainerFormat::Qcow2);
1681 assert_eq!(VmdkDecoder.format(), ContainerFormat::Vmdk);
1682 assert_eq!(VhdxDecoder.format(), ContainerFormat::Vhdx);
1683 let mut vhd = vec![0u8; 4096];
1686 vhd[0..8].copy_from_slice(b"conectix");
1687 assert!(Vfs::new().open_source(mem(vhd)).is_err());
1688 let mut q = vec![0u8; 4096];
1689 q[0..4].copy_from_slice(&[0x51, 0x46, 0x49, 0xfb]);
1690 assert!(Vfs::new().open_source(mem(q)).is_err());
1691 let mut v = vec![0u8; 4096];
1692 v[0..4].copy_from_slice(b"KDMV");
1693 assert!(Vfs::new().open_source(mem(v)).is_err());
1694 let mut x = vec![0u8; 4096];
1695 x[0..8].copy_from_slice(vhdx::FILE_MAGIC);
1696 assert!(Vfs::new().open_source(mem(x)).is_err());
1697 }
1698
1699 #[test]
1700 fn dmg_decoder_format_probe_and_open_error() {
1701 assert_eq!(DmgDecoder.format(), ContainerFormat::Dmg);
1702 assert_eq!(
1704 DmgDecoder.probe(&SniffWindow::with_tail(0, &[], 1024, &[0u8; 512])),
1705 Confidence::No
1706 );
1707 let mut v = vec![0u8; 1024];
1712 v[512..516].copy_from_slice(b"koly");
1713 v[512 + 224..512 + 232].copy_from_slice(&u64::MAX.to_be_bytes());
1714 assert!(Vfs::new().open_source(mem(v)).is_err());
1715 }
1716
1717 #[test]
1718 fn aff4_decoder_format_probe_and_open_error() {
1719 assert_eq!(Aff4Decoder.format(), ContainerFormat::Aff4);
1720 assert_eq!(Aff4Decoder.probe(&window(&[])), Confidence::No);
1722 assert_eq!(
1723 Aff4Decoder.probe(&window(&[0x50, 0x4b, 0x03, 0x04])),
1724 Confidence::Maybe
1725 );
1726 let mut v = vec![0u8; 256];
1729 v[0..4].copy_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
1730 assert!(Vfs::new().open_source(mem(v)).is_err());
1731 }
1732
1733 #[test]
1734 fn a_valid_container_holding_no_filesystem_resolves_to_none() {
1735 let vhd = include_bytes!("../tests/data/empty.vhd").to_vec();
1738 assert!(Vfs::new().open_source(mem(vhd)).unwrap().is_none());
1739 }
1740
1741 #[test]
1742 fn ext4_probe_kind_and_open_error() {
1743 assert_eq!(Ext4Probe.kind(), FsKind::EXT);
1744 let mut v = vec![0u8; 4096];
1747 v[1080] = 0x53;
1748 v[1081] = 0xef;
1749 v[1048..1052].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1750 assert!(Vfs::new().open_source(mem(v)).is_err());
1751 }
1752
1753 #[test]
1754 fn iso9660_probe_kind_and_open_error() {
1755 assert_eq!(Iso9660Probe.kind(), FsKind::ISO9660);
1756 assert_eq!(Iso9660Probe.probe(&window(&[])), Confidence::No);
1757 let mut v = vec![0u8; 40 * 1024];
1760 v[32769..32774].copy_from_slice(b"CD001");
1761 assert_eq!(
1762 Iso9660Probe.probe(&window(&v)),
1763 Confidence::Yes {
1764 how: "ISO 9660 CD001 volume descriptor"
1765 }
1766 );
1767 assert!(Vfs::new().open_source(mem(v)).is_err());
1768 }
1769
1770 #[test]
1771 fn apfs_probe_kind_and_open_error() {
1772 assert_eq!(ApfsProbe.kind(), FsKind::APFS);
1773 assert_eq!(ApfsProbe.probe(&window(&[])), Confidence::No);
1774 let mut v = vec![0u8; 40 * 1024];
1777 v[32..36].copy_from_slice(b"NXSB");
1778 assert_eq!(
1779 ApfsProbe.probe(&window(&v)),
1780 Confidence::Yes {
1781 how: "APFS NXSB container superblock"
1782 }
1783 );
1784 assert!(Vfs::new().open_source(mem(v)).is_err());
1785 }
1786
1787 #[test]
1788 fn hfsplus_probe_kind_and_no_on_short_window() {
1789 assert_eq!(HfsPlusProbe.kind(), FsKind::HFS_PLUS);
1790 assert_eq!(HfsPlusProbe.probe(&window(&[])), Confidence::No);
1792 let mut v = vec![0u8; 40 * 1024];
1794 v[1024..1026].copy_from_slice(&[0x48, 0x58]);
1795 assert_eq!(
1796 HfsPlusProbe.probe(&window(&v)),
1797 Confidence::Yes {
1798 how: "HFS+/HFSX volume header"
1799 }
1800 );
1801 }
1802
1803 #[test]
1804 fn fat_and_exfat_magic_but_garbage_are_loud_errors() {
1805 let mut x = vec![0u8; 4096];
1807 x[3..11].copy_from_slice(b"EXFAT ");
1808 x[510] = 0x55;
1809 x[511] = 0xaa;
1810 assert!(Vfs::new().open_source(mem(x)).is_err());
1811
1812 let mut f = vec![0u8; 4096];
1815 f[0] = 0xEB;
1816 f[11..13].copy_from_slice(&512u16.to_le_bytes());
1817 f[510] = 0x55;
1818 f[511] = 0xaa;
1819 assert!(Vfs::new().open_source(mem(f)).is_err());
1820 }
1821
1822 #[test]
1823 fn guid_hint_is_lowercase_hex() {
1824 assert_eq!(guid_hint(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
1825 }
1826
1827 #[test]
1828 fn gpt_parse_skips_unused_and_reversed_entries() {
1829 let mut d = vec![0u8; 1280];
1830 d[512..520].copy_from_slice(b"EFI PART");
1831 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());
1837 d[1024 + 40..1024 + 48].copy_from_slice(&200u64.to_le_bytes());
1838 d[1152] = 0xa2;
1840 d[1152 + 32..1152 + 40].copy_from_slice(&500u64.to_le_bytes());
1841 d[1152 + 40..1152 + 48].copy_from_slice(&400u64.to_le_bytes());
1842 let g = Gpt::parse(mem(d)).unwrap();
1843 assert_eq!(g.scheme(), VolumeScheme::Gpt);
1844 assert_eq!(g.volumes().len(), 1, "reversed entry 1 is skipped");
1845 assert_eq!(g.volumes()[0].start, 100 * 512);
1846 assert!(g.open_volume(0).is_ok());
1847 assert!(g.open_volume(7).is_err());
1848
1849 assert_eq!(Mem(vec![1, 2, 3]).read_at(99, &mut [0u8; 4]).unwrap(), 0);
1851 }
1852
1853 const APFS_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/apfs_volume.bin");
1856 const EXT4_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/ext4.img");
1857
1858 fn apfs_live_xid() -> u64 {
1862 use std::io::{Read, Seek, SeekFrom};
1863 let bytes = std::fs::read(APFS_FIXTURE).unwrap();
1864 let mut c = apfs_core::ApfsContainer::open(std::io::Cursor::new(bytes)).unwrap();
1865 let bs = u64::from(c.superblock().block_size);
1866 let vaddr = c.volume_superblock_addrs().unwrap()[0];
1867 let mut r = c.into_reader();
1868 r.seek(SeekFrom::Start(vaddr * bs)).unwrap();
1869 let mut buf = vec![0u8; bs as usize];
1870 r.read_exact(&mut buf).unwrap();
1871 apfs_core::volume::ApfsVolume::parse(&buf).unwrap().xid()
1872 }
1873
1874 fn zeros_file() -> tempfile::NamedTempFile {
1875 let mut f = tempfile::NamedTempFile::new().unwrap();
1876 f.write_all(&[0u8; 4096]).unwrap();
1877 f.flush().unwrap();
1878 f
1879 }
1880
1881 #[test]
1882 fn epoch_from_create_time_round_trips_and_orders() {
1883 let t = 0x0123_4567_89ab_cdefu64;
1884 let tag = epoch_from_create_time(t);
1885 assert_eq!(&tag.0[0..24], &[0u8; 24], "high 24 bytes are zero");
1886 assert_eq!(
1887 u64::from_be_bytes(tag.0[24..32].try_into().unwrap()),
1888 t,
1889 "create_time round-trips out of the low 8 bytes"
1890 );
1891 assert!(
1892 epoch_from_create_time(t + 1).0 > tag.0,
1893 "a later create_time yields a greater tag"
1894 );
1895 }
1896
1897 #[test]
1898 fn snapshot_view_carries_epoch_and_snapshot_locator() {
1899 let base = Locator::file("/ev.dmg");
1900 let v = snapshot_view(&base, 42, "daily".to_string(), 1000);
1901 assert_eq!(v.xid, 42);
1902 assert_eq!(v.name, "daily");
1903 assert_eq!(v.epoch, epoch_from_create_time(1000));
1904 assert!(matches!(
1905 v.locator.layer,
1906 Layer::Snapshot {
1907 store: SnapshotRef::ApfsXid(42)
1908 }
1909 ));
1910 }
1911
1912 #[test]
1913 fn snapshots_on_unrecognized_source_is_empty() {
1914 let f = zeros_file();
1915 assert!(Vfs::new().snapshots(f.path()).unwrap().is_empty());
1916 }
1917
1918 #[test]
1919 fn snapshots_on_non_apfs_filesystem_is_empty() {
1920 assert!(Vfs::new()
1922 .snapshots(Path::new(EXT4_FIXTURE))
1923 .unwrap()
1924 .is_empty());
1925 }
1926
1927 #[test]
1928 fn open_snapshot_without_filesystem_is_bootstrap_error() {
1929 let f = zeros_file();
1930 assert!(matches!(
1931 Vfs::new().open_snapshot(f.path(), 1),
1932 Err(VfsError::Bootstrap { .. })
1933 ));
1934 }
1935
1936 #[test]
1937 fn open_snapshot_on_non_apfs_is_unsupported() {
1938 assert!(matches!(
1939 Vfs::new().open_snapshot(Path::new(EXT4_FIXTURE), 1),
1940 Err(VfsError::Unsupported { .. })
1941 ));
1942 }
1943
1944 #[test]
1945 fn open_snapshot_unknown_xid_is_a_loud_decode_error() {
1946 let bogus = apfs_live_xid().wrapping_add(0xDEAD_BEEF);
1949 assert!(matches!(
1950 Vfs::new().open_snapshot(Path::new(APFS_FIXTURE), bogus),
1951 Err(VfsError::Decode { .. })
1952 ));
1953 }
1954
1955 #[test]
1956 fn open_snapshot_at_live_xid_mounts_and_walks() {
1957 let ev = Vfs::new()
1958 .open_snapshot(Path::new(APFS_FIXTURE), apfs_live_xid())
1959 .expect("open live-xid snapshot");
1960 let uri = ev.root.to_uri();
1961 assert!(
1962 uri.contains("snapshot:apfs") && uri.contains("fs:apfs"),
1963 "locator names the snapshot + APFS layers: {uri}"
1964 );
1965 let fs = ev.fs.expect("snapshot mounts a filesystem");
1966 let names: Vec<String> = walk(fs.as_ref())
1967 .unwrap()
1968 .into_iter()
1969 .filter_map(|e| {
1970 e.path
1971 .last()
1972 .map(|n| String::from_utf8_lossy(n).to_string())
1973 })
1974 .collect();
1975 assert!(names.iter().any(|n| n == "plain.txt"), "walk: {names:?}");
1976 }
1977
1978 #[test]
1981 fn engine_resolution_matches_openers_open_directly() {
1982 let bytes = std::fs::read(EXT4_FIXTURE).unwrap();
1988 let len = bytes.len() as u64;
1989
1990 let via_engine = Vfs::new()
1992 .open_source(mem(bytes.clone()))
1993 .unwrap()
1994 .expect("engine resolves the ext4 fixture");
1995
1996 let base = Locator::root(Layer::Range { start: 0, len });
1998 let resolved = default_openers()
1999 .open(mem(bytes), base, 0)
2000 .unwrap()
2001 .expect("Openers::open resolves the ext4 fixture");
2002
2003 let names = |fs: &dyn FileSystem| {
2005 let mut v: Vec<Vec<Vec<u8>>> = walk(fs).unwrap().into_iter().map(|e| e.path).collect();
2006 v.sort();
2007 v
2008 };
2009 assert_eq!(
2010 names(via_engine.as_ref()),
2011 names(resolved.fs.as_ref()),
2012 "engine and Openers::open mount the same filesystem"
2013 );
2014 let resolved_uri = resolved.spec.to_uri();
2016 assert!(
2017 matches!(
2018 resolved.spec.layer,
2019 Layer::Fs {
2020 kind: FsKind::EXT,
2021 ..
2022 }
2023 ),
2024 "registry resolved spec tops with fs:ext: {resolved_uri}"
2025 );
2026 }
2027}