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
26mod containerfs;
27
28pub struct Evidence {
31 pub root: Locator,
33 pub fs: Option<DynFs>,
35}
36
37pub struct Vfs {
39 openers: Openers,
40}
41
42impl Default for Vfs {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl Vfs {
49 #[must_use]
51 pub fn new() -> Self {
52 Self {
53 openers: default_openers(),
54 }
55 }
56
57 pub fn open(&self, path: &Path) -> VfsResult<Evidence> {
62 let base = open_base(path)?;
63 let base_spec = Locator::file(path);
64
65 if base_has_zip_magic(&base)? {
74 if let Some(fs) = containerfs::open_aff4_logical(path)? {
75 return Ok(container_evidence(&base_spec, fs));
76 }
77 }
78
79 if let Some(r) = self.openers.open(base.clone(), base_spec.clone(), 0)? {
80 return Ok(Evidence {
81 root: r.spec,
82 fs: Some(r.fs),
83 });
84 }
85 let name = path.file_name().and_then(|s| s.to_str());
94 if let Some(fs) = containerfs::open_archive(&base, name)? {
95 return Ok(container_evidence(&base_spec, fs));
96 }
97 if let Some(fs) = containerfs::open_ad1(path)? {
98 return Ok(container_evidence(&base_spec, fs));
99 }
100 if let Some(fs) = containerfs::open_dar(path)? {
101 return Ok(container_evidence(&base_spec, fs));
102 }
103 Ok(Evidence {
104 root: base_spec,
105 fs: None,
106 })
107 }
108
109 pub fn open_all(&self, path: &Path) -> VfsResult<Vec<Evidence>> {
129 const HEAD_CAP: u64 = 128 * 1024;
133 const TAIL_CAP: u64 = 4096;
134
135 let base = open_base(path)?;
136 let base_spec = Locator::file(path);
137
138 let total = base.len();
139 let head_len = total.clamp(1, HEAD_CAP) as usize;
140 let mut head = vec![0u8; head_len];
141 let hn = base.read_at(0, &mut head)?;
142 let tail_len = total.min(TAIL_CAP);
143 let mut tail = vec![0u8; tail_len as usize];
144 let tn = base.read_at(total - tail_len, &mut tail)?;
145 let window = SniffWindow::with_tail(
146 0,
147 head.get(..hn).unwrap_or(&[]),
148 total,
149 tail.get(..tn).unwrap_or(&[]),
150 );
151
152 for vsp in self.openers.volume_systems() {
153 if !vsp.probe(&window).is_candidate() {
154 continue;
155 }
156 let vs = vsp.open(base.clone())?;
157 let mut out = Vec::new();
158 for index in 0..vs.volumes().len() {
159 let sub = vs.open_volume(index)?;
160 let child = base_spec.clone().push(Layer::Volume {
161 scheme: vsp.scheme(),
162 index,
163 guid: None,
164 });
165 if let Some(r) = self.openers.open(sub, child, 1)? {
166 out.push(Evidence {
167 root: r.spec,
168 fs: Some(r.fs),
169 });
170 }
171 }
172 return Ok(out);
173 }
174
175 Ok(vec![self.open(path)?])
178 }
179
180 pub fn open_source(&self, source: DynSource) -> VfsResult<Option<DynFs>> {
183 let base = Locator::root(Layer::Range {
184 start: 0,
185 len: source.len(),
186 });
187 Ok(self.openers.open(source, base, 0)?.map(|r| r.fs))
188 }
189
190 pub fn snapshots(&self, path: &Path) -> VfsResult<Vec<SnapshotView>> {
205 let base = open_base(path)?;
206 let base_spec = Locator::file(path);
207 let Some(resolved) = self.openers.open(base, base_spec, 0)? else {
208 return Ok(Vec::new());
209 };
210 if !is_apfs(&resolved.spec) {
211 return Ok(Vec::new());
212 }
213 let source_spec = resolved.source_spec;
214 let len = resolved.source.len();
215 let cursor = SourceCursor::new(resolved.source, 0, len);
216 let snaps = apfs_core::vfs::ApfsFs::snapshots(cursor).map_err(map_apfs_err)?;
217 Ok(snaps
218 .into_iter()
219 .map(|s| snapshot_view(&source_spec, s.xid, s.name, s.create_time))
220 .collect())
221 }
222
223 pub fn open_snapshot(&self, path: &Path, xid: u64) -> VfsResult<Evidence> {
236 let base = open_base(path)?;
237 let base_spec = Locator::file(path);
238 let resolved = self
239 .openers
240 .open(base, base_spec, 0)?
241 .ok_or(VfsError::Bootstrap {
242 stage: "apfs snapshot",
243 detail: "no filesystem detected in evidence".to_string(),
244 })?;
245 if !is_apfs(&resolved.spec) {
246 return Err(VfsError::Unsupported {
247 layer: "snapshot",
248 scheme: "non-APFS filesystem has no APFS snapshot".to_string(),
249 });
250 }
251 let source_spec = resolved.source_spec;
252 let len = resolved.source.len();
253 let cursor = SourceCursor::new(resolved.source, 0, len);
254 let fs = apfs_core::vfs::ApfsFs::open_snapshot(cursor, xid).map_err(map_apfs_err)?;
255 let root = source_spec
256 .push(Layer::Snapshot {
257 store: SnapshotRef::ApfsXid(xid),
258 })
259 .push(Layer::Fs {
260 kind: FsKind::APFS,
261 at: NodeAddr::Path(Vec::new()),
262 });
263 Ok(Evidence {
264 root,
265 fs: Some(Arc::new(fs)),
266 })
267 }
268}
269
270#[derive(Debug, Clone)]
274pub struct SnapshotView {
275 pub epoch: EpochTag,
277 pub xid: u64,
279 pub name: String,
281 pub locator: Locator,
283}
284
285fn container_evidence(base_spec: &Locator, fs: DynFs) -> Evidence {
288 let root = base_spec.clone().push(Layer::Fs {
289 kind: fs.kind(),
290 at: NodeAddr::Path(Vec::new()),
291 });
292 Evidence { root, fs: Some(fs) }
293}
294
295fn is_apfs(spec: &Locator) -> bool {
297 matches!(
298 spec.layer,
299 Layer::Fs {
300 kind: FsKind::APFS,
301 ..
302 }
303 )
304}
305
306fn snapshot_view(source_spec: &Locator, xid: u64, name: String, create_time: u64) -> SnapshotView {
311 SnapshotView {
312 epoch: epoch_from_create_time(create_time),
313 xid,
314 name,
315 locator: source_spec.clone().push(Layer::Snapshot {
316 store: SnapshotRef::ApfsXid(xid),
317 }),
318 }
319}
320
321fn epoch_from_create_time(create_time_ns: u64) -> EpochTag {
327 let mut bytes = [0u8; 32];
328 bytes[24..32].copy_from_slice(&create_time_ns.to_be_bytes());
329 EpochTag::from_bytes(bytes)
330}
331
332#[allow(clippy::needless_pass_by_value)]
335fn map_apfs_err(e: apfs_core::ApfsError) -> VfsError {
336 VfsError::Decode {
337 layer: "apfs snapshot",
338 offset: 0,
339 detail: e.to_string(),
340 bytes: SmallHex::new(&[]),
341 }
342}
343
344#[must_use]
349pub fn default_openers() -> Openers {
350 Openers::new()
351 .filesystem(NtfsProbe)
352 .filesystem(Ext4Probe)
353 .filesystem(XfsProbe)
354 .filesystem(Iso9660Probe)
355 .filesystem(ApfsProbe)
356 .filesystem(HfsPlusProbe)
357 .filesystem(ExFatProbe)
358 .filesystem(FatProbe)
359 .filesystem(UdfProbe)
360 .filesystem(UfsProbe)
361 .filesystem(BtrfsProbe)
362 .volume_system(GptProbe)
363 .volume_system(MbrProbe)
364 .volume_system(ApmProbe)
365 .container(VhdDecoder)
366 .container(Qcow2Decoder)
367 .container(VmdkDecoder)
368 .container(VhdxDecoder)
369 .container(DmgDecoder)
370 .container(Aff4Decoder)
371 .archive(archive_core::ArchiveOpener)
372 .encryption(BitLockerProbe)
373 .encryption(LuksProbe)
374 .encryption(FileVaultProbe)
375 .encryption(VeraCryptProbe)
376}
377
378fn open_base(path: &Path) -> VfsResult<DynSource> {
382 if is_ewf(path) {
383 let reader = ewf::EwfReader::open(path).map_err(|e| VfsError::Bootstrap {
384 stage: "ewf::open",
385 detail: e.to_string(),
386 })?;
387 Ok(Arc::new(reader))
388 } else {
389 Ok(Arc::new(FileSource::open(path)?))
390 }
391}
392
393fn base_has_zip_magic(base: &DynSource) -> VfsResult<bool> {
398 let mut magic = [0u8; 4];
399 let n = base.read_at(0, &mut magic)?;
400 Ok(n >= 4 && magic == [0x50, 0x4b, 0x03, 0x04])
401}
402
403fn is_ewf(path: &Path) -> bool {
404 path.extension()
405 .and_then(|e| e.to_str())
406 .is_some_and(|e| e.eq_ignore_ascii_case("e01") || e.eq_ignore_ascii_case("ex01"))
407}
408
409struct NtfsProbe;
411
412impl FileSystemOpen for NtfsProbe {
413 fn kind(&self) -> FsKind {
414 FsKind::NTFS
415 }
416
417 fn probe(&self, w: &SniffWindow) -> Confidence {
418 if w.has_magic(3, b"NTFS ") {
420 Confidence::Yes { how: "NTFS OEM id" }
421 } else {
422 Confidence::No
423 }
424 }
425
426 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
427 let len = src.len();
428 let cursor = SourceCursor::new(src, 0, len);
429 let fs = ntfs_core::NtfsFs::open(cursor).map_err(|e| VfsError::Decode {
430 layer: "ntfs",
431 offset: 0,
432 detail: e.to_string(),
433 bytes: SmallHex::new(&[]),
434 })?;
435 Ok(Arc::new(fs))
436 }
437}
438
439struct Ext4Probe;
442
443impl FileSystemOpen for Ext4Probe {
444 fn kind(&self) -> FsKind {
445 FsKind::EXT
446 }
447
448 fn probe(&self, w: &SniffWindow) -> Confidence {
449 if w.has_magic(1080, &[0x53, 0xEF]) {
452 Confidence::Yes {
453 how: "ext2/3/4 superblock magic",
454 }
455 } else {
456 Confidence::No
457 }
458 }
459
460 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
461 let len = src.len();
462 let cursor = SourceCursor::new(src, 0, len);
463 let fs = ext4fs::Ext4Fs::open(cursor).map_err(|e| VfsError::Decode {
464 layer: "ext4",
465 offset: 0,
466 detail: e.to_string(),
467 bytes: SmallHex::new(&[]),
468 })?;
469 Ok(Arc::new(fs))
470 }
471}
472
473struct XfsProbe;
477
478impl FileSystemOpen for XfsProbe {
479 fn kind(&self) -> FsKind {
480 FsKind::XFS
481 }
482
483 fn probe(&self, w: &SniffWindow) -> Confidence {
484 xfs::vfs::xfs_probe(w)
485 }
486
487 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
488 Ok(Arc::new(xfs::vfs::XfsFs::open(&src)?))
489 }
490}
491
492struct Iso9660Probe;
496
497impl FileSystemOpen for Iso9660Probe {
498 fn kind(&self) -> FsKind {
499 FsKind::ISO9660
500 }
501
502 fn probe(&self, w: &SniffWindow) -> Confidence {
503 if w.has_magic(32769, b"CD001") {
506 Confidence::Yes {
507 how: "ISO 9660 CD001 volume descriptor",
508 }
509 } else {
510 Confidence::No
511 }
512 }
513
514 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
515 let len = src.len();
516 let cursor = SourceCursor::new(src, 0, len);
517 let fs = iso::vfs::IsoVfs::open(cursor).map_err(|e| VfsError::Decode {
518 layer: "iso9660",
519 offset: 0,
520 detail: e.to_string(),
521 bytes: SmallHex::new(&[]),
522 })?;
523 Ok(Arc::new(fs))
524 }
525}
526
527struct ApfsProbe;
530
531impl FileSystemOpen for ApfsProbe {
532 fn kind(&self) -> FsKind {
533 FsKind::APFS
534 }
535
536 fn probe(&self, w: &SniffWindow) -> Confidence {
537 if w.has_magic(32, b"NXSB") {
538 Confidence::Yes {
539 how: "APFS NXSB container superblock",
540 }
541 } else {
542 Confidence::No
543 }
544 }
545
546 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
547 let len = src.len();
548 let cursor = SourceCursor::new(src, 0, len);
549 let fs = apfs_core::vfs::ApfsFs::open(cursor).map_err(|e| VfsError::Decode {
550 layer: "apfs",
551 offset: 0,
552 detail: e.to_string(),
553 bytes: SmallHex::new(&[]),
554 })?;
555 Ok(Arc::new(fs))
556 }
557}
558
559struct HfsPlusProbe;
562
563impl FileSystemOpen for HfsPlusProbe {
564 fn kind(&self) -> FsKind {
565 FsKind::HFS_PLUS
566 }
567
568 fn probe(&self, w: &SniffWindow) -> Confidence {
569 match w.at(1024, 2) {
570 Some([0x48, 0x2B | 0x58]) => Confidence::Yes {
571 how: "HFS+/HFSX volume header",
572 },
573 _ => Confidence::No,
574 }
575 }
576
577 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
578 let len = src.len();
581 let mut volume = vec![0u8; usize::try_from(len).unwrap_or(usize::MAX)];
582 let n = src.read_at(0, &mut volume)?;
583 volume.truncate(n);
584 let fs = hfsplus::vfs::HfsFs::new(volume)?;
585 Ok(Arc::new(fs))
586 }
587}
588
589struct ExFatProbe;
593
594impl FileSystemOpen for ExFatProbe {
595 fn kind(&self) -> FsKind {
596 FsKind::EXFAT
597 }
598
599 fn probe(&self, w: &SniffWindow) -> Confidence {
600 if w.at(510, 2) == Some(&[0x55, 0xaa]) && w.has_magic(3, b"EXFAT ") {
601 Confidence::Yes {
602 how: "exFAT boot signature",
603 }
604 } else {
605 Confidence::No
606 }
607 }
608
609 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
610 let len = src.len();
611 let cursor = SourceCursor::new(src, 0, len);
612 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
613 layer: "exfat",
614 offset: 0,
615 detail: e.to_string(),
616 bytes: SmallHex::new(&[]),
617 })?;
618 Ok(Arc::new(fs))
619 }
620}
621
622struct FatProbe;
625
626impl FileSystemOpen for FatProbe {
627 fn kind(&self) -> FsKind {
628 FsKind::FAT
629 }
630
631 fn probe(&self, w: &SniffWindow) -> Confidence {
632 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
633 return Confidence::No;
634 }
635 let jump = w.at(0, 1).and_then(|s| s.first().copied());
636 let jump_ok = matches!(jump, Some(0xEB | 0xE9));
637 let bps = w
638 .at(11, 2)
639 .and_then(|b| <[u8; 2]>::try_from(b).ok())
640 .map_or(0, u16::from_le_bytes);
641 if jump_ok && bps.is_power_of_two() && (512..=4096).contains(&bps) {
642 Confidence::Yes { how: "FAT BPB" }
643 } else {
644 Confidence::No
645 }
646 }
647
648 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
649 let len = src.len();
650 let cursor = SourceCursor::new(src, 0, len);
651 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
652 layer: "fat",
653 offset: 0,
654 detail: e.to_string(),
655 bytes: SmallHex::new(&[]),
656 })?;
657 Ok(Arc::new(fs))
658 }
659}
660
661struct UdfProbe;
668
669impl FileSystemOpen for UdfProbe {
670 fn kind(&self) -> FsKind {
671 FsKind::UDF
672 }
673
674 fn probe(&self, w: &SniffWindow) -> Confidence {
675 if udf_forensic::detect_udf(&mut Cursor::new(w.bytes())) {
676 Confidence::Yes {
677 how: "UDF NSR02/NSR03 volume recognition sequence",
678 }
679 } else {
680 Confidence::No
681 }
682 }
683
684 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
685 let len = src.len();
686 let cursor = SourceCursor::new(src, 0, len);
687 let fs = udf_forensic::vfs::UdfVfs::open(cursor)?;
688 Ok(Arc::new(fs))
689 }
690}
691
692struct UfsProbe;
699
700impl FileSystemOpen for UfsProbe {
701 fn kind(&self) -> FsKind {
702 FsKind::UFS
703 }
704
705 fn probe(&self, w: &SniffWindow) -> Confidence {
706 ufs::vfs::ufs_probe(w)
707 }
708
709 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
710 let fs = ufs::vfs::UfsFs::open(&src)?;
711 Ok(Arc::new(fs))
712 }
713}
714
715const BTRFS_MAGIC_OFFSET: usize = btrfs_core::BTRFS_SUPER_INFO_OFFSET as usize + 0x40;
719
720struct BtrfsProbe;
733
734impl FileSystemOpen for BtrfsProbe {
735 fn kind(&self) -> FsKind {
736 FsKind::BTRFS
737 }
738
739 fn probe(&self, w: &SniffWindow) -> Confidence {
740 if w.has_magic(BTRFS_MAGIC_OFFSET, &btrfs_core::BTRFS_MAGIC) {
741 Confidence::Yes {
742 how: "btrfs _BHRfS_M superblock magic",
743 }
744 } else {
745 Confidence::No
746 }
747 }
748
749 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
750 let fs = btrfs_core::vfs::BtrfsFs::open(&*src)?;
751 Ok(Arc::new(fs))
752 }
753}
754
755struct MbrProbe;
758
759impl VolumeSystemOpen for MbrProbe {
760 fn scheme(&self) -> VolumeScheme {
761 VolumeScheme::Mbr
762 }
763
764 fn probe(&self, w: &SniffWindow) -> Confidence {
765 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
771 return Confidence::No;
772 }
773 if w.has_magic(3, b"EXFAT ") || w.has_magic(3, b"NTFS ") {
778 return Confidence::No;
779 }
780 let jump = w.at(0, 1).and_then(|s| s.first().copied());
781 if matches!(jump, Some(0xEB | 0xE9)) {
782 return Confidence::No;
783 }
784 let data = w.bytes();
785 for i in 0..4usize {
786 let base = 446 + i * 16;
787 let boot_flag = data.get(base).copied().unwrap_or(0xFF);
790 let ptype = data.get(base + 4).copied().unwrap_or(0);
791 let size = le_u32(data, base + 12);
792 if matches!(boot_flag, 0x00 | 0x80) && ptype != 0 && ptype != 0xEE && size != 0 {
793 return Confidence::Yes {
794 how: "MBR partition table",
795 };
796 }
797 }
798 Confidence::No
799 }
800
801 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
802 Ok(Box::new(Mbr::parse(src)?))
803 }
804}
805
806struct Mbr {
808 parent: DynSource,
809 volumes: Vec<VolumeDesc>,
810}
811
812impl Mbr {
813 fn parse(src: DynSource) -> VfsResult<Self> {
814 let mut sector = [0u8; 512];
815 src.read_at(0, &mut sector)?;
816 let mut volumes = Vec::new();
817 for i in 0..4usize {
818 let base = 446 + i * 16;
819 let ptype = sector.get(base + 4).copied().unwrap_or(0);
820 let start_lba = le_u32(§or, base + 8);
821 let size = le_u32(§or, base + 12);
822 if ptype == 0 || ptype == 0xEE || size == 0 {
823 continue;
824 }
825 volumes.push(VolumeDesc {
826 index: i,
827 kind: VolumeKind::Partition,
828 start: u64::from(start_lba) * 512,
829 len: u64::from(size) * 512,
830 type_hint: Some(format!("0x{ptype:02x}")),
831 label: None,
832 });
833 }
834 Ok(Self {
835 parent: src,
836 volumes,
837 })
838 }
839}
840
841impl VolumeSystem for Mbr {
842 fn scheme(&self) -> VolumeScheme {
843 VolumeScheme::Mbr
844 }
845
846 fn volumes(&self) -> &[VolumeDesc] {
847 &self.volumes
848 }
849
850 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
851 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
852 what: "mbr volume index",
853 offset: index as u64,
854 len: 1,
855 bound: self.volumes.len() as u64,
856 })?;
857 Ok(Arc::new(SubRange::new(
858 self.parent.clone(),
859 desc.start,
860 desc.len,
861 )))
862 }
863}
864
865struct GptProbe;
869
870impl VolumeSystemOpen for GptProbe {
871 fn scheme(&self) -> VolumeScheme {
872 VolumeScheme::Gpt
873 }
874
875 fn probe(&self, w: &SniffWindow) -> Confidence {
876 if w.has_magic(512, b"EFI PART") {
878 Confidence::Yes {
879 how: "GPT EFI PART header",
880 }
881 } else {
882 Confidence::No
883 }
884 }
885
886 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
887 Ok(Box::new(Gpt::parse(src)?))
888 }
889}
890
891struct Gpt {
893 parent: DynSource,
894 volumes: Vec<VolumeDesc>,
895}
896
897impl Gpt {
898 fn parse(src: DynSource) -> VfsResult<Self> {
899 let mut header = [0u8; 512];
901 src.read_at(512, &mut header)?;
902 if header.get(0..8) != Some(b"EFI PART".as_slice()) {
903 return Err(VfsError::Decode {
904 layer: "gpt",
905 offset: 512,
906 detail: "missing EFI PART signature".to_string(),
907 bytes: SmallHex::new(header.get(0..8).unwrap_or(&[])),
908 });
909 }
910 let entries_lba = le_u64(&header, 72);
911 let num_entries = le_u32(&header, 80).min(256) as usize;
913 let entry_size = le_u32(&header, 84).clamp(128, 512) as usize;
914 let array_len = num_entries.checked_mul(entry_size).unwrap_or(0);
915 let mut arr = vec![0u8; array_len];
916 src.read_at(entries_lba.saturating_mul(512), &mut arr)?;
917
918 let mut volumes = Vec::new();
919 for i in 0..num_entries {
920 let Some(base) = i.checked_mul(entry_size) else {
921 break; };
923 let Some(entry) = arr.get(base..base.saturating_add(entry_size)) else {
924 break; };
926 let type_guid = entry.get(0..16).unwrap_or(&[]);
928 if type_guid.iter().all(|&b| b == 0) {
929 continue;
930 }
931 let first = le_u64(entry, 32);
932 let last = le_u64(entry, 40);
933 if last < first {
934 continue;
935 }
936 let sectors = last - first + 1;
937 volumes.push(VolumeDesc {
938 index: i,
939 kind: VolumeKind::Partition,
940 start: first.saturating_mul(512),
941 len: sectors.saturating_mul(512),
942 type_hint: Some(guid_hint(type_guid)),
943 label: None,
944 });
945 }
946 Ok(Self {
947 parent: src,
948 volumes,
949 })
950 }
951}
952
953impl VolumeSystem for Gpt {
954 fn scheme(&self) -> VolumeScheme {
955 VolumeScheme::Gpt
956 }
957
958 fn volumes(&self) -> &[VolumeDesc] {
959 &self.volumes
960 }
961
962 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
963 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
964 what: "gpt volume index",
965 offset: index as u64,
966 len: 1,
967 bound: self.volumes.len() as u64,
968 })?;
969 Ok(Arc::new(SubRange::new(
970 self.parent.clone(),
971 desc.start,
972 desc.len,
973 )))
974 }
975}
976
977struct ApmProbe;
980
981impl VolumeSystemOpen for ApmProbe {
982 fn scheme(&self) -> VolumeScheme {
983 VolumeScheme::Apm
984 }
985
986 fn probe(&self, w: &SniffWindow) -> Confidence {
987 if w.has_magic(0, b"ER") && w.has_magic(512, b"PM") {
990 Confidence::Yes {
991 how: "Apple Partition Map",
992 }
993 } else {
994 Confidence::No
995 }
996 }
997
998 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
999 Ok(Box::new(Apm::parse(src)?))
1000 }
1001}
1002
1003struct Apm {
1004 parent: DynSource,
1005 volumes: Vec<VolumeDesc>,
1006}
1007
1008const APM_MAP_CAP: u64 = 256 * 1024;
1011
1012impl Apm {
1013 fn parse(src: DynSource) -> VfsResult<Self> {
1014 let cap = src.len().clamp(1, APM_MAP_CAP) as usize;
1017 let mut head = vec![0u8; cap];
1018 let n = src.read_at(0, &mut head)?;
1019 let map = apm::parse(head.get(..n).unwrap_or(&[])).ok_or_else(|| VfsError::Decode {
1020 layer: "apm",
1021 offset: 0,
1022 detail: "not an Apple Partition Map".to_string(),
1023 bytes: SmallHex::new(head.get(..2).unwrap_or(&[])),
1024 })?;
1025
1026 let block_size = u64::from(map.block_size.max(1));
1027 let mut volumes = Vec::new();
1028 for (i, part) in map.partitions.iter().enumerate() {
1029 if part.type_name.eq_ignore_ascii_case("Apple_partition_map")
1031 || part.type_name.eq_ignore_ascii_case("Apple_Free")
1032 || part.type_name.eq_ignore_ascii_case("Apple_Void")
1033 {
1034 continue;
1035 }
1036 volumes.push(VolumeDesc {
1037 index: i,
1038 kind: VolumeKind::Partition,
1039 start: u64::from(part.start_block) * block_size,
1040 len: u64::from(part.block_count) * block_size,
1041 type_hint: Some(part.type_name.clone()),
1042 label: (!part.name.is_empty()).then(|| part.name.clone()),
1043 });
1044 }
1045
1046 Ok(Self {
1047 parent: src,
1048 volumes,
1049 })
1050 }
1051}
1052
1053impl VolumeSystem for Apm {
1054 fn scheme(&self) -> VolumeScheme {
1055 VolumeScheme::Apm
1056 }
1057
1058 fn volumes(&self) -> &[VolumeDesc] {
1059 &self.volumes
1060 }
1061
1062 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
1063 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
1064 what: "apm volume index",
1065 offset: index as u64,
1066 len: 1,
1067 bound: self.volumes.len() as u64,
1068 })?;
1069 Ok(Arc::new(SubRange::new(
1070 self.parent.clone(),
1071 desc.start,
1072 desc.len,
1073 )))
1074 }
1075}
1076
1077fn guid_hint(bytes: &[u8]) -> String {
1078 use std::fmt::Write as _;
1079 let mut s = String::with_capacity(bytes.len() * 2);
1080 for b in bytes {
1081 let _ = write!(s, "{b:02x}");
1082 }
1083 s
1084}
1085
1086struct VhdDecoder;
1089
1090impl ContainerOpen for VhdDecoder {
1091 fn format(&self) -> ContainerFormat {
1092 ContainerFormat::Vhd
1093 }
1094
1095 fn probe(&self, w: &SniffWindow) -> Confidence {
1096 if w.has_magic(0, b"conectix") {
1100 Confidence::Yes {
1101 how: "VHD conectix footer",
1102 }
1103 } else {
1104 Confidence::No
1105 }
1106 }
1107
1108 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1109 let len = src.len();
1110 let cursor = SourceCursor::new(src, 0, len);
1111 let reader =
1112 vhd::VhdReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1113 layer: "vhd",
1114 offset: 0,
1115 detail: e.to_string(),
1116 bytes: SmallHex::new(&[]),
1117 })?;
1118 let vsize = reader.virtual_disk_size();
1119 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1120 }
1121}
1122
1123struct Qcow2Decoder;
1126
1127impl ContainerOpen for Qcow2Decoder {
1128 fn format(&self) -> ContainerFormat {
1129 ContainerFormat::Qcow2
1130 }
1131
1132 fn probe(&self, w: &SniffWindow) -> Confidence {
1133 if w.has_magic(0, &[0x51, 0x46, 0x49, 0xfb]) {
1134 Confidence::Yes { how: "QCOW2 magic" }
1135 } else {
1136 Confidence::No
1137 }
1138 }
1139
1140 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1141 let len = src.len();
1142 let cursor = SourceCursor::new(src, 0, len);
1143 let reader =
1144 qcow2::Qcow2Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1145 layer: "qcow2",
1146 offset: 0,
1147 detail: e.to_string(),
1148 bytes: SmallHex::new(&[]),
1149 })?;
1150 let vsize = reader.virtual_disk_size();
1151 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1152 }
1153}
1154
1155struct VmdkDecoder;
1159
1160impl ContainerOpen for VmdkDecoder {
1161 fn format(&self) -> ContainerFormat {
1162 ContainerFormat::Vmdk
1163 }
1164
1165 fn probe(&self, w: &SniffWindow) -> Confidence {
1166 if w.has_magic(0, b"KDMV") {
1168 Confidence::Yes {
1169 how: "VMDK KDMV magic",
1170 }
1171 } else {
1172 Confidence::No
1173 }
1174 }
1175
1176 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1177 let len = src.len();
1178 let cursor = SourceCursor::new(src, 0, len);
1179 let boxed: Box<dyn vmdk::ReadSeek + Send> = Box::new(cursor);
1180 let reader = vmdk::VmdkReader::open(boxed).map_err(|e| VfsError::Decode {
1181 layer: "vmdk",
1182 offset: 0,
1183 detail: e.to_string(),
1184 bytes: SmallHex::new(&[]),
1185 })?;
1186 let vsize = reader.virtual_disk_size();
1187 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1188 }
1189}
1190
1191struct VhdxDecoder;
1193
1194impl ContainerOpen for VhdxDecoder {
1195 fn format(&self) -> ContainerFormat {
1196 ContainerFormat::Vhdx
1197 }
1198
1199 fn probe(&self, w: &SniffWindow) -> Confidence {
1200 if w.has_magic(0, vhdx::FILE_MAGIC) {
1201 Confidence::Yes {
1202 how: "VHDX file magic",
1203 }
1204 } else {
1205 Confidence::No
1206 }
1207 }
1208
1209 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1210 let len = src.len();
1211 let cursor = SourceCursor::new(src, 0, len);
1212 let reader =
1213 vhdx::VhdxReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1214 layer: "vhdx",
1215 offset: 0,
1216 detail: e.to_string(),
1217 bytes: SmallHex::new(&[]),
1218 })?;
1219 let vsize = reader.virtual_disk_size();
1220 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1221 }
1222}
1223
1224struct DmgDecoder;
1228
1229impl ContainerOpen for DmgDecoder {
1230 fn format(&self) -> ContainerFormat {
1231 ContainerFormat::Dmg
1232 }
1233
1234 fn probe(&self, w: &SniffWindow) -> Confidence {
1235 if w.has_magic_from_end(512, b"koly") {
1237 Confidence::Yes {
1238 how: "DMG koly trailer",
1239 }
1240 } else {
1241 Confidence::No
1242 }
1243 }
1244
1245 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1246 let len = src.len();
1247 let cursor = SourceCursor::new(src, 0, len);
1248 let reader = dmg::DmgReader::open(cursor).map_err(|e| VfsError::Decode {
1249 layer: "dmg",
1250 offset: 0,
1251 detail: e.to_string(),
1252 bytes: SmallHex::new(&[]),
1253 })?;
1254 let vsize = reader.virtual_disk_size();
1255 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1256 }
1257}
1258
1259struct Aff4Decoder;
1263
1264impl ContainerOpen for Aff4Decoder {
1265 fn format(&self) -> ContainerFormat {
1266 ContainerFormat::Aff4
1267 }
1268
1269 fn probe(&self, w: &SniffWindow) -> Confidence {
1270 if w.has_magic(0, &[0x50, 0x4b, 0x03, 0x04]) {
1271 Confidence::Maybe
1272 } else {
1273 Confidence::No
1274 }
1275 }
1276
1277 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1278 let len = src.len();
1279 let cursor = SourceCursor::new(src, 0, len);
1280 let reader =
1281 aff4::Aff4Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1282 layer: "aff4",
1283 offset: 0,
1284 detail: e.to_string(),
1285 bytes: SmallHex::new(&[]),
1286 })?;
1287 let vsize = reader.virtual_disk_size();
1288 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1289 }
1290}
1291
1292struct BitLockerProbe;
1297
1298impl EncryptionOpen for BitLockerProbe {
1299 fn scheme(&self) -> EncryptionScheme {
1300 EncryptionScheme::Bitlocker
1301 }
1302
1303 fn probe(&self, w: &SniffWindow) -> Confidence {
1304 if w.has_magic(3, b"-FVE-FS-") {
1305 Confidence::Yes {
1306 how: "BitLocker -FVE-FS- signature",
1307 }
1308 } else {
1309 Confidence::No
1310 }
1311 }
1312
1313 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1314 Ok(Box::new(bitlocker::vfs::BitlockerLayer::new(src)))
1315 }
1316}
1317
1318struct LuksProbe;
1324
1325impl EncryptionOpen for LuksProbe {
1326 fn scheme(&self) -> EncryptionScheme {
1327 EncryptionScheme::Luks2
1330 }
1331
1332 fn probe(&self, w: &SniffWindow) -> Confidence {
1333 if w.has_magic(0, &[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]) {
1335 Confidence::Yes { how: "LUKS magic" }
1336 } else {
1337 Confidence::No
1338 }
1339 }
1340
1341 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1342 Ok(Box::new(luks::vfs::LuksLayer::new(src)))
1343 }
1344}
1345
1346struct FileVaultProbe;
1351
1352impl EncryptionOpen for FileVaultProbe {
1353 fn scheme(&self) -> EncryptionScheme {
1354 EncryptionScheme::FileVault
1355 }
1356
1357 fn probe(&self, w: &SniffWindow) -> Confidence {
1358 if w.has_magic(88, b"CS") {
1361 Confidence::Yes {
1362 how: "CoreStorage CS volume header",
1363 }
1364 } else {
1365 Confidence::No
1366 }
1367 }
1368
1369 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1370 Ok(Box::new(filevault::vfs::FileVaultLayer::new(src)))
1371 }
1372}
1373
1374struct VeraCryptProbe;
1380
1381impl EncryptionOpen for VeraCryptProbe {
1382 fn scheme(&self) -> EncryptionScheme {
1383 EncryptionScheme::VeraCrypt
1384 }
1385
1386 fn probe(&self, _w: &SniffWindow) -> Confidence {
1387 Confidence::Maybe
1390 }
1391
1392 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1393 Ok(Box::new(veracrypt::vfs::VeraCryptLayer::new(src)))
1394 }
1395}
1396
1397const WALK_MAX_DEPTH: usize = 256;
1399
1400pub struct WalkEntry {
1403 pub path: Vec<Vec<u8>>,
1404 pub id: FileId,
1405 pub meta: FsMeta,
1406}
1407
1408pub fn walk(fs: &dyn FileSystem) -> VfsResult<Vec<WalkEntry>> {
1413 let mut out = Vec::new();
1414 let mut visited: HashSet<FileId> = HashSet::new();
1415 let mut stack: Vec<(Vec<Vec<u8>>, FileId, usize)> = vec![(Vec::new(), fs.root(), 0)];
1416 while let Some((prefix, dir_id, depth)) = stack.pop() {
1417 if depth > WALK_MAX_DEPTH || !visited.insert(dir_id) {
1418 continue;
1419 }
1420 for entry in fs.read_dir(dir_id)? {
1421 let entry = entry?;
1422 if matches!(entry.name.as_slice(), b"." | b"..") {
1423 continue;
1424 }
1425 let mut path = prefix.clone();
1426 path.push(entry.name);
1427 let meta = fs.meta(entry.id)?;
1428 let is_dir = matches!(meta.kind, NodeKind::Dir);
1429 out.push(WalkEntry {
1430 path: path.clone(),
1431 id: entry.id,
1432 meta,
1433 });
1434 if is_dir {
1435 stack.push((path, entry.id, depth + 1));
1436 }
1437 }
1438 }
1439 Ok(out)
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444 use super::*;
1445 use forensic_vfs::ImageSource;
1446 use std::io::Write;
1447
1448 struct Mem(Vec<u8>);
1449 impl ImageSource for Mem {
1450 fn len(&self) -> u64 {
1451 self.0.len() as u64
1452 }
1453 fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
1454 let off = usize::try_from(offset).unwrap_or(usize::MAX);
1455 let Some(s) = self.0.get(off..) else {
1456 return Ok(0);
1457 };
1458 let n = s.len().min(buf.len());
1459 buf[..n].copy_from_slice(&s[..n]);
1460 Ok(n)
1461 }
1462 }
1463 fn mem(b: Vec<u8>) -> DynSource {
1464 Arc::new(Mem(b))
1465 }
1466 fn window(b: &[u8]) -> SniffWindow<'_> {
1467 SniffWindow::new(0, b)
1468 }
1469
1470 #[test]
1471 fn default_openers_registers_btrfs_ufs_udf() {
1472 let kinds: Vec<FsKind> = default_openers()
1475 .filesystems()
1476 .iter()
1477 .map(|p| p.kind())
1478 .collect();
1479 assert!(kinds.contains(&FsKind::BTRFS), "btrfs prober registered");
1480 assert!(kinds.contains(&FsKind::UFS), "ufs prober registered");
1481 assert!(kinds.contains(&FsKind::UDF), "udf prober registered");
1482 assert_eq!(kinds.len(), 11, "8 existing + 3 new filesystem probers");
1483 }
1484
1485 #[test]
1486 fn default_openers_registers_the_archive_opener() {
1487 assert_eq!(
1491 default_openers().archives().len(),
1492 1,
1493 "the archive opener is registered"
1494 );
1495 }
1496
1497 #[test]
1498 fn default_openers_registers_the_four_encryption_layers() {
1499 let openers = default_openers();
1503 let layers = openers.encryption_layers();
1504 assert_eq!(layers.len(), 4, "4 FDE probers registered");
1505 let schemes: Vec<EncryptionScheme> = layers.iter().map(|p| p.scheme()).collect();
1506 assert!(
1507 schemes.contains(&EncryptionScheme::Bitlocker),
1508 "bitlocker prober registered: {schemes:?}"
1509 );
1510 assert!(
1511 schemes
1512 .iter()
1513 .any(|s| matches!(s, EncryptionScheme::Luks1 | EncryptionScheme::Luks2)),
1514 "luks prober registered: {schemes:?}"
1515 );
1516 assert!(
1517 schemes.contains(&EncryptionScheme::FileVault),
1518 "filevault prober registered: {schemes:?}"
1519 );
1520 assert!(
1521 schemes.contains(&EncryptionScheme::VeraCrypt),
1522 "veracrypt prober registered: {schemes:?}"
1523 );
1524 }
1525
1526 #[test]
1527 fn encryption_probes_detect_their_signatures() {
1528 let mut bde = vec![0u8; 64];
1530 bde[3..11].copy_from_slice(b"-FVE-FS-");
1531 assert!(matches!(
1532 BitLockerProbe.probe(&window(&bde)),
1533 Confidence::Yes { .. }
1534 ));
1535 assert_eq!(BitLockerProbe.probe(&window(&[0u8; 64])), Confidence::No);
1536 assert_eq!(BitLockerProbe.scheme(), EncryptionScheme::Bitlocker);
1537
1538 let mut luks = vec![0u8; 64];
1540 luks[0..6].copy_from_slice(&[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]);
1541 assert!(matches!(
1542 LuksProbe.probe(&window(&luks)),
1543 Confidence::Yes { .. }
1544 ));
1545 assert_eq!(LuksProbe.probe(&window(&[0u8; 64])), Confidence::No);
1546
1547 let mut fv = vec![0u8; 128];
1549 fv[88..90].copy_from_slice(b"CS");
1550 assert!(matches!(
1551 FileVaultProbe.probe(&window(&fv)),
1552 Confidence::Yes { .. }
1553 ));
1554 assert_eq!(FileVaultProbe.probe(&window(&[0u8; 128])), Confidence::No);
1555
1556 assert_eq!(VeraCryptProbe.probe(&window(&[])), Confidence::Maybe);
1558 assert_eq!(
1559 VeraCryptProbe.probe(&window(&[0xffu8; 512])),
1560 Confidence::Maybe
1561 );
1562 assert_eq!(VeraCryptProbe.scheme(), EncryptionScheme::VeraCrypt);
1563 }
1564
1565 #[test]
1566 fn default_is_new_and_probers_report_their_kinds() {
1567 let _ = Vfs::default().open_source(mem(vec![0u8; 64])).unwrap();
1568 assert_eq!(NtfsProbe.kind(), FsKind::NTFS);
1569 assert_eq!(MbrProbe.scheme(), VolumeScheme::Mbr);
1570 assert_eq!(GptProbe.scheme(), VolumeScheme::Gpt);
1571 }
1572
1573 #[test]
1574 fn probers_say_no_on_unrecognized_bytes() {
1575 let empty = window(&[]);
1576 assert_eq!(NtfsProbe.probe(&empty), Confidence::No);
1577 assert_eq!(MbrProbe.probe(&empty), Confidence::No);
1578 assert_eq!(GptProbe.probe(&empty), Confidence::No);
1579 let mut prot = vec![0u8; 512];
1581 prot[446 + 4] = 0xEE;
1582 prot[446 + 12] = 1; prot[510] = 0x55;
1584 prot[511] = 0xaa;
1585 assert_eq!(MbrProbe.probe(&window(&prot)), Confidence::No);
1586 }
1587
1588 #[test]
1589 fn mbr_probe_rejects_filesystem_boot_sectors() {
1590 let mut exfat = vec![0u8; 512];
1598 exfat[3..11].copy_from_slice(b"EXFAT ");
1599 exfat[510] = 0x55;
1600 exfat[511] = 0xaa;
1601 exfat[446 + 4] = 0x07; exfat[446 + 12] = 0x20; assert_eq!(
1605 MbrProbe.probe(&window(&exfat)),
1606 Confidence::No,
1607 "exFAT boot sector must not be misread as an MBR partition table"
1608 );
1609
1610 let mut ntfs = vec![0u8; 512];
1612 ntfs[3..11].copy_from_slice(b"NTFS ");
1613 ntfs[510] = 0x55;
1614 ntfs[511] = 0xaa;
1615 ntfs[446 + 4] = 0x07;
1616 ntfs[446 + 12] = 0x20;
1617 assert_eq!(
1618 MbrProbe.probe(&window(&ntfs)),
1619 Confidence::No,
1620 "NTFS boot sector must not be misread as an MBR partition table"
1621 );
1622
1623 let mut fat = vec![0u8; 512];
1625 fat[0] = 0xEB; fat[510] = 0x55;
1627 fat[511] = 0xaa;
1628 fat[446 + 4] = 0x07;
1629 fat[446 + 12] = 0x20;
1630 assert_eq!(
1631 MbrProbe.probe(&window(&fat)),
1632 Confidence::No,
1633 "FAT boot sector must not be misread as an MBR partition table"
1634 );
1635
1636 let mut mbr = vec![0u8; 512];
1640 mbr[446] = 0x80; mbr[446 + 4] = 0x07; mbr[446 + 8] = 1; mbr[446 + 12] = 4; mbr[510] = 0x55;
1645 mbr[511] = 0xaa;
1646 assert!(
1647 matches!(MbrProbe.probe(&window(&mbr)), Confidence::Yes { .. }),
1648 "a genuine MBR partition table must still be detected"
1649 );
1650 }
1651
1652 #[test]
1653 fn ntfs_magic_but_invalid_boot_is_a_loud_error() {
1654 let mut v = vec![0u8; 4096];
1657 v[3..11].copy_from_slice(b"NTFS ");
1658 assert!(Vfs::new().open_source(mem(v)).is_err());
1659 }
1660
1661 #[test]
1662 fn a_garbage_e01_path_fails_loud() {
1663 let mut f = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
1664 f.write_all(b"not really an EWF image").unwrap();
1665 f.flush().unwrap();
1666 assert!(Vfs::new().open(f.path()).is_err());
1667 }
1668
1669 #[test]
1670 fn gpt_parse_without_signature_errors_and_mbr_volume_index_is_bounded() {
1671 assert!(Gpt::parse(mem(vec![0u8; 1024])).is_err());
1673 let mut d = vec![0u8; 512];
1675 d[446 + 4] = 0x07;
1676 d[446 + 8] = 1; d[446 + 12] = 4; d[510] = 0x55;
1679 d[511] = 0xaa;
1680 let m = Mbr::parse(mem(d)).unwrap();
1681 assert_eq!(m.scheme(), VolumeScheme::Mbr);
1682 assert_eq!(m.volumes().len(), 1);
1683 assert!(m.open_volume(0).is_ok());
1684 assert!(m.open_volume(9).is_err());
1685 }
1686
1687 #[test]
1688 fn apm_maps_partitions_and_errors_on_non_apm() {
1689 let mut img = vec![0u8; 512];
1691 img[0..2].copy_from_slice(b"ER");
1692 img[2..4].copy_from_slice(&512u16.to_be_bytes()); let pm = |map_cnt: u32, pstart: u32, pcnt: u32, ptype: &str| {
1695 let mut e = vec![0u8; 512];
1696 e[0..2].copy_from_slice(b"PM");
1697 e[4..8].copy_from_slice(&map_cnt.to_be_bytes());
1698 e[8..12].copy_from_slice(&pstart.to_be_bytes());
1699 e[0x0c..0x10].copy_from_slice(&pcnt.to_be_bytes());
1700 e[0x30..0x30 + ptype.len()].copy_from_slice(ptype.as_bytes());
1701 e
1702 };
1703 img.extend(pm(2, 1, 63, "Apple_partition_map"));
1705 img.extend(pm(2, 4, 2, "Apple_HFS"));
1706 img.extend(vec![0u8; 4 * 512]);
1707
1708 let apm = Apm::parse(mem(img)).unwrap();
1709 assert_eq!(apm.scheme(), VolumeScheme::Apm);
1710 assert_eq!(apm.volumes().len(), 1); assert_eq!(apm.volumes()[0].start, 4 * 512); assert!(apm.open_volume(0).is_ok());
1713 assert!(apm.open_volume(9).is_err());
1714
1715 assert!(Apm::parse(mem(vec![0u8; 2048])).is_err());
1717 }
1718
1719 #[test]
1720 fn recursion_is_depth_capped_on_a_self_referential_mbr() {
1721 let mut d = vec![0u8; 1024];
1724 d[446 + 4] = 0x83; d[446 + 12] = 2;
1727 d[510] = 0x55;
1728 d[511] = 0xaa;
1729 assert!(Vfs::new().open_source(mem(d)).unwrap().is_none());
1730 }
1731
1732 #[test]
1733 fn container_decoders_report_format_and_error_on_bad_content() {
1734 assert_eq!(VhdDecoder.format(), ContainerFormat::Vhd);
1735 assert_eq!(Qcow2Decoder.format(), ContainerFormat::Qcow2);
1736 assert_eq!(VmdkDecoder.format(), ContainerFormat::Vmdk);
1737 assert_eq!(VhdxDecoder.format(), ContainerFormat::Vhdx);
1738 let mut vhd = vec![0u8; 4096];
1741 vhd[0..8].copy_from_slice(b"conectix");
1742 assert!(Vfs::new().open_source(mem(vhd)).is_err());
1743 let mut q = vec![0u8; 4096];
1744 q[0..4].copy_from_slice(&[0x51, 0x46, 0x49, 0xfb]);
1745 assert!(Vfs::new().open_source(mem(q)).is_err());
1746 let mut v = vec![0u8; 4096];
1747 v[0..4].copy_from_slice(b"KDMV");
1748 assert!(Vfs::new().open_source(mem(v)).is_err());
1749 let mut x = vec![0u8; 4096];
1750 x[0..8].copy_from_slice(vhdx::FILE_MAGIC);
1751 assert!(Vfs::new().open_source(mem(x)).is_err());
1752 }
1753
1754 #[test]
1755 fn dmg_decoder_format_probe_and_open_error() {
1756 assert_eq!(DmgDecoder.format(), ContainerFormat::Dmg);
1757 assert_eq!(
1759 DmgDecoder.probe(&SniffWindow::with_tail(0, &[], 1024, &[0u8; 512])),
1760 Confidence::No
1761 );
1762 let mut v = vec![0u8; 1024];
1767 v[512..516].copy_from_slice(b"koly");
1768 v[512 + 224..512 + 232].copy_from_slice(&u64::MAX.to_be_bytes());
1769 assert!(Vfs::new().open_source(mem(v)).is_err());
1770 }
1771
1772 #[test]
1773 fn aff4_decoder_format_probe_and_open_error() {
1774 assert_eq!(Aff4Decoder.format(), ContainerFormat::Aff4);
1775 assert_eq!(Aff4Decoder.probe(&window(&[])), Confidence::No);
1777 assert_eq!(
1778 Aff4Decoder.probe(&window(&[0x50, 0x4b, 0x03, 0x04])),
1779 Confidence::Maybe
1780 );
1781 let mut v = vec![0u8; 256];
1784 v[0..4].copy_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
1785 assert!(Vfs::new().open_source(mem(v)).is_err());
1786 }
1787
1788 #[test]
1789 fn a_valid_container_holding_no_filesystem_resolves_to_none() {
1790 let vhd = include_bytes!("../tests/data/empty.vhd").to_vec();
1793 assert!(Vfs::new().open_source(mem(vhd)).unwrap().is_none());
1794 }
1795
1796 #[test]
1797 fn ext4_probe_kind_and_open_error() {
1798 assert_eq!(Ext4Probe.kind(), FsKind::EXT);
1799 let mut v = vec![0u8; 4096];
1802 v[1080] = 0x53;
1803 v[1081] = 0xef;
1804 v[1048..1052].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1805 assert!(Vfs::new().open_source(mem(v)).is_err());
1806 }
1807
1808 #[test]
1809 fn iso9660_probe_kind_and_open_error() {
1810 assert_eq!(Iso9660Probe.kind(), FsKind::ISO9660);
1811 assert_eq!(Iso9660Probe.probe(&window(&[])), Confidence::No);
1812 let mut v = vec![0u8; 40 * 1024];
1815 v[32769..32774].copy_from_slice(b"CD001");
1816 assert_eq!(
1817 Iso9660Probe.probe(&window(&v)),
1818 Confidence::Yes {
1819 how: "ISO 9660 CD001 volume descriptor"
1820 }
1821 );
1822 assert!(Vfs::new().open_source(mem(v)).is_err());
1823 }
1824
1825 #[test]
1826 fn apfs_probe_kind_and_open_error() {
1827 assert_eq!(ApfsProbe.kind(), FsKind::APFS);
1828 assert_eq!(ApfsProbe.probe(&window(&[])), Confidence::No);
1829 let mut v = vec![0u8; 40 * 1024];
1832 v[32..36].copy_from_slice(b"NXSB");
1833 assert_eq!(
1834 ApfsProbe.probe(&window(&v)),
1835 Confidence::Yes {
1836 how: "APFS NXSB container superblock"
1837 }
1838 );
1839 assert!(Vfs::new().open_source(mem(v)).is_err());
1840 }
1841
1842 #[test]
1843 fn hfsplus_probe_kind_and_no_on_short_window() {
1844 assert_eq!(HfsPlusProbe.kind(), FsKind::HFS_PLUS);
1845 assert_eq!(HfsPlusProbe.probe(&window(&[])), Confidence::No);
1847 let mut v = vec![0u8; 40 * 1024];
1849 v[1024..1026].copy_from_slice(&[0x48, 0x58]);
1850 assert_eq!(
1851 HfsPlusProbe.probe(&window(&v)),
1852 Confidence::Yes {
1853 how: "HFS+/HFSX volume header"
1854 }
1855 );
1856 }
1857
1858 #[test]
1859 fn fat_and_exfat_magic_but_garbage_are_loud_errors() {
1860 let mut x = vec![0u8; 4096];
1862 x[3..11].copy_from_slice(b"EXFAT ");
1863 x[510] = 0x55;
1864 x[511] = 0xaa;
1865 assert!(Vfs::new().open_source(mem(x)).is_err());
1866
1867 let mut f = vec![0u8; 4096];
1870 f[0] = 0xEB;
1871 f[11..13].copy_from_slice(&512u16.to_le_bytes());
1872 f[510] = 0x55;
1873 f[511] = 0xaa;
1874 assert!(Vfs::new().open_source(mem(f)).is_err());
1875 }
1876
1877 #[test]
1878 fn guid_hint_is_lowercase_hex() {
1879 assert_eq!(guid_hint(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
1880 }
1881
1882 #[test]
1883 fn gpt_parse_skips_unused_and_reversed_entries() {
1884 let mut d = vec![0u8; 1280];
1885 d[512..520].copy_from_slice(b"EFI PART");
1886 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());
1892 d[1024 + 40..1024 + 48].copy_from_slice(&200u64.to_le_bytes());
1893 d[1152] = 0xa2;
1895 d[1152 + 32..1152 + 40].copy_from_slice(&500u64.to_le_bytes());
1896 d[1152 + 40..1152 + 48].copy_from_slice(&400u64.to_le_bytes());
1897 let g = Gpt::parse(mem(d)).unwrap();
1898 assert_eq!(g.scheme(), VolumeScheme::Gpt);
1899 assert_eq!(g.volumes().len(), 1, "reversed entry 1 is skipped");
1900 assert_eq!(g.volumes()[0].start, 100 * 512);
1901 assert!(g.open_volume(0).is_ok());
1902 assert!(g.open_volume(7).is_err());
1903
1904 assert_eq!(Mem(vec![1, 2, 3]).read_at(99, &mut [0u8; 4]).unwrap(), 0);
1906 }
1907
1908 const APFS_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/apfs_volume.bin");
1911 const EXT4_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/ext4.img");
1912
1913 fn apfs_live_xid() -> u64 {
1917 use std::io::{Read, Seek, SeekFrom};
1918 let bytes = std::fs::read(APFS_FIXTURE).unwrap();
1919 let mut c = apfs_core::ApfsContainer::open(std::io::Cursor::new(bytes)).unwrap();
1920 let bs = u64::from(c.superblock().block_size);
1921 let vaddr = c.volume_superblock_addrs().unwrap()[0];
1922 let mut r = c.into_reader();
1923 r.seek(SeekFrom::Start(vaddr * bs)).unwrap();
1924 let mut buf = vec![0u8; bs as usize];
1925 r.read_exact(&mut buf).unwrap();
1926 apfs_core::volume::ApfsVolume::parse(&buf).unwrap().xid()
1927 }
1928
1929 fn zeros_file() -> tempfile::NamedTempFile {
1930 let mut f = tempfile::NamedTempFile::new().unwrap();
1931 f.write_all(&[0u8; 4096]).unwrap();
1932 f.flush().unwrap();
1933 f
1934 }
1935
1936 #[test]
1937 fn epoch_from_create_time_round_trips_and_orders() {
1938 let t = 0x0123_4567_89ab_cdefu64;
1939 let tag = epoch_from_create_time(t);
1940 assert_eq!(&tag.0[0..24], &[0u8; 24], "high 24 bytes are zero");
1941 assert_eq!(
1942 u64::from_be_bytes(tag.0[24..32].try_into().unwrap()),
1943 t,
1944 "create_time round-trips out of the low 8 bytes"
1945 );
1946 assert!(
1947 epoch_from_create_time(t + 1).0 > tag.0,
1948 "a later create_time yields a greater tag"
1949 );
1950 }
1951
1952 #[test]
1953 fn snapshot_view_carries_epoch_and_snapshot_locator() {
1954 let base = Locator::file("/ev.dmg");
1955 let v = snapshot_view(&base, 42, "daily".to_string(), 1000);
1956 assert_eq!(v.xid, 42);
1957 assert_eq!(v.name, "daily");
1958 assert_eq!(v.epoch, epoch_from_create_time(1000));
1959 assert!(matches!(
1960 v.locator.layer,
1961 Layer::Snapshot {
1962 store: SnapshotRef::ApfsXid(42)
1963 }
1964 ));
1965 }
1966
1967 #[test]
1968 fn snapshots_on_unrecognized_source_is_empty() {
1969 let f = zeros_file();
1970 assert!(Vfs::new().snapshots(f.path()).unwrap().is_empty());
1971 }
1972
1973 #[test]
1974 fn snapshots_on_non_apfs_filesystem_is_empty() {
1975 assert!(Vfs::new()
1977 .snapshots(Path::new(EXT4_FIXTURE))
1978 .unwrap()
1979 .is_empty());
1980 }
1981
1982 #[test]
1983 fn open_snapshot_without_filesystem_is_bootstrap_error() {
1984 let f = zeros_file();
1985 assert!(matches!(
1986 Vfs::new().open_snapshot(f.path(), 1),
1987 Err(VfsError::Bootstrap { .. })
1988 ));
1989 }
1990
1991 #[test]
1992 fn open_snapshot_on_non_apfs_is_unsupported() {
1993 assert!(matches!(
1994 Vfs::new().open_snapshot(Path::new(EXT4_FIXTURE), 1),
1995 Err(VfsError::Unsupported { .. })
1996 ));
1997 }
1998
1999 #[test]
2000 fn open_snapshot_unknown_xid_is_a_loud_decode_error() {
2001 let bogus = apfs_live_xid().wrapping_add(0xDEAD_BEEF);
2004 assert!(matches!(
2005 Vfs::new().open_snapshot(Path::new(APFS_FIXTURE), bogus),
2006 Err(VfsError::Decode { .. })
2007 ));
2008 }
2009
2010 #[test]
2011 fn open_snapshot_at_live_xid_mounts_and_walks() {
2012 let ev = Vfs::new()
2013 .open_snapshot(Path::new(APFS_FIXTURE), apfs_live_xid())
2014 .expect("open live-xid snapshot");
2015 let uri = ev.root.to_uri();
2016 assert!(
2017 uri.contains("snapshot:apfs") && uri.contains("fs:apfs"),
2018 "locator names the snapshot + APFS layers: {uri}"
2019 );
2020 let fs = ev.fs.expect("snapshot mounts a filesystem");
2021 let names: Vec<String> = walk(fs.as_ref())
2022 .unwrap()
2023 .into_iter()
2024 .filter_map(|e| {
2025 e.path
2026 .last()
2027 .map(|n| String::from_utf8_lossy(n).to_string())
2028 })
2029 .collect();
2030 assert!(names.iter().any(|n| n == "plain.txt"), "walk: {names:?}");
2031 }
2032
2033 #[test]
2036 fn engine_resolution_matches_openers_open_directly() {
2037 let bytes = std::fs::read(EXT4_FIXTURE).unwrap();
2043 let len = bytes.len() as u64;
2044
2045 let via_engine = Vfs::new()
2047 .open_source(mem(bytes.clone()))
2048 .unwrap()
2049 .expect("engine resolves the ext4 fixture");
2050
2051 let base = Locator::root(Layer::Range { start: 0, len });
2053 let resolved = default_openers()
2054 .open(mem(bytes), base, 0)
2055 .unwrap()
2056 .expect("Openers::open resolves the ext4 fixture");
2057
2058 let names = |fs: &dyn FileSystem| {
2060 let mut v: Vec<Vec<Vec<u8>>> = walk(fs).unwrap().into_iter().map(|e| e.path).collect();
2061 v.sort();
2062 v
2063 };
2064 assert_eq!(
2065 names(via_engine.as_ref()),
2066 names(resolved.fs.as_ref()),
2067 "engine and Openers::open mount the same filesystem"
2068 );
2069 let resolved_uri = resolved.spec.to_uri();
2071 assert!(
2072 matches!(
2073 resolved.spec.layer,
2074 Layer::Fs {
2075 kind: FsKind::EXT,
2076 ..
2077 }
2078 ),
2079 "registry resolved spec tops with fs:ext: {resolved_uri}"
2080 );
2081 }
2082}