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 .volume_system(GptProbe)
382 .volume_system(MbrProbe)
383 .volume_system(ApmProbe)
384 .container(VhdDecoder)
385 .container(Qcow2Decoder)
386 .container(VmdkDecoder)
387 .container(VhdxDecoder)
388 .container(DmgDecoder)
389 .container(Aff4Decoder)
390 .archive(archive_core::ArchiveOpener)
391 .encryption(BitLockerProbe)
392 .encryption(LuksProbe)
393 .encryption(FileVaultProbe)
394 .encryption(VeraCryptProbe)
395}
396
397fn open_base(path: &Path) -> VfsResult<DynSource> {
401 if is_ewf(path) {
402 let reader = ewf::EwfReader::open(path).map_err(|e| VfsError::Bootstrap {
403 stage: "ewf::open",
404 detail: e.to_string(),
405 })?;
406 Ok(Arc::new(reader))
407 } else {
408 Ok(Arc::new(FileSource::open(path)?))
409 }
410}
411
412fn base_has_zip_magic(base: &DynSource) -> VfsResult<bool> {
417 let mut magic = [0u8; 4];
418 let n = base.read_at(0, &mut magic)?;
419 Ok(n >= 4 && magic == [0x50, 0x4b, 0x03, 0x04])
420}
421
422fn is_ewf(path: &Path) -> bool {
423 path.extension()
424 .and_then(|e| e.to_str())
425 .is_some_and(|e| e.eq_ignore_ascii_case("e01") || e.eq_ignore_ascii_case("ex01"))
426}
427
428struct NtfsProbe;
430
431impl FileSystemOpen for NtfsProbe {
432 fn kind(&self) -> FsKind {
433 FsKind::NTFS
434 }
435
436 fn probe(&self, w: &SniffWindow) -> Confidence {
437 if w.has_magic(3, b"NTFS ") {
439 Confidence::Yes { how: "NTFS OEM id" }
440 } else {
441 Confidence::No
442 }
443 }
444
445 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
446 let len = src.len();
447 let cursor = SourceCursor::new(src, 0, len);
448 let fs = ntfs_core::NtfsFs::open(cursor).map_err(|e| VfsError::Decode {
449 layer: "ntfs",
450 offset: 0,
451 detail: e.to_string(),
452 bytes: SmallHex::new(&[]),
453 })?;
454 Ok(Arc::new(fs))
455 }
456}
457
458struct Ext4Probe;
461
462impl FileSystemOpen for Ext4Probe {
463 fn kind(&self) -> FsKind {
464 FsKind::EXT
465 }
466
467 fn probe(&self, w: &SniffWindow) -> Confidence {
468 if w.has_magic(1080, &[0x53, 0xEF]) {
471 Confidence::Yes {
472 how: "ext2/3/4 superblock magic",
473 }
474 } else {
475 Confidence::No
476 }
477 }
478
479 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
480 let len = src.len();
481 let cursor = SourceCursor::new(src, 0, len);
482 let fs = ext4fs::Ext4Fs::open(cursor).map_err(|e| VfsError::Decode {
483 layer: "ext4",
484 offset: 0,
485 detail: e.to_string(),
486 bytes: SmallHex::new(&[]),
487 })?;
488 Ok(Arc::new(fs))
489 }
490}
491
492struct XfsProbe;
496
497impl FileSystemOpen for XfsProbe {
498 fn kind(&self) -> FsKind {
499 FsKind::XFS
500 }
501
502 fn probe(&self, w: &SniffWindow) -> Confidence {
503 xfs::vfs::xfs_probe(w)
504 }
505
506 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
507 Ok(Arc::new(xfs::vfs::XfsFs::open(&src)?))
508 }
509}
510
511struct Iso9660Probe;
515
516impl FileSystemOpen for Iso9660Probe {
517 fn kind(&self) -> FsKind {
518 FsKind::ISO9660
519 }
520
521 fn probe(&self, w: &SniffWindow) -> Confidence {
522 if w.has_magic(32769, b"CD001") {
525 Confidence::Yes {
526 how: "ISO 9660 CD001 volume descriptor",
527 }
528 } else {
529 Confidence::No
530 }
531 }
532
533 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
534 let len = src.len();
535 let cursor = SourceCursor::new(src, 0, len);
536 let fs = iso::vfs::IsoVfs::open(cursor).map_err(|e| VfsError::Decode {
537 layer: "iso9660",
538 offset: 0,
539 detail: e.to_string(),
540 bytes: SmallHex::new(&[]),
541 })?;
542 Ok(Arc::new(fs))
543 }
544}
545
546struct ApfsProbe;
549
550impl FileSystemOpen for ApfsProbe {
551 fn kind(&self) -> FsKind {
552 FsKind::APFS
553 }
554
555 fn probe(&self, w: &SniffWindow) -> Confidence {
556 if w.has_magic(32, b"NXSB") {
557 Confidence::Yes {
558 how: "APFS NXSB container superblock",
559 }
560 } else {
561 Confidence::No
562 }
563 }
564
565 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
566 let len = src.len();
567 let cursor = SourceCursor::new(src, 0, len);
568 let fs = apfs_core::vfs::ApfsFs::open(cursor).map_err(|e| VfsError::Decode {
569 layer: "apfs",
570 offset: 0,
571 detail: e.to_string(),
572 bytes: SmallHex::new(&[]),
573 })?;
574 Ok(Arc::new(fs))
575 }
576}
577
578struct HfsPlusProbe;
581
582impl FileSystemOpen for HfsPlusProbe {
583 fn kind(&self) -> FsKind {
584 FsKind::HFS_PLUS
585 }
586
587 fn probe(&self, w: &SniffWindow) -> Confidence {
588 match w.at(1024, 2) {
589 Some([0x48, 0x2B | 0x58]) => Confidence::Yes {
590 how: "HFS+/HFSX volume header",
591 },
592 _ => Confidence::No,
593 }
594 }
595
596 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
597 let len = src.len();
600 let mut volume = vec![0u8; usize::try_from(len).unwrap_or(usize::MAX)];
601 let n = src.read_at(0, &mut volume)?;
602 volume.truncate(n);
603 let fs = hfsplus::vfs::HfsFs::new(volume)?;
604 Ok(Arc::new(fs))
605 }
606}
607
608struct ExFatProbe;
612
613impl FileSystemOpen for ExFatProbe {
614 fn kind(&self) -> FsKind {
615 FsKind::EXFAT
616 }
617
618 fn probe(&self, w: &SniffWindow) -> Confidence {
619 if w.at(510, 2) == Some(&[0x55, 0xaa]) && w.has_magic(3, b"EXFAT ") {
620 Confidence::Yes {
621 how: "exFAT boot signature",
622 }
623 } else {
624 Confidence::No
625 }
626 }
627
628 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
629 let len = src.len();
630 let cursor = SourceCursor::new(src, 0, len);
631 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
632 layer: "exfat",
633 offset: 0,
634 detail: e.to_string(),
635 bytes: SmallHex::new(&[]),
636 })?;
637 Ok(Arc::new(fs))
638 }
639}
640
641struct FatProbe;
644
645impl FileSystemOpen for FatProbe {
646 fn kind(&self) -> FsKind {
647 FsKind::FAT
648 }
649
650 fn probe(&self, w: &SniffWindow) -> Confidence {
651 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
652 return Confidence::No;
653 }
654 let jump = w.at(0, 1).and_then(|s| s.first().copied());
655 let jump_ok = matches!(jump, Some(0xEB | 0xE9));
656 let bps = w
657 .at(11, 2)
658 .and_then(|b| <[u8; 2]>::try_from(b).ok())
659 .map_or(0, u16::from_le_bytes);
660 if jump_ok && bps.is_power_of_two() && (512..=4096).contains(&bps) {
661 Confidence::Yes { how: "FAT BPB" }
662 } else {
663 Confidence::No
664 }
665 }
666
667 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
668 let len = src.len();
669 let cursor = SourceCursor::new(src, 0, len);
670 let fs = fat::FatFs::open(cursor).map_err(|e| VfsError::Decode {
671 layer: "fat",
672 offset: 0,
673 detail: e.to_string(),
674 bytes: SmallHex::new(&[]),
675 })?;
676 Ok(Arc::new(fs))
677 }
678}
679
680struct UdfProbe;
687
688impl FileSystemOpen for UdfProbe {
689 fn kind(&self) -> FsKind {
690 FsKind::UDF
691 }
692
693 fn probe(&self, w: &SniffWindow) -> Confidence {
694 if udf_forensic::detect_udf(&mut Cursor::new(w.bytes())) {
695 Confidence::Yes {
696 how: "UDF NSR02/NSR03 volume recognition sequence",
697 }
698 } else {
699 Confidence::No
700 }
701 }
702
703 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
704 let len = src.len();
705 let cursor = SourceCursor::new(src, 0, len);
706 let fs = udf_forensic::vfs::UdfVfs::open(cursor)?;
707 Ok(Arc::new(fs))
708 }
709}
710
711struct UfsProbe;
718
719impl FileSystemOpen for UfsProbe {
720 fn kind(&self) -> FsKind {
721 FsKind::UFS
722 }
723
724 fn probe(&self, w: &SniffWindow) -> Confidence {
725 ufs::vfs::ufs_probe(w)
726 }
727
728 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
729 let fs = ufs::vfs::UfsFs::open(&src)?;
730 Ok(Arc::new(fs))
731 }
732}
733
734const BTRFS_MAGIC_OFFSET: usize = btrfs_core::BTRFS_SUPER_INFO_OFFSET as usize + 0x40;
738
739struct BtrfsProbe;
752
753impl FileSystemOpen for BtrfsProbe {
754 fn kind(&self) -> FsKind {
755 FsKind::BTRFS
756 }
757
758 fn probe(&self, w: &SniffWindow) -> Confidence {
759 if w.has_magic(BTRFS_MAGIC_OFFSET, &btrfs_core::BTRFS_MAGIC) {
760 Confidence::Yes {
761 how: "btrfs _BHRfS_M superblock magic",
762 }
763 } else {
764 Confidence::No
765 }
766 }
767
768 fn open(&self, src: DynSource) -> VfsResult<DynFs> {
769 let fs = btrfs_core::vfs::BtrfsFs::open(&*src)?;
770 Ok(Arc::new(fs))
771 }
772}
773
774struct MbrProbe;
777
778impl VolumeSystemOpen for MbrProbe {
779 fn scheme(&self) -> VolumeScheme {
780 VolumeScheme::Mbr
781 }
782
783 fn probe(&self, w: &SniffWindow) -> Confidence {
784 if w.at(510, 2) != Some(&[0x55, 0xaa]) {
790 return Confidence::No;
791 }
792 if w.has_magic(3, b"EXFAT ") || w.has_magic(3, b"NTFS ") {
797 return Confidence::No;
798 }
799 let jump = w.at(0, 1).and_then(|s| s.first().copied());
800 if matches!(jump, Some(0xEB | 0xE9)) {
801 return Confidence::No;
802 }
803 let data = w.bytes();
804 for i in 0..4usize {
805 let base = 446 + i * 16;
806 let boot_flag = data.get(base).copied().unwrap_or(0xFF);
809 let ptype = data.get(base + 4).copied().unwrap_or(0);
810 let size = le_u32(data, base + 12);
811 if matches!(boot_flag, 0x00 | 0x80) && ptype != 0 && ptype != 0xEE && size != 0 {
812 return Confidence::Yes {
813 how: "MBR partition table",
814 };
815 }
816 }
817 Confidence::No
818 }
819
820 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
821 Ok(Box::new(Mbr::parse(src)?))
822 }
823}
824
825struct Mbr {
827 parent: DynSource,
828 volumes: Vec<VolumeDesc>,
829}
830
831impl Mbr {
832 fn parse(src: DynSource) -> VfsResult<Self> {
833 let mut sector = [0u8; 512];
834 src.read_at(0, &mut sector)?;
835 let mut volumes = Vec::new();
836 for i in 0..4usize {
837 let base = 446 + i * 16;
838 let ptype = sector.get(base + 4).copied().unwrap_or(0);
839 let start_lba = le_u32(§or, base + 8);
840 let size = le_u32(§or, base + 12);
841 if ptype == 0 || ptype == 0xEE || size == 0 {
842 continue;
843 }
844 volumes.push(VolumeDesc {
845 index: i,
846 kind: VolumeKind::Partition,
847 start: u64::from(start_lba) * 512,
848 len: u64::from(size) * 512,
849 type_hint: Some(format!("0x{ptype:02x}")),
850 label: None,
851 });
852 }
853 Ok(Self {
854 parent: src,
855 volumes,
856 })
857 }
858}
859
860impl VolumeSystem for Mbr {
861 fn scheme(&self) -> VolumeScheme {
862 VolumeScheme::Mbr
863 }
864
865 fn volumes(&self) -> &[VolumeDesc] {
866 &self.volumes
867 }
868
869 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
870 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
871 what: "mbr volume index",
872 offset: index as u64,
873 len: 1,
874 bound: self.volumes.len() as u64,
875 })?;
876 Ok(Arc::new(SubRange::new(
877 self.parent.clone(),
878 desc.start,
879 desc.len,
880 )))
881 }
882}
883
884struct GptProbe;
888
889impl VolumeSystemOpen for GptProbe {
890 fn scheme(&self) -> VolumeScheme {
891 VolumeScheme::Gpt
892 }
893
894 fn probe(&self, w: &SniffWindow) -> Confidence {
895 if w.has_magic(512, b"EFI PART") {
897 Confidence::Yes {
898 how: "GPT EFI PART header",
899 }
900 } else {
901 Confidence::No
902 }
903 }
904
905 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
906 Ok(Box::new(Gpt::parse(src)?))
907 }
908}
909
910struct Gpt {
912 parent: DynSource,
913 volumes: Vec<VolumeDesc>,
914}
915
916impl Gpt {
917 fn parse(src: DynSource) -> VfsResult<Self> {
918 let mut header = [0u8; 512];
920 src.read_at(512, &mut header)?;
921 if header.get(0..8) != Some(b"EFI PART".as_slice()) {
922 return Err(VfsError::Decode {
923 layer: "gpt",
924 offset: 512,
925 detail: "missing EFI PART signature".to_string(),
926 bytes: SmallHex::new(header.get(0..8).unwrap_or(&[])),
927 });
928 }
929 let entries_lba = le_u64(&header, 72);
930 let num_entries = le_u32(&header, 80).min(256) as usize;
932 let entry_size = le_u32(&header, 84).clamp(128, 512) as usize;
933 let array_len = num_entries.checked_mul(entry_size).unwrap_or(0);
934 let mut arr = vec![0u8; array_len];
935 src.read_at(entries_lba.saturating_mul(512), &mut arr)?;
936
937 let mut volumes = Vec::new();
938 for i in 0..num_entries {
939 let Some(base) = i.checked_mul(entry_size) else {
940 break; };
942 let Some(entry) = arr.get(base..base.saturating_add(entry_size)) else {
943 break; };
945 let type_guid = entry.get(0..16).unwrap_or(&[]);
947 if type_guid.iter().all(|&b| b == 0) {
948 continue;
949 }
950 let first = le_u64(entry, 32);
951 let last = le_u64(entry, 40);
952 if last < first {
953 continue;
954 }
955 let sectors = last - first + 1;
956 volumes.push(VolumeDesc {
957 index: i,
958 kind: VolumeKind::Partition,
959 start: first.saturating_mul(512),
960 len: sectors.saturating_mul(512),
961 type_hint: Some(guid_hint(type_guid)),
962 label: None,
963 });
964 }
965 Ok(Self {
966 parent: src,
967 volumes,
968 })
969 }
970}
971
972impl VolumeSystem for Gpt {
973 fn scheme(&self) -> VolumeScheme {
974 VolumeScheme::Gpt
975 }
976
977 fn volumes(&self) -> &[VolumeDesc] {
978 &self.volumes
979 }
980
981 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
982 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
983 what: "gpt volume index",
984 offset: index as u64,
985 len: 1,
986 bound: self.volumes.len() as u64,
987 })?;
988 Ok(Arc::new(SubRange::new(
989 self.parent.clone(),
990 desc.start,
991 desc.len,
992 )))
993 }
994}
995
996struct ApmProbe;
999
1000impl VolumeSystemOpen for ApmProbe {
1001 fn scheme(&self) -> VolumeScheme {
1002 VolumeScheme::Apm
1003 }
1004
1005 fn probe(&self, w: &SniffWindow) -> Confidence {
1006 if w.has_magic(0, b"ER") && w.has_magic(512, b"PM") {
1009 Confidence::Yes {
1010 how: "Apple Partition Map",
1011 }
1012 } else {
1013 Confidence::No
1014 }
1015 }
1016
1017 fn open(&self, src: DynSource) -> VfsResult<Box<dyn VolumeSystem>> {
1018 Ok(Box::new(Apm::parse(src)?))
1019 }
1020}
1021
1022struct Apm {
1023 parent: DynSource,
1024 volumes: Vec<VolumeDesc>,
1025}
1026
1027const APM_MAP_CAP: u64 = 256 * 1024;
1030
1031impl Apm {
1032 fn parse(src: DynSource) -> VfsResult<Self> {
1033 let cap = src.len().clamp(1, APM_MAP_CAP) as usize;
1036 let mut head = vec![0u8; cap];
1037 let n = src.read_at(0, &mut head)?;
1038 let map = apm::parse(head.get(..n).unwrap_or(&[])).ok_or_else(|| VfsError::Decode {
1039 layer: "apm",
1040 offset: 0,
1041 detail: "not an Apple Partition Map".to_string(),
1042 bytes: SmallHex::new(head.get(..2).unwrap_or(&[])),
1043 })?;
1044
1045 let block_size = u64::from(map.block_size.max(1));
1046 let mut volumes = Vec::new();
1047 for (i, part) in map.partitions.iter().enumerate() {
1048 if part.type_name.eq_ignore_ascii_case("Apple_partition_map")
1050 || part.type_name.eq_ignore_ascii_case("Apple_Free")
1051 || part.type_name.eq_ignore_ascii_case("Apple_Void")
1052 {
1053 continue;
1054 }
1055 volumes.push(VolumeDesc {
1056 index: i,
1057 kind: VolumeKind::Partition,
1058 start: u64::from(part.start_block) * block_size,
1059 len: u64::from(part.block_count) * block_size,
1060 type_hint: Some(part.type_name.clone()),
1061 label: (!part.name.is_empty()).then(|| part.name.clone()),
1062 });
1063 }
1064
1065 Ok(Self {
1066 parent: src,
1067 volumes,
1068 })
1069 }
1070}
1071
1072impl VolumeSystem for Apm {
1073 fn scheme(&self) -> VolumeScheme {
1074 VolumeScheme::Apm
1075 }
1076
1077 fn volumes(&self) -> &[VolumeDesc] {
1078 &self.volumes
1079 }
1080
1081 fn open_volume(&self, index: usize) -> VfsResult<DynSource> {
1082 let desc = self.volumes.get(index).ok_or(VfsError::OutOfRange {
1083 what: "apm volume index",
1084 offset: index as u64,
1085 len: 1,
1086 bound: self.volumes.len() as u64,
1087 })?;
1088 Ok(Arc::new(SubRange::new(
1089 self.parent.clone(),
1090 desc.start,
1091 desc.len,
1092 )))
1093 }
1094}
1095
1096fn guid_hint(bytes: &[u8]) -> String {
1097 use std::fmt::Write as _;
1098 let mut s = String::with_capacity(bytes.len() * 2);
1099 for b in bytes {
1100 let _ = write!(s, "{b:02x}");
1101 }
1102 s
1103}
1104
1105struct VhdDecoder;
1108
1109impl ContainerOpen for VhdDecoder {
1110 fn format(&self) -> ContainerFormat {
1111 ContainerFormat::Vhd
1112 }
1113
1114 fn probe(&self, w: &SniffWindow) -> Confidence {
1115 if w.has_magic(0, b"conectix") {
1119 Confidence::Yes {
1120 how: "VHD conectix footer",
1121 }
1122 } else {
1123 Confidence::No
1124 }
1125 }
1126
1127 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1128 let len = src.len();
1129 let cursor = SourceCursor::new(src, 0, len);
1130 let reader =
1131 vhd::VhdReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1132 layer: "vhd",
1133 offset: 0,
1134 detail: e.to_string(),
1135 bytes: SmallHex::new(&[]),
1136 })?;
1137 let vsize = reader.virtual_disk_size();
1138 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1139 }
1140}
1141
1142struct Qcow2Decoder;
1145
1146impl ContainerOpen for Qcow2Decoder {
1147 fn format(&self) -> ContainerFormat {
1148 ContainerFormat::Qcow2
1149 }
1150
1151 fn probe(&self, w: &SniffWindow) -> Confidence {
1152 if w.has_magic(0, &[0x51, 0x46, 0x49, 0xfb]) {
1153 Confidence::Yes { how: "QCOW2 magic" }
1154 } else {
1155 Confidence::No
1156 }
1157 }
1158
1159 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1160 let len = src.len();
1161 let cursor = SourceCursor::new(src, 0, len);
1162 let reader =
1163 qcow2::Qcow2Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1164 layer: "qcow2",
1165 offset: 0,
1166 detail: e.to_string(),
1167 bytes: SmallHex::new(&[]),
1168 })?;
1169 let vsize = reader.virtual_disk_size();
1170 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1171 }
1172}
1173
1174struct VmdkDecoder;
1178
1179impl ContainerOpen for VmdkDecoder {
1180 fn format(&self) -> ContainerFormat {
1181 ContainerFormat::Vmdk
1182 }
1183
1184 fn probe(&self, w: &SniffWindow) -> Confidence {
1185 if w.has_magic(0, b"KDMV") {
1187 Confidence::Yes {
1188 how: "VMDK KDMV magic",
1189 }
1190 } else {
1191 Confidence::No
1192 }
1193 }
1194
1195 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1196 let len = src.len();
1197 let cursor = SourceCursor::new(src, 0, len);
1198 let boxed: Box<dyn vmdk::ReadSeek + Send> = Box::new(cursor);
1199 let reader = vmdk::VmdkReader::open(boxed).map_err(|e| VfsError::Decode {
1200 layer: "vmdk",
1201 offset: 0,
1202 detail: e.to_string(),
1203 bytes: SmallHex::new(&[]),
1204 })?;
1205 let vsize = reader.virtual_disk_size();
1206 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1207 }
1208}
1209
1210struct VhdxDecoder;
1212
1213impl ContainerOpen for VhdxDecoder {
1214 fn format(&self) -> ContainerFormat {
1215 ContainerFormat::Vhdx
1216 }
1217
1218 fn probe(&self, w: &SniffWindow) -> Confidence {
1219 if w.has_magic(0, vhdx::FILE_MAGIC) {
1220 Confidence::Yes {
1221 how: "VHDX file magic",
1222 }
1223 } else {
1224 Confidence::No
1225 }
1226 }
1227
1228 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1229 let len = src.len();
1230 let cursor = SourceCursor::new(src, 0, len);
1231 let reader =
1232 vhdx::VhdxReader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1233 layer: "vhdx",
1234 offset: 0,
1235 detail: e.to_string(),
1236 bytes: SmallHex::new(&[]),
1237 })?;
1238 let vsize = reader.virtual_disk_size();
1239 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1240 }
1241}
1242
1243struct DmgDecoder;
1247
1248impl ContainerOpen for DmgDecoder {
1249 fn format(&self) -> ContainerFormat {
1250 ContainerFormat::Dmg
1251 }
1252
1253 fn probe(&self, w: &SniffWindow) -> Confidence {
1254 if w.has_magic_from_end(512, b"koly") {
1256 Confidence::Yes {
1257 how: "DMG koly trailer",
1258 }
1259 } else {
1260 Confidence::No
1261 }
1262 }
1263
1264 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1265 let len = src.len();
1266 let cursor = SourceCursor::new(src, 0, len);
1267 let reader = dmg::DmgReader::open(cursor).map_err(|e| VfsError::Decode {
1268 layer: "dmg",
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 Aff4Decoder;
1282
1283impl ContainerOpen for Aff4Decoder {
1284 fn format(&self) -> ContainerFormat {
1285 ContainerFormat::Aff4
1286 }
1287
1288 fn probe(&self, w: &SniffWindow) -> Confidence {
1289 if w.has_magic(0, &[0x50, 0x4b, 0x03, 0x04]) {
1290 Confidence::Maybe
1291 } else {
1292 Confidence::No
1293 }
1294 }
1295
1296 fn open(&self, src: DynSource) -> VfsResult<DynSource> {
1297 let len = src.len();
1298 let cursor = SourceCursor::new(src, 0, len);
1299 let reader =
1300 aff4::Aff4Reader::open_reader(Box::new(cursor)).map_err(|e| VfsError::Decode {
1301 layer: "aff4",
1302 offset: 0,
1303 detail: e.to_string(),
1304 bytes: SmallHex::new(&[]),
1305 })?;
1306 let vsize = reader.virtual_disk_size();
1307 Ok(Arc::new(SeekPoolSource::single(reader, vsize)))
1308 }
1309}
1310
1311struct BitLockerProbe;
1316
1317impl EncryptionOpen for BitLockerProbe {
1318 fn scheme(&self) -> EncryptionScheme {
1319 EncryptionScheme::Bitlocker
1320 }
1321
1322 fn probe(&self, w: &SniffWindow) -> Confidence {
1323 if w.has_magic(3, b"-FVE-FS-") {
1324 Confidence::Yes {
1325 how: "BitLocker -FVE-FS- signature",
1326 }
1327 } else {
1328 Confidence::No
1329 }
1330 }
1331
1332 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1333 Ok(Box::new(bitlocker::vfs::BitlockerLayer::new(src)))
1334 }
1335}
1336
1337struct LuksProbe;
1343
1344impl EncryptionOpen for LuksProbe {
1345 fn scheme(&self) -> EncryptionScheme {
1346 EncryptionScheme::Luks2
1349 }
1350
1351 fn probe(&self, w: &SniffWindow) -> Confidence {
1352 if w.has_magic(0, &[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]) {
1354 Confidence::Yes { how: "LUKS magic" }
1355 } else {
1356 Confidence::No
1357 }
1358 }
1359
1360 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1361 Ok(Box::new(luks::vfs::LuksLayer::new(src)))
1362 }
1363}
1364
1365struct FileVaultProbe;
1370
1371impl EncryptionOpen for FileVaultProbe {
1372 fn scheme(&self) -> EncryptionScheme {
1373 EncryptionScheme::FileVault
1374 }
1375
1376 fn probe(&self, w: &SniffWindow) -> Confidence {
1377 if w.has_magic(88, b"CS") {
1380 Confidence::Yes {
1381 how: "CoreStorage CS volume header",
1382 }
1383 } else {
1384 Confidence::No
1385 }
1386 }
1387
1388 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1389 Ok(Box::new(filevault::vfs::FileVaultLayer::new(src)))
1390 }
1391}
1392
1393struct VeraCryptProbe;
1399
1400impl EncryptionOpen for VeraCryptProbe {
1401 fn scheme(&self) -> EncryptionScheme {
1402 EncryptionScheme::VeraCrypt
1403 }
1404
1405 fn probe(&self, _w: &SniffWindow) -> Confidence {
1406 Confidence::Maybe
1409 }
1410
1411 fn open(&self, src: DynSource) -> VfsResult<Box<dyn EncryptionLayer>> {
1412 Ok(Box::new(veracrypt::vfs::VeraCryptLayer::new(src)))
1413 }
1414}
1415
1416const WALK_MAX_DEPTH: usize = 256;
1418
1419pub struct WalkEntry {
1422 pub path: Vec<Vec<u8>>,
1423 pub id: FileId,
1424 pub meta: FsMeta,
1425}
1426
1427pub fn walk(fs: &dyn FileSystem) -> VfsResult<Vec<WalkEntry>> {
1432 let mut out = Vec::new();
1433 let mut visited: HashSet<FileId> = HashSet::new();
1434 let mut stack: Vec<(Vec<Vec<u8>>, FileId, usize)> = vec![(Vec::new(), fs.root(), 0)];
1435 while let Some((prefix, dir_id, depth)) = stack.pop() {
1436 if depth > WALK_MAX_DEPTH || !visited.insert(dir_id) {
1437 continue;
1438 }
1439 for entry in fs.read_dir(dir_id)? {
1440 let entry = entry?;
1441 if matches!(entry.name.as_slice(), b"." | b"..") {
1442 continue;
1443 }
1444 let mut path = prefix.clone();
1445 path.push(entry.name);
1446 let meta = fs.meta(entry.id)?;
1447 let is_dir = matches!(meta.kind, NodeKind::Dir);
1448 out.push(WalkEntry {
1449 path: path.clone(),
1450 id: entry.id,
1451 meta,
1452 });
1453 if is_dir {
1454 stack.push((path, entry.id, depth + 1));
1455 }
1456 }
1457 }
1458 Ok(out)
1459}
1460
1461#[cfg(test)]
1462mod tests {
1463 use super::*;
1464 use forensic_vfs::ImageSource;
1465 use std::io::Write;
1466
1467 struct Mem(Vec<u8>);
1468 impl ImageSource for Mem {
1469 fn len(&self) -> u64 {
1470 self.0.len() as u64
1471 }
1472 fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
1473 let off = usize::try_from(offset).unwrap_or(usize::MAX);
1474 let Some(s) = self.0.get(off..) else {
1475 return Ok(0);
1476 };
1477 let n = s.len().min(buf.len());
1478 buf[..n].copy_from_slice(&s[..n]);
1479 Ok(n)
1480 }
1481 }
1482 fn mem(b: Vec<u8>) -> DynSource {
1483 Arc::new(Mem(b))
1484 }
1485 fn window(b: &[u8]) -> SniffWindow<'_> {
1486 SniffWindow::new(0, b)
1487 }
1488
1489 #[test]
1490 fn default_openers_registers_btrfs_ufs_udf() {
1491 let kinds: Vec<FsKind> = default_openers()
1494 .filesystems()
1495 .iter()
1496 .map(|p| p.kind())
1497 .collect();
1498 assert!(kinds.contains(&FsKind::BTRFS), "btrfs prober registered");
1499 assert!(kinds.contains(&FsKind::UFS), "ufs prober registered");
1500 assert!(kinds.contains(&FsKind::UDF), "udf prober registered");
1501 assert_eq!(kinds.len(), 11, "8 existing + 3 new filesystem probers");
1502 }
1503
1504 #[test]
1505 fn default_openers_registers_the_archive_opener() {
1506 assert_eq!(
1510 default_openers().archives().len(),
1511 1,
1512 "the archive opener is registered"
1513 );
1514 }
1515
1516 #[test]
1517 fn default_openers_registers_the_four_encryption_layers() {
1518 let openers = default_openers();
1522 let layers = openers.encryption_layers();
1523 assert_eq!(layers.len(), 4, "4 FDE probers registered");
1524 let schemes: Vec<EncryptionScheme> = layers.iter().map(|p| p.scheme()).collect();
1525 assert!(
1526 schemes.contains(&EncryptionScheme::Bitlocker),
1527 "bitlocker prober registered: {schemes:?}"
1528 );
1529 assert!(
1530 schemes
1531 .iter()
1532 .any(|s| matches!(s, EncryptionScheme::Luks1 | EncryptionScheme::Luks2)),
1533 "luks prober registered: {schemes:?}"
1534 );
1535 assert!(
1536 schemes.contains(&EncryptionScheme::FileVault),
1537 "filevault prober registered: {schemes:?}"
1538 );
1539 assert!(
1540 schemes.contains(&EncryptionScheme::VeraCrypt),
1541 "veracrypt prober registered: {schemes:?}"
1542 );
1543 }
1544
1545 #[test]
1546 fn encryption_probes_detect_their_signatures() {
1547 let mut bde = vec![0u8; 64];
1549 bde[3..11].copy_from_slice(b"-FVE-FS-");
1550 assert!(matches!(
1551 BitLockerProbe.probe(&window(&bde)),
1552 Confidence::Yes { .. }
1553 ));
1554 assert_eq!(BitLockerProbe.probe(&window(&[0u8; 64])), Confidence::No);
1555 assert_eq!(BitLockerProbe.scheme(), EncryptionScheme::Bitlocker);
1556
1557 let mut luks = vec![0u8; 64];
1559 luks[0..6].copy_from_slice(&[0x4c, 0x55, 0x4b, 0x53, 0xba, 0xbe]);
1560 assert!(matches!(
1561 LuksProbe.probe(&window(&luks)),
1562 Confidence::Yes { .. }
1563 ));
1564 assert_eq!(LuksProbe.probe(&window(&[0u8; 64])), Confidence::No);
1565
1566 let mut fv = vec![0u8; 128];
1568 fv[88..90].copy_from_slice(b"CS");
1569 assert!(matches!(
1570 FileVaultProbe.probe(&window(&fv)),
1571 Confidence::Yes { .. }
1572 ));
1573 assert_eq!(FileVaultProbe.probe(&window(&[0u8; 128])), Confidence::No);
1574
1575 assert_eq!(VeraCryptProbe.probe(&window(&[])), Confidence::Maybe);
1577 assert_eq!(
1578 VeraCryptProbe.probe(&window(&[0xffu8; 512])),
1579 Confidence::Maybe
1580 );
1581 assert_eq!(VeraCryptProbe.scheme(), EncryptionScheme::VeraCrypt);
1582 }
1583
1584 #[test]
1585 fn default_is_new_and_probers_report_their_kinds() {
1586 let _ = Vfs::default().open_source(mem(vec![0u8; 64])).unwrap();
1587 assert_eq!(NtfsProbe.kind(), FsKind::NTFS);
1588 assert_eq!(MbrProbe.scheme(), VolumeScheme::Mbr);
1589 assert_eq!(GptProbe.scheme(), VolumeScheme::Gpt);
1590 }
1591
1592 #[test]
1593 fn probers_say_no_on_unrecognized_bytes() {
1594 let empty = window(&[]);
1595 assert_eq!(NtfsProbe.probe(&empty), Confidence::No);
1596 assert_eq!(MbrProbe.probe(&empty), Confidence::No);
1597 assert_eq!(GptProbe.probe(&empty), Confidence::No);
1598 let mut prot = vec![0u8; 512];
1600 prot[446 + 4] = 0xEE;
1601 prot[446 + 12] = 1; prot[510] = 0x55;
1603 prot[511] = 0xaa;
1604 assert_eq!(MbrProbe.probe(&window(&prot)), Confidence::No);
1605 }
1606
1607 #[test]
1608 fn mbr_probe_rejects_filesystem_boot_sectors() {
1609 let mut exfat = vec![0u8; 512];
1617 exfat[3..11].copy_from_slice(b"EXFAT ");
1618 exfat[510] = 0x55;
1619 exfat[511] = 0xaa;
1620 exfat[446 + 4] = 0x07; exfat[446 + 12] = 0x20; assert_eq!(
1624 MbrProbe.probe(&window(&exfat)),
1625 Confidence::No,
1626 "exFAT boot sector must not be misread as an MBR partition table"
1627 );
1628
1629 let mut ntfs = vec![0u8; 512];
1631 ntfs[3..11].copy_from_slice(b"NTFS ");
1632 ntfs[510] = 0x55;
1633 ntfs[511] = 0xaa;
1634 ntfs[446 + 4] = 0x07;
1635 ntfs[446 + 12] = 0x20;
1636 assert_eq!(
1637 MbrProbe.probe(&window(&ntfs)),
1638 Confidence::No,
1639 "NTFS boot sector must not be misread as an MBR partition table"
1640 );
1641
1642 let mut fat = vec![0u8; 512];
1644 fat[0] = 0xEB; fat[510] = 0x55;
1646 fat[511] = 0xaa;
1647 fat[446 + 4] = 0x07;
1648 fat[446 + 12] = 0x20;
1649 assert_eq!(
1650 MbrProbe.probe(&window(&fat)),
1651 Confidence::No,
1652 "FAT boot sector must not be misread as an MBR partition table"
1653 );
1654
1655 let mut mbr = vec![0u8; 512];
1659 mbr[446] = 0x80; mbr[446 + 4] = 0x07; mbr[446 + 8] = 1; mbr[446 + 12] = 4; mbr[510] = 0x55;
1664 mbr[511] = 0xaa;
1665 assert!(
1666 matches!(MbrProbe.probe(&window(&mbr)), Confidence::Yes { .. }),
1667 "a genuine MBR partition table must still be detected"
1668 );
1669 }
1670
1671 #[test]
1672 fn ntfs_magic_but_invalid_boot_is_a_loud_error() {
1673 let mut v = vec![0u8; 4096];
1676 v[3..11].copy_from_slice(b"NTFS ");
1677 assert!(Vfs::new().open_source(mem(v)).is_err());
1678 }
1679
1680 #[test]
1681 fn a_garbage_e01_path_fails_loud() {
1682 let mut f = tempfile::Builder::new().suffix(".E01").tempfile().unwrap();
1683 f.write_all(b"not really an EWF image").unwrap();
1684 f.flush().unwrap();
1685 assert!(Vfs::new().open(f.path()).is_err());
1686 }
1687
1688 #[test]
1689 fn gpt_parse_without_signature_errors_and_mbr_volume_index_is_bounded() {
1690 assert!(Gpt::parse(mem(vec![0u8; 1024])).is_err());
1692 let mut d = vec![0u8; 512];
1694 d[446 + 4] = 0x07;
1695 d[446 + 8] = 1; d[446 + 12] = 4; d[510] = 0x55;
1698 d[511] = 0xaa;
1699 let m = Mbr::parse(mem(d)).unwrap();
1700 assert_eq!(m.scheme(), VolumeScheme::Mbr);
1701 assert_eq!(m.volumes().len(), 1);
1702 assert!(m.open_volume(0).is_ok());
1703 assert!(m.open_volume(9).is_err());
1704 }
1705
1706 #[test]
1707 fn apm_maps_partitions_and_errors_on_non_apm() {
1708 let mut img = vec![0u8; 512];
1710 img[0..2].copy_from_slice(b"ER");
1711 img[2..4].copy_from_slice(&512u16.to_be_bytes()); let pm = |map_cnt: u32, pstart: u32, pcnt: u32, ptype: &str| {
1714 let mut e = vec![0u8; 512];
1715 e[0..2].copy_from_slice(b"PM");
1716 e[4..8].copy_from_slice(&map_cnt.to_be_bytes());
1717 e[8..12].copy_from_slice(&pstart.to_be_bytes());
1718 e[0x0c..0x10].copy_from_slice(&pcnt.to_be_bytes());
1719 e[0x30..0x30 + ptype.len()].copy_from_slice(ptype.as_bytes());
1720 e
1721 };
1722 img.extend(pm(2, 1, 63, "Apple_partition_map"));
1724 img.extend(pm(2, 4, 2, "Apple_HFS"));
1725 img.extend(vec![0u8; 4 * 512]);
1726
1727 let apm = Apm::parse(mem(img)).unwrap();
1728 assert_eq!(apm.scheme(), VolumeScheme::Apm);
1729 assert_eq!(apm.volumes().len(), 1); assert_eq!(apm.volumes()[0].start, 4 * 512); assert!(apm.open_volume(0).is_ok());
1732 assert!(apm.open_volume(9).is_err());
1733
1734 assert!(Apm::parse(mem(vec![0u8; 2048])).is_err());
1736 }
1737
1738 #[test]
1739 fn recursion_is_depth_capped_on_a_self_referential_mbr() {
1740 let mut d = vec![0u8; 1024];
1743 d[446 + 4] = 0x83; d[446 + 12] = 2;
1746 d[510] = 0x55;
1747 d[511] = 0xaa;
1748 assert!(Vfs::new().open_source(mem(d)).unwrap().is_none());
1749 }
1750
1751 #[test]
1752 fn container_decoders_report_format_and_error_on_bad_content() {
1753 assert_eq!(VhdDecoder.format(), ContainerFormat::Vhd);
1754 assert_eq!(Qcow2Decoder.format(), ContainerFormat::Qcow2);
1755 assert_eq!(VmdkDecoder.format(), ContainerFormat::Vmdk);
1756 assert_eq!(VhdxDecoder.format(), ContainerFormat::Vhdx);
1757 let mut vhd = vec![0u8; 4096];
1760 vhd[0..8].copy_from_slice(b"conectix");
1761 assert!(Vfs::new().open_source(mem(vhd)).is_err());
1762 let mut q = vec![0u8; 4096];
1763 q[0..4].copy_from_slice(&[0x51, 0x46, 0x49, 0xfb]);
1764 assert!(Vfs::new().open_source(mem(q)).is_err());
1765 let mut v = vec![0u8; 4096];
1766 v[0..4].copy_from_slice(b"KDMV");
1767 assert!(Vfs::new().open_source(mem(v)).is_err());
1768 let mut x = vec![0u8; 4096];
1769 x[0..8].copy_from_slice(vhdx::FILE_MAGIC);
1770 assert!(Vfs::new().open_source(mem(x)).is_err());
1771 }
1772
1773 #[test]
1774 fn dmg_decoder_format_probe_and_open_error() {
1775 assert_eq!(DmgDecoder.format(), ContainerFormat::Dmg);
1776 assert_eq!(
1778 DmgDecoder.probe(&SniffWindow::with_tail(0, &[], 1024, &[0u8; 512])),
1779 Confidence::No
1780 );
1781 let mut v = vec![0u8; 1024];
1786 v[512..516].copy_from_slice(b"koly");
1787 v[512 + 224..512 + 232].copy_from_slice(&u64::MAX.to_be_bytes());
1788 assert!(Vfs::new().open_source(mem(v)).is_err());
1789 }
1790
1791 #[test]
1792 fn aff4_decoder_format_probe_and_open_error() {
1793 assert_eq!(Aff4Decoder.format(), ContainerFormat::Aff4);
1794 assert_eq!(Aff4Decoder.probe(&window(&[])), Confidence::No);
1796 assert_eq!(
1797 Aff4Decoder.probe(&window(&[0x50, 0x4b, 0x03, 0x04])),
1798 Confidence::Maybe
1799 );
1800 let mut v = vec![0u8; 256];
1803 v[0..4].copy_from_slice(&[0x50, 0x4b, 0x03, 0x04]);
1804 assert!(Vfs::new().open_source(mem(v)).is_err());
1805 }
1806
1807 #[test]
1808 fn a_valid_container_holding_no_filesystem_resolves_to_none() {
1809 let vhd = include_bytes!("../tests/data/empty.vhd").to_vec();
1812 assert!(Vfs::new().open_source(mem(vhd)).unwrap().is_none());
1813 }
1814
1815 #[test]
1816 fn ext4_probe_kind_and_open_error() {
1817 assert_eq!(Ext4Probe.kind(), FsKind::EXT);
1818 let mut v = vec![0u8; 4096];
1821 v[1080] = 0x53;
1822 v[1081] = 0xef;
1823 v[1048..1052].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1824 assert!(Vfs::new().open_source(mem(v)).is_err());
1825 }
1826
1827 #[test]
1828 fn iso9660_probe_kind_and_open_error() {
1829 assert_eq!(Iso9660Probe.kind(), FsKind::ISO9660);
1830 assert_eq!(Iso9660Probe.probe(&window(&[])), Confidence::No);
1831 let mut v = vec![0u8; 40 * 1024];
1834 v[32769..32774].copy_from_slice(b"CD001");
1835 assert_eq!(
1836 Iso9660Probe.probe(&window(&v)),
1837 Confidence::Yes {
1838 how: "ISO 9660 CD001 volume descriptor"
1839 }
1840 );
1841 assert!(Vfs::new().open_source(mem(v)).is_err());
1842 }
1843
1844 #[test]
1845 fn apfs_probe_kind_and_open_error() {
1846 assert_eq!(ApfsProbe.kind(), FsKind::APFS);
1847 assert_eq!(ApfsProbe.probe(&window(&[])), Confidence::No);
1848 let mut v = vec![0u8; 40 * 1024];
1851 v[32..36].copy_from_slice(b"NXSB");
1852 assert_eq!(
1853 ApfsProbe.probe(&window(&v)),
1854 Confidence::Yes {
1855 how: "APFS NXSB container superblock"
1856 }
1857 );
1858 assert!(Vfs::new().open_source(mem(v)).is_err());
1859 }
1860
1861 #[test]
1862 fn hfsplus_probe_kind_and_no_on_short_window() {
1863 assert_eq!(HfsPlusProbe.kind(), FsKind::HFS_PLUS);
1864 assert_eq!(HfsPlusProbe.probe(&window(&[])), Confidence::No);
1866 let mut v = vec![0u8; 40 * 1024];
1868 v[1024..1026].copy_from_slice(&[0x48, 0x58]);
1869 assert_eq!(
1870 HfsPlusProbe.probe(&window(&v)),
1871 Confidence::Yes {
1872 how: "HFS+/HFSX volume header"
1873 }
1874 );
1875 }
1876
1877 #[test]
1878 fn fat_and_exfat_magic_but_garbage_are_loud_errors() {
1879 let mut x = vec![0u8; 4096];
1881 x[3..11].copy_from_slice(b"EXFAT ");
1882 x[510] = 0x55;
1883 x[511] = 0xaa;
1884 assert!(Vfs::new().open_source(mem(x)).is_err());
1885
1886 let mut f = vec![0u8; 4096];
1889 f[0] = 0xEB;
1890 f[11..13].copy_from_slice(&512u16.to_le_bytes());
1891 f[510] = 0x55;
1892 f[511] = 0xaa;
1893 assert!(Vfs::new().open_source(mem(f)).is_err());
1894 }
1895
1896 #[test]
1897 fn guid_hint_is_lowercase_hex() {
1898 assert_eq!(guid_hint(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
1899 }
1900
1901 #[test]
1902 fn gpt_parse_skips_unused_and_reversed_entries() {
1903 let mut d = vec![0u8; 1280];
1904 d[512..520].copy_from_slice(b"EFI PART");
1905 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());
1911 d[1024 + 40..1024 + 48].copy_from_slice(&200u64.to_le_bytes());
1912 d[1152] = 0xa2;
1914 d[1152 + 32..1152 + 40].copy_from_slice(&500u64.to_le_bytes());
1915 d[1152 + 40..1152 + 48].copy_from_slice(&400u64.to_le_bytes());
1916 let g = Gpt::parse(mem(d)).unwrap();
1917 assert_eq!(g.scheme(), VolumeScheme::Gpt);
1918 assert_eq!(g.volumes().len(), 1, "reversed entry 1 is skipped");
1919 assert_eq!(g.volumes()[0].start, 100 * 512);
1920 assert!(g.open_volume(0).is_ok());
1921 assert!(g.open_volume(7).is_err());
1922
1923 assert_eq!(Mem(vec![1, 2, 3]).read_at(99, &mut [0u8; 4]).unwrap(), 0);
1925 }
1926
1927 const APFS_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/apfs_volume.bin");
1930 const EXT4_FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/ext4.img");
1931
1932 fn apfs_live_xid() -> u64 {
1936 use std::io::{Read, Seek, SeekFrom};
1937 let bytes = std::fs::read(APFS_FIXTURE).unwrap();
1938 let mut c = apfs_core::ApfsContainer::open(std::io::Cursor::new(bytes)).unwrap();
1939 let bs = u64::from(c.superblock().block_size);
1940 let vaddr = c.volume_superblock_addrs().unwrap()[0];
1941 let mut r = c.into_reader();
1942 r.seek(SeekFrom::Start(vaddr * bs)).unwrap();
1943 let mut buf = vec![0u8; bs as usize];
1944 r.read_exact(&mut buf).unwrap();
1945 apfs_core::volume::ApfsVolume::parse(&buf).unwrap().xid()
1946 }
1947
1948 fn zeros_file() -> tempfile::NamedTempFile {
1949 let mut f = tempfile::NamedTempFile::new().unwrap();
1950 f.write_all(&[0u8; 4096]).unwrap();
1951 f.flush().unwrap();
1952 f
1953 }
1954
1955 #[test]
1956 fn epoch_from_create_time_round_trips_and_orders() {
1957 let t = 0x0123_4567_89ab_cdefu64;
1958 let tag = epoch_from_create_time(t);
1959 assert_eq!(&tag.0[0..24], &[0u8; 24], "high 24 bytes are zero");
1960 assert_eq!(
1961 u64::from_be_bytes(tag.0[24..32].try_into().unwrap()),
1962 t,
1963 "create_time round-trips out of the low 8 bytes"
1964 );
1965 assert!(
1966 epoch_from_create_time(t + 1).0 > tag.0,
1967 "a later create_time yields a greater tag"
1968 );
1969 }
1970
1971 #[test]
1972 fn snapshot_view_carries_epoch_and_snapshot_locator() {
1973 let base = Locator::file("/ev.dmg");
1974 let v = snapshot_view(&base, 42, "daily".to_string(), 1000);
1975 assert_eq!(v.xid, 42);
1976 assert_eq!(v.name, "daily");
1977 assert_eq!(v.epoch, epoch_from_create_time(1000));
1978 assert!(matches!(
1979 v.locator.layer,
1980 Layer::Snapshot {
1981 store: SnapshotRef::ApfsXid(42)
1982 }
1983 ));
1984 }
1985
1986 #[test]
1987 fn snapshots_on_unrecognized_source_is_empty() {
1988 let f = zeros_file();
1989 assert!(Vfs::new().snapshots(f.path()).unwrap().is_empty());
1990 }
1991
1992 #[test]
1993 fn snapshots_on_non_apfs_filesystem_is_empty() {
1994 assert!(Vfs::new()
1996 .snapshots(Path::new(EXT4_FIXTURE))
1997 .unwrap()
1998 .is_empty());
1999 }
2000
2001 #[test]
2002 fn open_snapshot_without_filesystem_is_bootstrap_error() {
2003 let f = zeros_file();
2004 assert!(matches!(
2005 Vfs::new().open_snapshot(f.path(), 1),
2006 Err(VfsError::Bootstrap { .. })
2007 ));
2008 }
2009
2010 #[test]
2011 fn open_snapshot_on_non_apfs_is_unsupported() {
2012 assert!(matches!(
2013 Vfs::new().open_snapshot(Path::new(EXT4_FIXTURE), 1),
2014 Err(VfsError::Unsupported { .. })
2015 ));
2016 }
2017
2018 #[test]
2019 fn open_snapshot_unknown_xid_is_a_loud_decode_error() {
2020 let bogus = apfs_live_xid().wrapping_add(0xDEAD_BEEF);
2023 assert!(matches!(
2024 Vfs::new().open_snapshot(Path::new(APFS_FIXTURE), bogus),
2025 Err(VfsError::Decode { .. })
2026 ));
2027 }
2028
2029 #[test]
2030 fn open_snapshot_at_live_xid_mounts_and_walks() {
2031 let ev = Vfs::new()
2032 .open_snapshot(Path::new(APFS_FIXTURE), apfs_live_xid())
2033 .expect("open live-xid snapshot");
2034 let uri = ev.root.to_uri();
2035 assert!(
2036 uri.contains("snapshot:apfs") && uri.contains("fs:apfs"),
2037 "locator names the snapshot + APFS layers: {uri}"
2038 );
2039 let fs = ev.fs.expect("snapshot mounts a filesystem");
2040 let names: Vec<String> = walk(fs.as_ref())
2041 .unwrap()
2042 .into_iter()
2043 .filter_map(|e| {
2044 e.path
2045 .last()
2046 .map(|n| String::from_utf8_lossy(n).to_string())
2047 })
2048 .collect();
2049 assert!(names.iter().any(|n| n == "plain.txt"), "walk: {names:?}");
2050 }
2051
2052 #[test]
2055 fn engine_resolution_matches_openers_open_directly() {
2056 let bytes = std::fs::read(EXT4_FIXTURE).unwrap();
2062 let len = bytes.len() as u64;
2063
2064 let via_engine = Vfs::new()
2066 .open_source(mem(bytes.clone()))
2067 .unwrap()
2068 .expect("engine resolves the ext4 fixture");
2069
2070 let base = Locator::root(Layer::Range { start: 0, len });
2072 let resolved = default_openers()
2073 .open(mem(bytes), base, 0)
2074 .unwrap()
2075 .expect("Openers::open resolves the ext4 fixture");
2076
2077 let names = |fs: &dyn FileSystem| {
2079 let mut v: Vec<Vec<Vec<u8>>> = walk(fs).unwrap().into_iter().map(|e| e.path).collect();
2080 v.sort();
2081 v
2082 };
2083 assert_eq!(
2084 names(via_engine.as_ref()),
2085 names(resolved.fs.as_ref()),
2086 "engine and Openers::open mount the same filesystem"
2087 );
2088 let resolved_uri = resolved.spec.to_uri();
2090 assert!(
2091 matches!(
2092 resolved.spec.layer,
2093 Layer::Fs {
2094 kind: FsKind::EXT,
2095 ..
2096 }
2097 ),
2098 "registry resolved spec tops with fs:ext: {resolved_uri}"
2099 );
2100 }
2101
2102 #[test]
2103 fn plain_zip_surfaces_as_a_browsable_archive() {
2104 let mut cursor = Cursor::new(Vec::new());
2111 {
2112 let opts = zip::write::SimpleFileOptions::default()
2113 .compression_method(zip::CompressionMethod::Stored);
2114 let mut zw = zip::ZipWriter::new(&mut cursor);
2115 zw.start_file("hello.txt", opts).unwrap();
2116 zw.write_all(b"hello from zip").unwrap();
2117 zw.finish().unwrap();
2118 }
2119 let bytes = cursor.into_inner();
2120
2121 let mut f = tempfile::Builder::new().suffix(".zip").tempfile().unwrap();
2122 f.write_all(&bytes).unwrap();
2123 f.flush().unwrap();
2124
2125 let ev = Vfs::new().open(f.path()).expect("open plain-zip evidence");
2126 let fs = ev
2127 .fs
2128 .expect("a plain zip must surface as a browsable archive FileSystem, not error");
2129
2130 let root = fs.root();
2131 let hello = fs
2132 .lookup(root, b"hello.txt")
2133 .expect("lookup hello.txt")
2134 .expect("hello.txt present");
2135 assert_eq!(fs.meta(hello).expect("meta hello").kind, NodeKind::File);
2136 let mut buf = vec![0u8; 64];
2137 let n = fs
2138 .read_at(hello, forensic_vfs::StreamId::Default, 0, &mut buf)
2139 .expect("read hello.txt");
2140 assert_eq!(&buf[..n], b"hello from zip");
2141 }
2142}