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 match aff4::container_kind(path) {
75 Ok(aff4::ContainerKind::Logical) => {
76 if let Some(fs) = containerfs::open_aff4_logical(path)? {
77 return Ok(container_evidence(&base_spec, fs));
78 } }
80 Ok(_) => {}
83 Err(_) => {
90 let name = path.file_name().and_then(|s| s.to_str());
91 if let Some(fs) = containerfs::open_archive(&base, name)? {
92 return Ok(container_evidence(&base_spec, fs));
93 } }
95 }
96 }
97
98 if let Some(r) = self.openers.open(base.clone(), base_spec.clone(), 0)? {
99 return Ok(Evidence {
100 root: r.spec,
101 fs: Some(r.fs),
102 });
103 }
104 let name = path.file_name().and_then(|s| s.to_str());
113 if let Some(fs) = containerfs::open_archive(&base, name)? {
114 return Ok(container_evidence(&base_spec, fs));
115 }
116 if let Some(fs) = containerfs::open_ad1(path)? {
117 return Ok(container_evidence(&base_spec, fs));
118 }
119 if let Some(fs) = containerfs::open_dar(path)? {
120 return Ok(container_evidence(&base_spec, fs));
121 }
122 Ok(Evidence {
123 root: base_spec,
124 fs: None,
125 })
126 }
127
128 pub fn open_all(&self, path: &Path) -> VfsResult<Vec<Evidence>> {
148 const HEAD_CAP: u64 = 128 * 1024;
152 const TAIL_CAP: u64 = 4096;
153
154 let base = open_base(path)?;
155 let base_spec = Locator::file(path);
156
157 let total = base.len();
158 let head_len = total.clamp(1, HEAD_CAP) as usize;
159 let mut head = vec![0u8; head_len];
160 let hn = base.read_at(0, &mut head)?;
161 let tail_len = total.min(TAIL_CAP);
162 let mut tail = vec![0u8; tail_len as usize];
163 let tn = base.read_at(total - tail_len, &mut tail)?;
164 let window = SniffWindow::with_tail(
165 0,
166 head.get(..hn).unwrap_or(&[]),
167 total,
168 tail.get(..tn).unwrap_or(&[]),
169 );
170
171 for vsp in self.openers.volume_systems() {
172 if !vsp.probe(&window).is_candidate() {
173 continue;
174 }
175 let vs = vsp.open(base.clone())?;
176 let mut out = Vec::new();
177 for index in 0..vs.volumes().len() {
178 let sub = vs.open_volume(index)?;
179 let child = base_spec.clone().push(Layer::Volume {
180 scheme: vsp.scheme(),
181 index,
182 guid: None,
183 });
184 if let Some(r) = self.openers.open(sub, child, 1)? {
185 out.push(Evidence {
186 root: r.spec,
187 fs: Some(r.fs),
188 });
189 }
190 }
191 return Ok(out);
192 }
193
194 Ok(vec![self.open(path)?])
197 }
198
199 pub fn open_source(&self, source: DynSource) -> VfsResult<Option<DynFs>> {
202 let base = Locator::root(Layer::Range {
203 start: 0,
204 len: source.len(),
205 });
206 Ok(self.openers.open(source, base, 0)?.map(|r| r.fs))
207 }
208
209 pub fn snapshots(&self, path: &Path) -> VfsResult<Vec<SnapshotView>> {
224 let base = open_base(path)?;
225 let base_spec = Locator::file(path);
226 let Some(resolved) = self.openers.open(base, base_spec, 0)? else {
227 return Ok(Vec::new());
228 };
229 if !is_apfs(&resolved.spec) {
230 return Ok(Vec::new());
231 }
232 let source_spec = resolved.source_spec;
233 let len = resolved.source.len();
234 let cursor = SourceCursor::new(resolved.source, 0, len);
235 let snaps = apfs_core::vfs::ApfsFs::snapshots(cursor).map_err(map_apfs_err)?;
236 Ok(snaps
237 .into_iter()
238 .map(|s| snapshot_view(&source_spec, s.xid, s.name, s.create_time))
239 .collect())
240 }
241
242 pub fn open_snapshot(&self, path: &Path, xid: u64) -> VfsResult<Evidence> {
255 let base = open_base(path)?;
256 let base_spec = Locator::file(path);
257 let resolved = self
258 .openers
259 .open(base, base_spec, 0)?
260 .ok_or(VfsError::Bootstrap {
261 stage: "apfs snapshot",
262 detail: "no filesystem detected in evidence".to_string(),
263 })?;
264 if !is_apfs(&resolved.spec) {
265 return Err(VfsError::Unsupported {
266 layer: "snapshot",
267 scheme: "non-APFS filesystem has no APFS snapshot".to_string(),
268 });
269 }
270 let source_spec = resolved.source_spec;
271 let len = resolved.source.len();
272 let cursor = SourceCursor::new(resolved.source, 0, len);
273 let fs = apfs_core::vfs::ApfsFs::open_snapshot(cursor, xid).map_err(map_apfs_err)?;
274 let root = source_spec
275 .push(Layer::Snapshot {
276 store: SnapshotRef::ApfsXid(xid),
277 })
278 .push(Layer::Fs {
279 kind: FsKind::APFS,
280 at: NodeAddr::Path(Vec::new()),
281 });
282 Ok(Evidence {
283 root,
284 fs: Some(Arc::new(fs)),
285 })
286 }
287}
288
289#[derive(Debug, Clone)]
293pub struct SnapshotView {
294 pub epoch: EpochTag,
296 pub xid: u64,
298 pub name: String,
300 pub locator: Locator,
302}
303
304fn container_evidence(base_spec: &Locator, fs: DynFs) -> Evidence {
307 let root = base_spec.clone().push(Layer::Fs {
308 kind: fs.kind(),
309 at: NodeAddr::Path(Vec::new()),
310 });
311 Evidence { root, fs: Some(fs) }
312}
313
314fn is_apfs(spec: &Locator) -> bool {
316 matches!(
317 spec.layer,
318 Layer::Fs {
319 kind: FsKind::APFS,
320 ..
321 }
322 )
323}
324
325fn snapshot_view(source_spec: &Locator, xid: u64, name: String, create_time: u64) -> SnapshotView {
330 SnapshotView {
331 epoch: epoch_from_create_time(create_time),
332 xid,
333 name,
334 locator: source_spec.clone().push(Layer::Snapshot {
335 store: SnapshotRef::ApfsXid(xid),
336 }),
337 }
338}
339
340fn epoch_from_create_time(create_time_ns: u64) -> EpochTag {
346 let mut bytes = [0u8; 32];
347 bytes[24..32].copy_from_slice(&create_time_ns.to_be_bytes());
348 EpochTag::from_bytes(bytes)
349}
350
351#[allow(clippy::needless_pass_by_value)]
354fn map_apfs_err(e: apfs_core::ApfsError) -> VfsError {
355 VfsError::Decode {
356 layer: "apfs snapshot",
357 offset: 0,
358 detail: e.to_string(),
359 bytes: SmallHex::new(&[]),
360 }
361}
362
363#[must_use]
368pub fn default_openers() -> Openers {
369 Openers::new()
370 .filesystem(NtfsProbe)
371 .filesystem(Ext4Probe)
372 .filesystem(XfsProbe)
373 .filesystem(Iso9660Probe)
374 .filesystem(ApfsProbe)
375 .filesystem(HfsPlusProbe)
376 .filesystem(ExFatProbe)
377 .filesystem(FatProbe)
378 .filesystem(UdfProbe)
379 .filesystem(UfsProbe)
380 .filesystem(BtrfsProbe)
381 .filesystem(ZfsProbe)
382 .volume_system(GptProbe)
383 .volume_system(MbrProbe)
384 .volume_system(ApmProbe)
385 .container(VhdDecoder)
386 .container(Qcow2Decoder)
387 .container(VmdkDecoder)
388 .container(VhdxDecoder)
389 .container(DmgDecoder)
390 .container(Aff4Decoder)
391 .archive(archive_core::ArchiveOpener)
392 .encryption(BitLockerProbe)
393 .encryption(LuksProbe)
394 .encryption(FileVaultProbe)
395 .encryption(VeraCryptProbe)
396}
397
398fn open_base(path: &Path) -> VfsResult<DynSource> {
402 if is_ewf(path) {
403 let reader = ewf::EwfReader::open(path).map_err(|e| VfsError::Bootstrap {
404 stage: "ewf::open",
405 detail: e.to_string(),
406 })?;
407 Ok(Arc::new(reader))
408 } else {
409 Ok(Arc::new(FileSource::open(path)?))
410 }
411}
412
413fn base_has_zip_magic(base: &DynSource) -> VfsResult<bool> {
418 let mut magic = [0u8; 4];
419 let n = base.read_at(0, &mut magic)?;
420 Ok(n >= 4 && magic == [0x50, 0x4b, 0x03, 0x04])
421}
422
423fn is_ewf(path: &Path) -> bool {
424 path.extension()
425 .and_then(|e| e.to_str())
426 .is_some_and(|e| e.eq_ignore_ascii_case("e01") || e.eq_ignore_ascii_case("ex01"))
427}
428
429struct NtfsProbe;
431
432impl FileSystemOpen for NtfsProbe {
433 fn kind(&self) -> FsKind {
434 FsKind::NTFS
435 }
436
437 fn probe(&self, w: &SniffWindow) -> Confidence {
438 if w.has_magic(3, b"NTFS ") {
440 Confidence::Yes { how: "NTFS OEM id" }
441 } else {
442 Confidence::No
443 }
444 }
445
446 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
447 let len = src.len();
448 let cursor = SourceCursor::new(src, 0, len);
449 let fs = ntfs_core::NtfsFs::open(cursor).map_err(|e| VfsError::Decode {
450 layer: "ntfs",
451 offset: 0,
452 detail: e.to_string(),
453 bytes: SmallHex::new(&[]),
454 })?;
455 Ok(Arc::new(fs))
456 }
457}
458
459struct Ext4Probe;
462
463impl FileSystemOpen for Ext4Probe {
464 fn kind(&self) -> FsKind {
465 FsKind::EXT
466 }
467
468 fn probe(&self, w: &SniffWindow) -> Confidence {
469 if w.has_magic(1080, &[0x53, 0xEF]) {
472 Confidence::Yes {
473 how: "ext2/3/4 superblock magic",
474 }
475 } else {
476 Confidence::No
477 }
478 }
479
480 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
481 let len = src.len();
482 let cursor = SourceCursor::new(src, 0, len);
483 let fs = ext4fs::Ext4Fs::open(cursor).map_err(|e| VfsError::Decode {
484 layer: "ext4",
485 offset: 0,
486 detail: e.to_string(),
487 bytes: SmallHex::new(&[]),
488 })?;
489 Ok(Arc::new(fs))
490 }
491}
492
493struct XfsProbe;
497
498impl FileSystemOpen for XfsProbe {
499 fn kind(&self) -> FsKind {
500 FsKind::XFS
501 }
502
503 fn probe(&self, w: &SniffWindow) -> Confidence {
504 xfs::vfs::xfs_probe(w)
505 }
506
507 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
508 Ok(Arc::new(xfs::vfs::XfsFs::open(&src)?))
509 }
510}
511
512struct Iso9660Probe;
516
517impl FileSystemOpen for Iso9660Probe {
518 fn kind(&self) -> FsKind {
519 FsKind::ISO9660
520 }
521
522 fn probe(&self, w: &SniffWindow) -> Confidence {
523 if w.has_magic(32769, b"CD001") {
526 Confidence::Yes {
527 how: "ISO 9660 CD001 volume descriptor",
528 }
529 } else {
530 Confidence::No
531 }
532 }
533
534 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
535 let len = src.len();
536 let cursor = SourceCursor::new(src, 0, len);
537 let fs = iso::vfs::IsoVfs::open(cursor).map_err(|e| VfsError::Decode {
538 layer: "iso9660",
539 offset: 0,
540 detail: e.to_string(),
541 bytes: SmallHex::new(&[]),
542 })?;
543 Ok(Arc::new(fs))
544 }
545}
546
547struct ApfsProbe;
550
551impl FileSystemOpen for ApfsProbe {
552 fn kind(&self) -> FsKind {
553 FsKind::APFS
554 }
555
556 fn probe(&self, w: &SniffWindow) -> Confidence {
557 if w.has_magic(32, b"NXSB") {
558 Confidence::Yes {
559 how: "APFS NXSB container superblock",
560 }
561 } else {
562 Confidence::No
563 }
564 }
565
566 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
567 let len = src.len();
568 let cursor = SourceCursor::new(src, 0, len);
569 let fs = apfs_core::vfs::ApfsFs::open(cursor).map_err(|e| VfsError::Decode {
570 layer: "apfs",
571 offset: 0,
572 detail: e.to_string(),
573 bytes: SmallHex::new(&[]),
574 })?;
575 Ok(Arc::new(fs))
576 }
577}
578
579struct HfsPlusProbe;
582
583impl FileSystemOpen for HfsPlusProbe {
584 fn kind(&self) -> FsKind {
585 FsKind::HFS_PLUS
586 }
587
588 fn probe(&self, w: &SniffWindow) -> Confidence {
589 match w.at(1024, 2) {
590 Some([0x48, 0x2B | 0x58]) => Confidence::Yes {
591 how: "HFS+/HFSX volume header",
592 },
593 _ => Confidence::No,
594 }
595 }
596
597 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
598 let len = src.len();
601 let mut volume = vec![0u8; usize::try_from(len).unwrap_or(usize::MAX)];
602 let n = src.read_at(0, &mut volume)?;
603 volume.truncate(n);
604 let fs = hfsplus::vfs::HfsFs::new(volume)?;
605 Ok(Arc::new(fs))
606 }
607}
608
609struct ExFatProbe;
613
614impl FileSystemOpen for ExFatProbe {
615 fn kind(&self) -> FsKind {
616 FsKind::EXFAT
617 }
618
619 fn probe(&self, w: &SniffWindow) -> Confidence {
620 if w.at(510, 2) == Some(&[0x55, 0xaa]) && w.has_magic(3, b"EXFAT ") {
621 Confidence::Yes {
622 how: "exFAT boot signature",
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 = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
633 layer: "exfat",
634 offset: 0,
635 detail: e.to_string(),
636 bytes: SmallHex::new(&[]),
637 })?;
638 Ok(Arc::new(fs))
639 }
640}
641
642struct FatProbe;
645
646impl FileSystemOpen for FatProbe {
647 fn kind(&self) -> FsKind {
648 FsKind::FAT
649 }
650
651 fn probe(&self, w: &SniffWindow) -> Confidence {
652 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
653 return Confidence::No;
654 }
655 let jump = w.at(0, 1).and_then(|s| s.first().copied());
656 let jump_ok = matches!(jump, Some(0xEB | 0xE9));
657 let bps = w
658 .at(11, 2)
659 .and_then(|b| <[u8; 2]>::try_from(b).ok())
660 .map_or(0, u16::from_le_bytes);
661 if jump_ok && bps.is_power_of_two() && (512..=4096).contains(&bps) {
662 Confidence::Yes { how: "FAT BPB" }
663 } else {
664 Confidence::No
665 }
666 }
667
668 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
669 let len = src.len();
670 let cursor = SourceCursor::new(src, 0, len);
671 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
672 layer: "fat",
673 offset: 0,
674 detail: e.to_string(),
675 bytes: SmallHex::new(&[]),
676 })?;
677 Ok(Arc::new(fs))
678 }
679}
680
681struct UdfProbe;
688
689impl FileSystemOpen for UdfProbe {
690 fn kind(&self) -> FsKind {
691 FsKind::UDF
692 }
693
694 fn probe(&self, w: &SniffWindow) -> Confidence {
695 if udf_forensic::detect_udf(&mut Cursor::new(w.bytes())) {
696 Confidence::Yes {
697 how: "UDF NSR02/NSR03 volume recognition sequence",
698 }
699 } else {
700 Confidence::No
701 }
702 }
703
704 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
705 let len = src.len();
706 let cursor = SourceCursor::new(src, 0, len);
707 let fs = udf_forensic::vfs::UdfVfs::open(cursor)?;
708 Ok(Arc::new(fs))
709 }
710}
711
712struct UfsProbe;
719
720impl FileSystemOpen for UfsProbe {
721 fn kind(&self) -> FsKind {
722 FsKind::UFS
723 }
724
725 fn probe(&self, w: &SniffWindow) -> Confidence {
726 ufs::vfs::ufs_probe(w)
727 }
728
729 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
730 let fs = ufs::vfs::UfsFs::open(&src)?;
731 Ok(Arc::new(fs))
732 }
733}
734
735const BTRFS_MAGIC_OFFSET: usize = btrfs_core::BTRFS_SUPER_INFO_OFFSET as usize + 0x40;
739
740struct BtrfsProbe;
753
754impl FileSystemOpen for BtrfsProbe {
755 fn kind(&self) -> FsKind {
756 FsKind::BTRFS
757 }
758
759 fn probe(&self, w: &SniffWindow) -> Confidence {
760 if w.has_magic(BTRFS_MAGIC_OFFSET, &btrfs_core::BTRFS_MAGIC) {
761 Confidence::Yes {
762 how: "btrfs _BHRfS_M superblock magic",
763 }
764 } else {
765 Confidence::No
766 }
767 }
768
769 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
770 let fs = btrfs_core::vfs::BtrfsFs::open(&*src)?;
771 Ok(Arc::new(fs))
772 }
773}
774
775struct ZfsProbe;
793
794impl FileSystemOpen for ZfsProbe {
795 fn kind(&self) -> FsKind {
796 FsKind::ZFS
797 }
798
799 fn probe(&self, w: &SniffWindow) -> Confidence {
800 zfs::vfs::zfs_probe(w)
801 }
802
803 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
804 let fs = zfs::vfs::ZfsFs::open(&src)?;
805 Ok(Arc::new(fs))
806 }
807}
808
809struct MbrProbe;
812
813impl VolumeSystemOpen for MbrProbe {
814 fn scheme(&self) -> VolumeScheme {
815 VolumeScheme::Mbr
816 }
817
818 fn probe(&self, w: &SniffWindow) -> Confidence {
819 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
825 return Confidence::No;
826 }
827 if w.has_magic(3, b"EXFAT ") || w.has_magic(3, b"NTFS ") {
832 return Confidence::No;
833 }
834 let jump = w.at(0, 1).and_then(|s| s.first().copied());
835 if matches!(jump, Some(0xEB | 0xE9)) {
836 return Confidence::No;
837 }
838 let data = w.bytes();
839 for i in 0..4usize {
840 let base = 446 + i * 16;
841 let boot_flag = data.get(base).copied().unwrap_or(0xFF);
844 let ptype = data.get(base + 4).copied().unwrap_or(0);
845 let size = le_u32(data, base + 12);
846 if matches!(boot_flag, 0x00 | 0x80) && ptype != 0 && ptype != 0xEE && size != 0 {
847 return Confidence::Yes {
848 how: "MBR partition table",
849 };
850 }
851 }
852 Confidence::No
853 }
854
855 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
856 Ok(Box::new(Mbr::parse(src)?))
857 }
858}
859
860struct Mbr {
862 parent: DynSource,
863 volumes: Vec<VolumeDesc>,
864}
865
866impl Mbr {
867 fn parse(src: DynSource) -> VfsResult<Self> {
868 let mut sector = [0u8; 512];
869 src.read_at(0, &mut sector)?;
870 let mut volumes = Vec::new();
871 for i in 0..4usize {
872 let base = 446 + i * 16;
873 let ptype = sector.get(base + 4).copied().unwrap_or(0);
874 let start_lba = le_u32(§or, base + 8);
875 let size = le_u32(§or, base + 12);
876 if ptype == 0 || ptype == 0xEE || size == 0 {
877 continue;
878 }
879 volumes.push(VolumeDesc {
880 index: i,
881 kind: VolumeKind::Partition,
882 start: u64::from(start_lba) * 512,
883 len: u64::from(size) * 512,
884 type_hint: Some(format!("0x{ptype:02x}")),
885 label: None,
886 });
887 }
888 Ok(Self {
889 parent: src,
890 volumes,
891 })
892 }
893}
894
895impl VolumeSystem for Mbr {
896 fn scheme(&self) -> VolumeScheme {
897 VolumeScheme::Mbr
898 }
899
900 fn volumes(&self) -> &[VolumeDesc] {
901 &self.volumes
902 }
903
904 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
905 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
906 what: "mbr volume index",
907 offset: index as u64,
908 len: 1,
909 bound: self.volumes.len() as u64,
910 })?;
911 Ok(Arc::new(SubRange::new(
912 self.parent.clone(),
913 desc.start,
914 desc.len,
915 )))
916 }
917}
918
919struct GptProbe;
923
924impl VolumeSystemOpen for GptProbe {
925 fn scheme(&self) -> VolumeScheme {
926 VolumeScheme::Gpt
927 }
928
929 fn probe(&self, w: &SniffWindow) -> Confidence {
930 if w.has_magic(512, b"EFI PART") {
932 Confidence::Yes {
933 how: "GPT EFI PART header",
934 }
935 } else {
936 Confidence::No
937 }
938 }
939
940 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
941 Ok(Box::new(Gpt::parse(src)?))
942 }
943}
944
945struct Gpt {
947 parent: DynSource,
948 volumes: Vec<VolumeDesc>,
949}
950
951impl Gpt {
952 fn parse(src: DynSource) -> VfsResult<Self> {
953 let mut header = [0u8; 512];
955 src.read_at(512, &mut header)?;
956 if header.get(0..8) != Some(b"EFI PART".as_slice()) {
957 return Err(VfsError::Decode {
958 layer: "gpt",
959 offset: 512,
960 detail: "missing EFI PART signature".to_string(),
961 bytes: SmallHex::new(header.get(0..8).unwrap_or(&[])),
962 });
963 }
964 let entries_lba = le_u64(&header, 72);
965 let num_entries = le_u32(&header, 80).min(256) as usize;
967 let entry_size = le_u32(&header, 84).clamp(128, 512) as usize;
968 let array_len = num_entries.checked_mul(entry_size).unwrap_or(0);
969 let mut arr = vec![0u8; array_len];
970 src.read_at(entries_lba.saturating_mul(512), &mut arr)?;
971
972 let mut volumes = Vec::new();
973 for i in 0..num_entries {
974 let Some(base) = i.checked_mul(entry_size) else {
975 break; };
977 let Some(entry) = arr.get(base..base.saturating_add(entry_size)) else {
978 break; };
980 let type_guid = entry.get(0..16).unwrap_or(&[]);
982 if type_guid.iter().all(|&b| b == 0) {
983 continue;
984 }
985 let first = le_u64(entry, 32);
986 let last = le_u64(entry, 40);
987 if last < first {
988 continue;
989 }
990 let sectors = last - first + 1;
991 volumes.push(VolumeDesc {
992 index: i,
993 kind: VolumeKind::Partition,
994 start: first.saturating_mul(512),
995 len: sectors.saturating_mul(512),
996 type_hint: Some(guid_hint(type_guid)),
997 label: None,
998 });
999 }
1000 Ok(Self {
1001 parent: src,
1002 volumes,
1003 })
1004 }
1005}
1006
1007impl VolumeSystem for Gpt {
1008 fn scheme(&self) -> VolumeScheme {
1009 VolumeScheme::Gpt
1010 }
1011
1012 fn volumes(&self) -> &[VolumeDesc] {
1013 &self.volumes
1014 }
1015
1016 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
1017 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
1018 what: "gpt volume index",
1019 offset: index as u64,
1020 len: 1,
1021 bound: self.volumes.len() as u64,
1022 })?;
1023 Ok(Arc::new(SubRange::new(
1024 self.parent.clone(),
1025 desc.start,
1026 desc.len,
1027 )))
1028 }
1029}
1030
1031struct ApmProbe;
1034
1035impl VolumeSystemOpen for ApmProbe {
1036 fn scheme(&self) -> VolumeScheme {
1037 VolumeScheme::Apm
1038 }
1039
1040 fn probe(&self, w: &SniffWindow) -> Confidence {
1041 if w.has_magic(0, b"ER") && w.has_magic(512, b"PM") {
1044 Confidence::Yes {
1045 how: "Apple Partition Map",
1046 }
1047 } else {
1048 Confidence::No
1049 }
1050 }
1051
1052 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
1053 Ok(Box::new(Apm::parse(src)?))
1054 }
1055}
1056
1057struct Apm {
1058 parent: DynSource,
1059 volumes: Vec<VolumeDesc>,
1060}
1061
1062const APM_MAP_CAP: u64 = 256 * 1024;
1065
1066impl Apm {
1067 fn parse(src: DynSource) -> VfsResult<Self> {
1068 let cap = src.len().clamp(1, APM_MAP_CAP) as usize;
1071 let mut head = vec![0u8; cap];
1072 let n = src.read_at(0, &mut head)?;
1073 let map = apm::parse(head.get(..n).unwrap_or(&[])).ok_or_else(|| VfsError::Decode {
1074 layer: "apm",
1075 offset: 0,
1076 detail: "not an Apple Partition Map".to_string(),
1077 bytes: SmallHex::new(head.get(..2).unwrap_or(&[])),
1078 })?;
1079
1080 let block_size = u64::from(map.block_size.max(1));
1081 let mut volumes = Vec::new();
1082 for (i, part) in map.partitions.iter().enumerate() {
1083 if part.type_name.eq_ignore_ascii_case("Apple_partition_map")
1085 || part.type_name.eq_ignore_ascii_case("Apple_Free")
1086 || part.type_name.eq_ignore_ascii_case("Apple_Void")
1087 {
1088 continue;
1089 }
1090 volumes.push(VolumeDesc {
1091 index: i,
1092 kind: VolumeKind::Partition,
1093 start: u64::from(part.start_block) * block_size,
1094 len: u64::from(part.block_count) * block_size,
1095 type_hint: Some(part.type_name.clone()),
1096 label: (!part.name.is_empty()).then(|| part.name.clone()),
1097 });
1098 }
1099
1100 Ok(Self {
1101 parent: src,
1102 volumes,
1103 })
1104 }
1105}
1106
1107impl VolumeSystem for Apm {
1108 fn scheme(&self) -> VolumeScheme {
1109 VolumeScheme::Apm
1110 }
1111
1112 fn volumes(&self) -> &[VolumeDesc] {
1113 &self.volumes
1114 }
1115
1116 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
1117 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
1118 what: "apm volume index",
1119 offset: index as u64,
1120 len: 1,
1121 bound: self.volumes.len() as u64,
1122 })?;
1123 Ok(Arc::new(SubRange::new(
1124 self.parent.clone(),
1125 desc.start,
1126 desc.len,
1127 )))
1128 }
1129}
1130
1131fn guid_hint(bytes: &[u8]) -> String {
1132 use std::fmt::Write as _;
1133 let mut s = String::with_capacity(bytes.len() * 2);
1134 for b in bytes {
1135 let _ = write!(s, "{b:02x}");
1136 }
1137 s
1138}
1139
1140struct VhdDecoder;
1143
1144impl ContainerOpen for VhdDecoder {
1145 fn format(&self) -> ContainerFormat {
1146 ContainerFormat::Vhd
1147 }
1148
1149 fn probe(&self, w: &SniffWindow) -> Confidence {
1150 if w.has_magic(0, b"conectix") {
1154 Confidence::Yes {
1155 how: "VHD conectix footer",
1156 }
1157 } else {
1158 Confidence::No
1159 }
1160 }
1161
1162 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1163 let len = src.len();
1164 let cursor = SourceCursor::new(src, 0, len);
1165 let reader =
1166 vhd::VhdReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1167 layer: "vhd",
1168 offset: 0,
1169 detail: e.to_string(),
1170 bytes: SmallHex::new(&[]),
1171 })?;
1172 let vsize = reader.virtual_disk_size();
1173 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1174 }
1175}
1176
1177struct Qcow2Decoder;
1180
1181impl ContainerOpen for Qcow2Decoder {
1182 fn format(&self) -> ContainerFormat {
1183 ContainerFormat::Qcow2
1184 }
1185
1186 fn probe(&self, w: &SniffWindow) -> Confidence {
1187 if w.has_magic(0, &[0x51, 0x46, 0x49, 0xfb]) {
1188 Confidence::Yes { how: "QCOW2 magic" }
1189 } else {
1190 Confidence::No
1191 }
1192 }
1193
1194 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1195 let len = src.len();
1196 let cursor = SourceCursor::new(src, 0, len);
1197 let reader =
1198 qcow2::Qcow2Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1199 layer: "qcow2",
1200 offset: 0,
1201 detail: e.to_string(),
1202 bytes: SmallHex::new(&[]),
1203 })?;
1204 let vsize = reader.virtual_disk_size();
1205 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1206 }
1207}
1208
1209struct VmdkDecoder;
1213
1214impl ContainerOpen for VmdkDecoder {
1215 fn format(&self) -> ContainerFormat {
1216 ContainerFormat::Vmdk
1217 }
1218
1219 fn probe(&self, w: &SniffWindow) -> Confidence {
1220 if w.has_magic(0, b"KDMV") {
1222 Confidence::Yes {
1223 how: "VMDK KDMV magic",
1224 }
1225 } else {
1226 Confidence::No
1227 }
1228 }
1229
1230 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1231 let len = src.len();
1232 let cursor = SourceCursor::new(src, 0, len);
1233 let boxed: Box<dyn vmdk::ReadSeek + Send> = Box::new(cursor);
1234 let reader = vmdk::VmdkReader::open(boxed).map_err(|e| VfsError::Decode {
1235 layer: "vmdk",
1236 offset: 0,
1237 detail: e.to_string(),
1238 bytes: SmallHex::new(&[]),
1239 })?;
1240 let vsize = reader.virtual_disk_size();
1241 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1242 }
1243}
1244
1245struct VhdxDecoder;
1247
1248impl ContainerOpen for VhdxDecoder {
1249 fn format(&self) -> ContainerFormat {
1250 ContainerFormat::Vhdx
1251 }
1252
1253 fn probe(&self, w: &SniffWindow) -> Confidence {
1254 if w.has_magic(0, vhdx::FILE_MAGIC) {
1255 Confidence::Yes {
1256 how: "VHDX file magic",
1257 }
1258 } else {
1259 Confidence::No
1260 }
1261 }
1262
1263 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1264 let len = src.len();
1265 let cursor = SourceCursor::new(src, 0, len);
1266 let reader =
1267 vhdx::VhdxReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1268 layer: "vhdx",
1269 offset: 0,
1270 detail: e.to_string(),
1271 bytes: SmallHex::new(&[]),
1272 })?;
1273 let vsize = reader.virtual_disk_size();
1274 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1275 }
1276}
1277
1278struct DmgDecoder;
1282
1283impl ContainerOpen for DmgDecoder {
1284 fn format(&self) -> ContainerFormat {
1285 ContainerFormat::Dmg
1286 }
1287
1288 fn probe(&self, w: &SniffWindow) -> Confidence {
1289 if w.has_magic_from_end(512, b"koly") {
1291 Confidence::Yes {
1292 how: "DMG koly trailer",
1293 }
1294 } else {
1295 Confidence::No
1296 }
1297 }
1298
1299 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1300 let len = src.len();
1301 let cursor = SourceCursor::new(src, 0, len);
1302 let reader = dmg::DmgReader::open(cursor).map_err(|e| VfsError::Decode {
1303 layer: "dmg",
1304 offset: 0,
1305 detail: e.to_string(),
1306 bytes: SmallHex::new(&[]),
1307 })?;
1308 let vsize = reader.virtual_disk_size();
1309 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1310 }
1311}
1312
1313struct Aff4Decoder;
1317
1318impl ContainerOpen for Aff4Decoder {
1319 fn format(&self) -> ContainerFormat {
1320 ContainerFormat::Aff4
1321 }
1322
1323 fn probe(&self, w: &SniffWindow) -> Confidence {
1324 if w.has_magic(0, &[0x50, 0x4b, 0x03, 0x04]) {
1325 Confidence::Maybe
1326 } else {
1327 Confidence::No
1328 }
1329 }
1330
1331 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1332 let len = src.len();
1333 let cursor = SourceCursor::new(src, 0, len);
1334 let reader =
1335 aff4::Aff4Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1336 layer: "aff4",
1337 offset: 0,
1338 detail: e.to_string(),
1339 bytes: SmallHex::new(&[]),
1340 })?;
1341 let vsize = reader.virtual_disk_size();
1342 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1343 }
1344}
1345
1346struct BitLockerProbe;
1351
1352impl EncryptionOpen for BitLockerProbe {
1353 fn scheme(&self) -> EncryptionScheme {
1354 EncryptionScheme::Bitlocker
1355 }
1356
1357 fn probe(&self, w: &SniffWindow) -> Confidence {
1358 if w.has_magic(3, b"-FVE-FS-") {
1359 Confidence::Yes {
1360 how: "BitLocker -FVE-FS- signature",
1361 }
1362 } else {
1363 Confidence::No
1364 }
1365 }
1366
1367 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1368 Ok(Box::new(bitlocker::vfs::BitlockerLayer::new(src)))
1369 }
1370}
1371
1372struct LuksProbe;
1378
1379impl EncryptionOpen for LuksProbe {
1380 fn scheme(&self) -> EncryptionScheme {
1381 EncryptionScheme::Luks2
1384 }
1385
1386 fn probe(&self, w: &SniffWindow) -> Confidence {
1387 if w.has_magic(0, &[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]) {
1389 Confidence::Yes { how: "LUKS magic" }
1390 } else {
1391 Confidence::No
1392 }
1393 }
1394
1395 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1396 Ok(Box::new(luks::vfs::LuksLayer::new(src)))
1397 }
1398}
1399
1400struct FileVaultProbe;
1405
1406impl EncryptionOpen for FileVaultProbe {
1407 fn scheme(&self) -> EncryptionScheme {
1408 EncryptionScheme::FileVault
1409 }
1410
1411 fn probe(&self, w: &SniffWindow) -> Confidence {
1412 if w.has_magic(88, b"CS") {
1415 Confidence::Yes {
1416 how: "CoreStorage CS volume header",
1417 }
1418 } else {
1419 Confidence::No
1420 }
1421 }
1422
1423 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1424 Ok(Box::new(filevault::vfs::FileVaultLayer::new(src)))
1425 }
1426}
1427
1428struct VeraCryptProbe;
1434
1435impl EncryptionOpen for VeraCryptProbe {
1436 fn scheme(&self) -> EncryptionScheme {
1437 EncryptionScheme::VeraCrypt
1438 }
1439
1440 fn probe(&self, _w: &SniffWindow) -> Confidence {
1441 Confidence::Maybe
1444 }
1445
1446 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1447 Ok(Box::new(veracrypt::vfs::VeraCryptLayer::new(src)))
1448 }
1449}
1450
1451const WALK_MAX_DEPTH: usize = 256;
1453
1454pub struct WalkEntry {
1457 pub path: Vec<Vec<u8>>,
1458 pub id: FileId,
1459 pub meta: FsMeta,
1460}
1461
1462pub fn walk(fs: &dyn FileSystem) -> VfsResult<Vec<WalkEntry>> {
1467 let mut out = Vec::new();
1468 let mut visited: HashSet<FileId> = HashSet::new();
1469 let mut stack: Vec<(Vec<Vec<u8>>, FileId, usize)> = vec![(Vec::new(), fs.root(), 0)];
1470 while let Some((prefix, dir_id, depth)) = stack.pop() {
1471 if depth > WALK_MAX_DEPTH || !visited.insert(dir_id) {
1472 continue;
1473 }
1474 for entry in fs.read_dir(dir_id)? {
1475 let entry = entry?;
1476 if matches!(entry.name.as_slice(), b"." | b"..") {
1477 continue;
1478 }
1479 let mut path = prefix.clone();
1480 path.push(entry.name);
1481 let meta = fs.meta(entry.id)?;
1482 let is_dir = matches!(meta.kind, NodeKind::Dir);
1483 out.push(WalkEntry {
1484 path: path.clone(),
1485 id: entry.id,
1486 meta,
1487 });
1488 if is_dir {
1489 stack.push((path, entry.id, depth + 1));
1490 }
1491 }
1492 }
1493 Ok(out)
1494}
1495
1496#[cfg(test)]
1497mod tests {
1498 use super::*;
1499 use forensic_vfs::ImageSource;
1500 use std::io::Write;
1501
1502 struct Mem(Vec<u8>);
1503 impl ImageSource for Mem {
1504 fn len(&self) -> u64 {
1505 self.0.len() as u64
1506 }
1507 fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
1508 let off = usize::try_from(offset).unwrap_or(usize::MAX);
1509 let Some(s) = self.0.get(off..) else {
1510 return Ok(0);
1511 };
1512 let n = s.len().min(buf.len());
1513 buf[..n].copy_from_slice(&s[..n]);
1514 Ok(n)
1515 }
1516 }
1517 fn mem(b: Vec<u8>) -> DynSource {
1518 Arc::new(Mem(b))
1519 }
1520 fn window(b: &[u8]) -> SniffWindow<'_> {
1521 SniffWindow::new(0, b)
1522 }
1523
1524 #[test]
1525 fn default_openers_registers_btrfs_ufs_udf_zfs() {
1526 let kinds: Vec<FsKind> = default_openers()
1529 .filesystems()
1530 .iter()
1531 .map(|p| p.kind())
1532 .collect();
1533 assert!(kinds.contains(&FsKind::BTRFS), "btrfs prober registered");
1534 assert!(kinds.contains(&FsKind::UFS), "ufs prober registered");
1535 assert!(kinds.contains(&FsKind::UDF), "udf prober registered");
1536 assert!(kinds.contains(&FsKind::ZFS), "zfs prober registered");
1537 assert_eq!(kinds.len(), 12, "8 original + btrfs/ufs/udf + zfs");
1538 }
1539
1540 #[test]
1541 fn default_openers_registers_the_archive_opener() {
1542 assert_eq!(
1546 default_openers().archives().len(),
1547 1,
1548 "the archive opener is registered"
1549 );
1550 }
1551
1552 #[test]
1553 fn default_openers_registers_the_four_encryption_layers() {
1554 let openers = default_openers();
1558 let layers = openers.encryption_layers();
1559 assert_eq!(layers.len(), 4, "4 FDE probers registered");
1560 let schemes: Vec<EncryptionScheme> = layers.iter().map(|p| p.scheme()).collect();
1561 assert!(
1562 schemes.contains(&EncryptionScheme::Bitlocker),
1563 "bitlocker prober registered: {schemes:?}"
1564 );
1565 assert!(
1566 schemes
1567 .iter()
1568 .any(|s| matches!(s, EncryptionScheme::Luks1 | EncryptionScheme::Luks2)),
1569 "luks prober registered: {schemes:?}"
1570 );
1571 assert!(
1572 schemes.contains(&EncryptionScheme::FileVault),
1573 "filevault prober registered: {schemes:?}"
1574 );
1575 assert!(
1576 schemes.contains(&EncryptionScheme::VeraCrypt),
1577 "veracrypt prober registered: {schemes:?}"
1578 );
1579 }
1580
1581 #[test]
1582 fn encryption_probes_detect_their_signatures() {
1583 let mut bde = vec![0u8; 64];
1585 bde[3..11].copy_from_slice(b"-FVE-FS-");
1586 assert!(matches!(
1587 BitLockerProbe.probe(&window(&bde)),
1588 Confidence::Yes { .. }
1589 ));
1590 assert_eq!(BitLockerProbe.probe(&window(&[0u8; 64])), Confidence::No);
1591 assert_eq!(BitLockerProbe.scheme(), EncryptionScheme::Bitlocker);
1592
1593 let mut luks = vec![0u8; 64];
1595 luks[0..6].copy_from_slice(&[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]);
1596 assert!(matches!(
1597 LuksProbe.probe(&window(&luks)),
1598 Confidence::Yes { .. }
1599 ));
1600 assert_eq!(LuksProbe.probe(&window(&[0u8; 64])), Confidence::No);
1601
1602 let mut fv = vec![0u8; 128];
1604 fv[88..90].copy_from_slice(b"CS");
1605 assert!(matches!(
1606 FileVaultProbe.probe(&window(&fv)),
1607 Confidence::Yes { .. }
1608 ));
1609 assert_eq!(FileVaultProbe.probe(&window(&[0u8; 128])), Confidence::No);
1610
1611 assert_eq!(VeraCryptProbe.probe(&window(&[])), Confidence::Maybe);
1613 assert_eq!(
1614 VeraCryptProbe.probe(&window(&[0xffu8; 512])),
1615 Confidence::Maybe
1616 );
1617 assert_eq!(VeraCryptProbe.scheme(), EncryptionScheme::VeraCrypt);
1618 }
1619
1620 #[test]
1621 fn default_is_new_and_probers_report_their_kinds() {
1622 let _ = Vfs::default().open_source(mem(vec![0u8; 64])).unwrap();
1623 assert_eq!(NtfsProbe.kind(), FsKind::NTFS);
1624 assert_eq!(MbrProbe.scheme(), VolumeScheme::Mbr);
1625 assert_eq!(GptProbe.scheme(), VolumeScheme::Gpt);
1626 }
1627
1628 #[test]
1629 fn probers_say_no_on_unrecognized_bytes() {
1630 let empty = window(&[]);
1631 assert_eq!(NtfsProbe.probe(&empty), Confidence::No);
1632 assert_eq!(MbrProbe.probe(&empty), Confidence::No);
1633 assert_eq!(GptProbe.probe(&empty), Confidence::No);
1634 let mut prot = vec![0u8; 512];
1636 prot[446 + 4] = 0xEE;
1637 prot[446 + 12] = 1; prot[510] = 0x55;
1639 prot[511] = 0xaa;
1640 assert_eq!(MbrProbe.probe(&window(&prot)), Confidence::No);
1641 }
1642
1643 #[test]
1644 fn mbr_probe_rejects_filesystem_boot_sectors() {
1645 let mut exfat = vec![0u8; 512];
1653 exfat[3..11].copy_from_slice(b"EXFAT ");
1654 exfat[510] = 0x55;
1655 exfat[511] = 0xaa;
1656 exfat[446 + 4] = 0x07; exfat[446 + 12] = 0x20; assert_eq!(
1660 MbrProbe.probe(&window(&exfat)),
1661 Confidence::No,
1662 "exFAT boot sector must not be misread as an MBR partition table"
1663 );
1664
1665 let mut ntfs = vec![0u8; 512];
1667 ntfs[3..11].copy_from_slice(b"NTFS ");
1668 ntfs[510] = 0x55;
1669 ntfs[511] = 0xaa;
1670 ntfs[446 + 4] = 0x07;
1671 ntfs[446 + 12] = 0x20;
1672 assert_eq!(
1673 MbrProbe.probe(&window(&ntfs)),
1674 Confidence::No,
1675 "NTFS boot sector must not be misread as an MBR partition table"
1676 );
1677
1678 let mut fat = vec![0u8; 512];
1680 fat[0] = 0xEB; fat[510] = 0x55;
1682 fat[511] = 0xaa;
1683 fat[446 + 4] = 0x07;
1684 fat[446 + 12] = 0x20;
1685 assert_eq!(
1686 MbrProbe.probe(&window(&fat)),
1687 Confidence::No,
1688 "FAT boot sector must not be misread as an MBR partition table"
1689 );
1690
1691 let mut mbr = vec![0u8; 512];
1695 mbr[446] = 0x80; mbr[446 + 4] = 0x07; mbr[446 + 8] = 1; mbr[446 + 12] = 4; mbr[510] = 0x55;
1700 mbr[511] = 0xaa;
1701 assert!(
1702 matches!(MbrProbe.probe(&window(&mbr)), Confidence::Yes { .. }),
1703 "a genuine MBR partition table must still be detected"
1704 );
1705 }
1706
1707 #[test]
1708 fn ntfs_magic_but_invalid_boot_is_a_loud_error() {
1709 let mut v = vec![0u8; 4096];
1712 v[3..11].copy_from_slice(b"NTFS ");
1713 assert!(Vfs::new().open_source(mem(v)).is_err());
1714 }
1715
1716 #[test]
1717 fn a_garbage_e01_path_fails_loud() {
1718 let mut f = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
1719 f.write_all(b"not really an EWF image").unwrap();
1720 f.flush().unwrap();
1721 assert!(Vfs::new().open(f.path()).is_err());
1722 }
1723
1724 #[test]
1725 fn gpt_parse_without_signature_errors_and_mbr_volume_index_is_bounded() {
1726 assert!(Gpt::parse(mem(vec![0u8; 1024])).is_err());
1728 let mut d = vec![0u8; 512];
1730 d[446 + 4] = 0x07;
1731 d[446 + 8] = 1; d[446 + 12] = 4; d[510] = 0x55;
1734 d[511] = 0xaa;
1735 let m = Mbr::parse(mem(d)).unwrap();
1736 assert_eq!(m.scheme(), VolumeScheme::Mbr);
1737 assert_eq!(m.volumes().len(), 1);
1738 assert!(m.open_volume(0).is_ok());
1739 assert!(m.open_volume(9).is_err());
1740 }
1741
1742 #[test]
1743 fn apm_maps_partitions_and_errors_on_non_apm() {
1744 let mut img = vec![0u8; 512];
1746 img[0..2].copy_from_slice(b"ER");
1747 img[2..4].copy_from_slice(&512u16.to_be_bytes()); let pm = |map_cnt: u32, pstart: u32, pcnt: u32, ptype: &str| {
1750 let mut e = vec![0u8; 512];
1751 e[0..2].copy_from_slice(b"PM");
1752 e[4..8].copy_from_slice(&map_cnt.to_be_bytes());
1753 e[8..12].copy_from_slice(&pstart.to_be_bytes());
1754 e[0x0c..0x10].copy_from_slice(&pcnt.to_be_bytes());
1755 e[0x30..0x30 + ptype.len()].copy_from_slice(ptype.as_bytes());
1756 e
1757 };
1758 img.extend(pm(2, 1, 63, "Apple_partition_map"));
1760 img.extend(pm(2, 4, 2, "Apple_HFS"));
1761 img.extend(vec![0u8; 4 * 512]);
1762
1763 let apm = Apm::parse(mem(img)).unwrap();
1764 assert_eq!(apm.scheme(), VolumeScheme::Apm);
1765 assert_eq!(apm.volumes().len(), 1); assert_eq!(apm.volumes()[0].start, 4 * 512); assert!(apm.open_volume(0).is_ok());
1768 assert!(apm.open_volume(9).is_err());
1769
1770 assert!(Apm::parse(mem(vec![0u8; 2048])).is_err());
1772 }
1773
1774 #[test]
1775 fn recursion_is_depth_capped_on_a_self_referential_mbr() {
1776 let mut d = vec![0u8; 1024];
1779 d[446 + 4] = 0x83; d[446 + 12] = 2;
1782 d[510] = 0x55;
1783 d[511] = 0xaa;
1784 assert!(Vfs::new().open_source(mem(d)).unwrap().is_none());
1785 }
1786
1787 #[test]
1788 fn container_decoders_report_format_and_error_on_bad_content() {
1789 assert_eq!(VhdDecoder.format(), ContainerFormat::Vhd);
1790 assert_eq!(Qcow2Decoder.format(), ContainerFormat::Qcow2);
1791 assert_eq!(VmdkDecoder.format(), ContainerFormat::Vmdk);
1792 assert_eq!(VhdxDecoder.format(), ContainerFormat::Vhdx);
1793 let mut vhd = vec![0u8; 4096];
1796 vhd[0..8].copy_from_slice(b"conectix");
1797 assert!(Vfs::new().open_source(mem(vhd)).is_err());
1798 let mut q = vec![0u8; 4096];
1799 q[0..4].copy_from_slice(&[0x51, 0x46, 0x49, 0xfb]);
1800 assert!(Vfs::new().open_source(mem(q)).is_err());
1801 let mut v = vec![0u8; 4096];
1802 v[0..4].copy_from_slice(b"KDMV");
1803 assert!(Vfs::new().open_source(mem(v)).is_err());
1804 let mut x = vec![0u8; 4096];
1805 x[0..8].copy_from_slice(vhdx::FILE_MAGIC);
1806 assert!(Vfs::new().open_source(mem(x)).is_err());
1807 }
1808
1809 #[test]
1810 fn dmg_decoder_format_probe_and_open_error() {
1811 assert_eq!(DmgDecoder.format(), ContainerFormat::Dmg);
1812 assert_eq!(
1814 DmgDecoder.probe(&SniffWindow::with_tail(0, &[], 1024, &[0u8; 512])),
1815 Confidence::No
1816 );
1817 let mut v = vec![0u8; 1024];
1822 v[512..516].copy_from_slice(b"koly");
1823 v[512 + 224..512 + 232].copy_from_slice(&u64::MAX.to_be_bytes());
1824 assert!(Vfs::new().open_source(mem(v)).is_err());
1825 }
1826
1827 #[test]
1828 fn aff4_decoder_format_probe_and_open_error() {
1829 assert_eq!(Aff4Decoder.format(), ContainerFormat::Aff4);
1830 assert_eq!(Aff4Decoder.probe(&window(&[])), Confidence::No);
1832 assert_eq!(
1833 Aff4Decoder.probe(&window(&[0x50, 0x4b, 0x03, 0x04])),
1834 Confidence::Maybe
1835 );
1836 let mut v = vec![0u8; 256];
1839 v[0..4].copy_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
1840 assert!(Vfs::new().open_source(mem(v)).is_err());
1841 }
1842
1843 #[test]
1844 fn a_valid_container_holding_no_filesystem_resolves_to_none() {
1845 let vhd = include_bytes!("../tests/data/empty.vhd").to_vec();
1848 assert!(Vfs::new().open_source(mem(vhd)).unwrap().is_none());
1849 }
1850
1851 #[test]
1852 fn ext4_probe_kind_and_open_error() {
1853 assert_eq!(Ext4Probe.kind(), FsKind::EXT);
1854 let mut v = vec![0u8; 4096];
1857 v[1080] = 0x53;
1858 v[1081] = 0xef;
1859 v[1048..1052].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1860 assert!(Vfs::new().open_source(mem(v)).is_err());
1861 }
1862
1863 #[test]
1864 fn iso9660_probe_kind_and_open_error() {
1865 assert_eq!(Iso9660Probe.kind(), FsKind::ISO9660);
1866 assert_eq!(Iso9660Probe.probe(&window(&[])), Confidence::No);
1867 let mut v = vec![0u8; 40 * 1024];
1870 v[32769..32774].copy_from_slice(b"CD001");
1871 assert_eq!(
1872 Iso9660Probe.probe(&window(&v)),
1873 Confidence::Yes {
1874 how: "ISO 9660 CD001 volume descriptor"
1875 }
1876 );
1877 assert!(Vfs::new().open_source(mem(v)).is_err());
1878 }
1879
1880 #[test]
1881 fn apfs_probe_kind_and_open_error() {
1882 assert_eq!(ApfsProbe.kind(), FsKind::APFS);
1883 assert_eq!(ApfsProbe.probe(&window(&[])), Confidence::No);
1884 let mut v = vec![0u8; 40 * 1024];
1887 v[32..36].copy_from_slice(b"NXSB");
1888 assert_eq!(
1889 ApfsProbe.probe(&window(&v)),
1890 Confidence::Yes {
1891 how: "APFS NXSB container superblock"
1892 }
1893 );
1894 assert!(Vfs::new().open_source(mem(v)).is_err());
1895 }
1896
1897 #[test]
1898 fn hfsplus_probe_kind_and_no_on_short_window() {
1899 assert_eq!(HfsPlusProbe.kind(), FsKind::HFS_PLUS);
1900 assert_eq!(HfsPlusProbe.probe(&window(&[])), Confidence::No);
1902 let mut v = vec![0u8; 40 * 1024];
1904 v[1024..1026].copy_from_slice(&[0x48, 0x58]);
1905 assert_eq!(
1906 HfsPlusProbe.probe(&window(&v)),
1907 Confidence::Yes {
1908 how: "HFS+/HFSX volume header"
1909 }
1910 );
1911 }
1912
1913 #[test]
1914 fn fat_and_exfat_magic_but_garbage_are_loud_errors() {
1915 let mut x = vec![0u8; 4096];
1917 x[3..11].copy_from_slice(b"EXFAT ");
1918 x[510] = 0x55;
1919 x[511] = 0xaa;
1920 assert!(Vfs::new().open_source(mem(x)).is_err());
1921
1922 let mut f = vec![0u8; 4096];
1925 f[0] = 0xEB;
1926 f[11..13].copy_from_slice(&512u16.to_le_bytes());
1927 f[510] = 0x55;
1928 f[511] = 0xaa;
1929 assert!(Vfs::new().open_source(mem(f)).is_err());
1930 }
1931
1932 #[test]
1933 fn guid_hint_is_lowercase_hex() {
1934 assert_eq!(guid_hint(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
1935 }
1936
1937 #[test]
1938 fn gpt_parse_skips_unused_and_reversed_entries() {
1939 let mut d = vec![0u8; 1280];
1940 d[512..520].copy_from_slice(b"EFI PART");
1941 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());
1947 d[1024 + 40..1024 + 48].copy_from_slice(&200u64.to_le_bytes());
1948 d[1152] = 0xa2;
1950 d[1152 + 32..1152 + 40].copy_from_slice(&500u64.to_le_bytes());
1951 d[1152 + 40..1152 + 48].copy_from_slice(&400u64.to_le_bytes());
1952 let g = Gpt::parse(mem(d)).unwrap();
1953 assert_eq!(g.scheme(), VolumeScheme::Gpt);
1954 assert_eq!(g.volumes().len(), 1, "reversed entry 1 is skipped");
1955 assert_eq!(g.volumes()[0].start, 100 * 512);
1956 assert!(g.open_volume(0).is_ok());
1957 assert!(g.open_volume(7).is_err());
1958
1959 assert_eq!(Mem(vec![1, 2, 3]).read_at(99, &mut [0u8; 4]).unwrap(), 0);
1961 }
1962
1963 const APFS_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/apfs_volume.bin");
1966 const EXT4_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/ext4.img");
1967
1968 fn apfs_live_xid() -> u64 {
1972 use std::io::{Read, Seek, SeekFrom};
1973 let bytes = std::fs::read(APFS_FIXTURE).unwrap();
1974 let mut c = apfs_core::ApfsContainer::open(std::io::Cursor::new(bytes)).unwrap();
1975 let bs = u64::from(c.superblock().block_size);
1976 let vaddr = c.volume_superblock_addrs().unwrap()[0];
1977 let mut r = c.into_reader();
1978 r.seek(SeekFrom::Start(vaddr * bs)).unwrap();
1979 let mut buf = vec![0u8; bs as usize];
1980 r.read_exact(&mut buf).unwrap();
1981 apfs_core::volume::ApfsVolume::parse(&buf).unwrap().xid()
1982 }
1983
1984 fn zeros_file() -> tempfile::NamedTempFile {
1985 let mut f = tempfile::NamedTempFile::new().unwrap();
1986 f.write_all(&[0u8; 4096]).unwrap();
1987 f.flush().unwrap();
1988 f
1989 }
1990
1991 #[test]
1992 fn epoch_from_create_time_round_trips_and_orders() {
1993 let t = 0x0123_4567_89ab_cdefu64;
1994 let tag = epoch_from_create_time(t);
1995 assert_eq!(&tag.0[0..24], &[0u8; 24], "high 24 bytes are zero");
1996 assert_eq!(
1997 u64::from_be_bytes(tag.0[24..32].try_into().unwrap()),
1998 t,
1999 "create_time round-trips out of the low 8 bytes"
2000 );
2001 assert!(
2002 epoch_from_create_time(t + 1).0 > tag.0,
2003 "a later create_time yields a greater tag"
2004 );
2005 }
2006
2007 #[test]
2008 fn snapshot_view_carries_epoch_and_snapshot_locator() {
2009 let base = Locator::file("/ev.dmg");
2010 let v = snapshot_view(&base, 42, "daily".to_string(), 1000);
2011 assert_eq!(v.xid, 42);
2012 assert_eq!(v.name, "daily");
2013 assert_eq!(v.epoch, epoch_from_create_time(1000));
2014 assert!(matches!(
2015 v.locator.layer,
2016 Layer::Snapshot {
2017 store: SnapshotRef::ApfsXid(42)
2018 }
2019 ));
2020 }
2021
2022 #[test]
2023 fn snapshots_on_unrecognized_source_is_empty() {
2024 let f = zeros_file();
2025 assert!(Vfs::new().snapshots(f.path()).unwrap().is_empty());
2026 }
2027
2028 #[test]
2029 fn snapshots_on_non_apfs_filesystem_is_empty() {
2030 assert!(Vfs::new()
2032 .snapshots(Path::new(EXT4_FIXTURE))
2033 .unwrap()
2034 .is_empty());
2035 }
2036
2037 #[test]
2038 fn open_snapshot_without_filesystem_is_bootstrap_error() {
2039 let f = zeros_file();
2040 assert!(matches!(
2041 Vfs::new().open_snapshot(f.path(), 1),
2042 Err(VfsError::Bootstrap { .. })
2043 ));
2044 }
2045
2046 #[test]
2047 fn open_snapshot_on_non_apfs_is_unsupported() {
2048 assert!(matches!(
2049 Vfs::new().open_snapshot(Path::new(EXT4_FIXTURE), 1),
2050 Err(VfsError::Unsupported { .. })
2051 ));
2052 }
2053
2054 #[test]
2055 fn open_snapshot_unknown_xid_is_a_loud_decode_error() {
2056 let bogus = apfs_live_xid().wrapping_add(0xDEAD_BEEF);
2059 assert!(matches!(
2060 Vfs::new().open_snapshot(Path::new(APFS_FIXTURE), bogus),
2061 Err(VfsError::Decode { .. })
2062 ));
2063 }
2064
2065 #[test]
2066 fn open_snapshot_at_live_xid_mounts_and_walks() {
2067 let ev = Vfs::new()
2068 .open_snapshot(Path::new(APFS_FIXTURE), apfs_live_xid())
2069 .expect("open live-xid snapshot");
2070 let uri = ev.root.to_uri();
2071 assert!(
2072 uri.contains("snapshot:apfs") && uri.contains("fs:apfs"),
2073 "locator names the snapshot + APFS layers: {uri}"
2074 );
2075 let fs = ev.fs.expect("snapshot mounts a filesystem");
2076 let names: Vec<String> = walk(fs.as_ref())
2077 .unwrap()
2078 .into_iter()
2079 .filter_map(|e| {
2080 e.path
2081 .last()
2082 .map(|n| String::from_utf8_lossy(n).to_string())
2083 })
2084 .collect();
2085 assert!(names.iter().any(|n| n == "plain.txt"), "walk: {names:?}");
2086 }
2087
2088 #[test]
2091 fn engine_resolution_matches_openers_open_directly() {
2092 let bytes = std::fs::read(EXT4_FIXTURE).unwrap();
2098 let len = bytes.len() as u64;
2099
2100 let via_engine = Vfs::new()
2102 .open_source(mem(bytes.clone()))
2103 .unwrap()
2104 .expect("engine resolves the ext4 fixture");
2105
2106 let base = Locator::root(Layer::Range { start: 0, len });
2108 let resolved = default_openers()
2109 .open(mem(bytes), base, 0)
2110 .unwrap()
2111 .expect("Openers::open resolves the ext4 fixture");
2112
2113 let names = |fs: &dyn FileSystem| {
2115 let mut v: Vec<Vec<Vec<u8>>> = walk(fs).unwrap().into_iter().map(|e| e.path).collect();
2116 v.sort();
2117 v
2118 };
2119 assert_eq!(
2120 names(via_engine.as_ref()),
2121 names(resolved.fs.as_ref()),
2122 "engine and Openers::open mount the same filesystem"
2123 );
2124 let resolved_uri = resolved.spec.to_uri();
2126 assert!(
2127 matches!(
2128 resolved.spec.layer,
2129 Layer::Fs {
2130 kind: FsKind::EXT,
2131 ..
2132 }
2133 ),
2134 "registry resolved spec tops with fs:ext: {resolved_uri}"
2135 );
2136 }
2137
2138 #[test]
2139 fn plain_zip_surfaces_as_a_browsable_archive() {
2140 let mut cursor = Cursor::new(Vec::new());
2147 {
2148 let opts = zip::write::SimpleFileOptions::default()
2149 .compression_method(zip::CompressionMethod::Stored);
2150 let mut zw = zip::ZipWriter::new(&mut cursor);
2151 zw.start_file("hello.txt", opts).unwrap();
2152 zw.write_all(b"hello from zip").unwrap();
2153 zw.finish().unwrap();
2154 }
2155 let bytes = cursor.into_inner();
2156
2157 let mut f = tempfile::Builder::new().suffix(".zip").tempfile().unwrap();
2158 f.write_all(&bytes).unwrap();
2159 f.flush().unwrap();
2160
2161 let ev = Vfs::new().open(f.path()).expect("open plain-zip evidence");
2162 let fs = ev
2163 .fs
2164 .expect("a plain zip must surface as a browsable archive FileSystem, not error");
2165
2166 let root = fs.root();
2167 let hello = fs
2168 .lookup(root, b"hello.txt")
2169 .expect("lookup hello.txt")
2170 .expect("hello.txt present");
2171 assert_eq!(fs.meta(hello).expect("meta hello").kind, NodeKind::File);
2172 let mut buf = vec![0u8; 64];
2173 let n = fs
2174 .read_at(hello, forensic_vfs::StreamId::Default, 0, &mut buf)
2175 .expect("read hello.txt");
2176 assert_eq!(&buf[..n], b"hello from zip");
2177 }
2178}