forensicnomicon_core/filesystems.rs
1//! Filesystem superblock / boot-sector magic signatures.
2//!
3//! Single source of truth mapping a `(offset, magic-bytes)` pair to a filesystem
4//! name, for forensic tools that fingerprint a partition's content. Each entry
5//! cites the authoritative on-disk-format reference for its offset and magic.
6//!
7//! Offsets are absolute byte offsets from the start of the partition/volume.
8//!
9//! General references:
10//! - Wikipedia, "List of file signatures": <https://en.wikipedia.org/wiki/List_of_file_signatures>
11//! - The `file`/libmagic database (`magic/Magdir/filesystems`):
12//! <https://github.com/file/file/blob/master/magic/Magdir/filesystems>
13
14/// One filesystem magic signature.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize))]
17pub struct FsSignature {
18 /// Filesystem name.
19 pub name: &'static str,
20 /// Absolute byte offset of the magic within the volume.
21 pub offset: usize,
22 /// The magic bytes expected at `offset`.
23 pub magic: &'static [u8],
24}
25
26/// Well-known filesystem magic signatures.
27///
28/// Per-entry sources:
29/// - **ext2/3/4** — superblock magic `0xEF53` (LE `53 EF`) at offset `0x438`:
30/// Linux kernel `fs/ext4/ext4.h` `EXT4_SUPER_MAGIC`; Wikipedia "Ext4".
31/// - **NTFS** — OEM ID `"NTFS "` at offset `3`: Microsoft NTFS docs; Wikipedia "NTFS".
32/// - **exFAT** — OEM name `"EXFAT "` at offset `3`: Microsoft exFAT specification.
33/// - **XFS** — superblock magic `"XFSB"` at offset `0`: XFS Algorithms & Data
34/// Structures (SGI/Red Hat).
35/// - **LUKS1** — magic `"LUKS\xba\xbe"` at offset `0`: LUKS On-Disk Format Spec.
36/// - **FAT32** — `BS_FilSysType` `"FAT32 "` at offset `0x52`: Microsoft FAT
37/// Specification (`fatgen103`).
38/// - **FAT16/FAT12** — `BS_FilSysType` `"FAT16 "` / `"FAT12 "` at offset `0x36`:
39/// Microsoft FAT Specification.
40/// - **Linux swap** — `"SWAPSPACE2"` at offset `0xFF6` (page end − 10): `mkswap(8)` / kernel.
41/// - **ISO 9660** — `"CD001"` at offset `0x8001` (sector 16): ECMA-119.
42/// - **HFS+** — signature `"H+"` at offset `0x400`: Apple Technical Note TN1150.
43/// - **APFS** — container superblock `nx_magic` `"NXSB"` at offset `32` (after the
44/// 32-byte `obj_phys_t` header): Apple File System Reference; util-linux
45/// `libblkid/src/superblocks/apfs.c` (`.magic = "NXSB", .sboff = 32`).
46/// - **Btrfs** — superblock magic `"_BHRfS_M"` at offset `65600` (`0x10040`; the
47/// superblock is at 64 KiB and `magic` is at `+0x40`): util-linux
48/// `libblkid/src/superblocks/btrfs.c` (`.kboff = 64, .sboff = 0x40`).
49/// - **LVM2 PV** — label `"LABELONE"` at the start of the PV label sector, which
50/// is sector 0 **or** sector 1 (offset `0` or `512`; the default is sector 1):
51/// util-linux `libblkid/src/superblocks/lvm.c` (checks `buf` and `buf + 512`);
52/// libvslvm "Logical Volume Manager (LVM) format".
53/// - **UFS1** — `fs_magic` `0x00011954` (LE `54 19 01 00`) at offset `9564`:
54/// util-linux `libblkid/src/superblocks/ufs.c` reads `fs_magic` at
55/// `offsets[i]*1024 + offsetof(struct ufs_super_block, fs_magic)`, with
56/// `offsets[] = {0, 8, 64, 256}` (KiB) and `offsetof(fs_magic) = 1372` (the
57/// final field of the 1376-byte superblock). The canonical UFS1 primary
58/// superblock is `SBLOCK_UFS1 = 8192`, so `8192 + 1372 = 9564`. Magic
59/// `UFS_MAGIC = 0x00011954`; FreeBSD `sys/ufs/ffs/fs.h` (`FS_UFS1_MAGIC`,
60/// `SBLOCK_UFS1`). Source:
61/// <https://raw.githubusercontent.com/util-linux/util-linux/master/libblkid/src/superblocks/ufs.c>
62/// - **UFS2** — `fs_magic` `0x19540119` (LE `19 01 54 19`) at offset `66908`:
63/// same `ufs.c` reader; UFS2 primary superblock is `SBLOCK_UFS2 = 65536`, so
64/// `65536 + 1372 = 66908`. Magic `UFS2_MAGIC = 0x19540119`; FreeBSD
65/// `sys/ufs/ffs/fs.h` (`FS_UFS2_MAGIC`, `SBLOCK_UFS2`).
66/// - **ReFS** — magic `"\0\0\0ReFS\0"` (8 bytes) at offset `0`: util-linux
67/// `libblkid/src/superblocks/refs.c`
68/// (`{ .magic = "\000\000\000ReFS\000", .len = 8 }`, `kboff`/`sboff` default
69/// `0`). The `"ReFS"` string sits at byte `3` (the OEM-ID field NTFS/FAT use,
70/// zeroed on ReFS) framed by leading `00 00 00` and a trailing `00`; the
71/// `"FSRS"` File-System-Recognition-Structure id follows at `0x10`. DFRWS 2020
72/// "Forensic Analysis of the Resilient File System (ReFS) Version 3.4" (Prade
73/// et al.); <https://www.resilientfilesystem.co.uk/refs-volume-boot-record>.
74/// Source:
75/// <https://raw.githubusercontent.com/util-linux/util-linux/master/libblkid/src/superblocks/refs.c>
76/// - **UDF** — Volume-Structure-Descriptor `stdIdent` `"NSR02"`/`"NSR03"` at
77/// offset `0x8801` (`34817`): the ECMA-167 Volume Recognition Sequence begins
78/// at sector 16 (`UDF_VSD_OFFSET = 0x8000`); each 2048-byte descriptor is
79/// `structType`(1 byte) + `stdIdent[5]`, so `stdIdent` is at `+1`. The
80/// UDF-defining NSR descriptor follows `BEA01` at sector 17, giving
81/// `0x8000 + 2048 + 1 = 0x8801`. util-linux `libblkid/src/superblocks/udf.c`
82/// scans up to 64 VRS descriptors (`.magic = "NSR02"/"NSR03", .kboff = 32,
83/// .sboff = 1`) and treats NSR02/NSR03 as the positive UDF match; ECMA-167
84/// 3rd ed. 2/9.1, 3/9.1. This fixed-offset entry captures the standard
85/// layout (BEA01→NSR→TEA01, verified on a real macOS `newfs_udf` image with
86/// `NSR03` at `0x8801`); a UDF-bridged disc that pushes NSR past sector 17
87/// needs the multi-sector VRS scan libblkid performs. Source:
88/// <https://raw.githubusercontent.com/util-linux/util-linux/master/libblkid/src/superblocks/udf.c>
89/// - **ZFS** — *not in this table by design.* ZFS has no single fixed-offset
90/// magic the [`FsSignature`] struct can carry: util-linux
91/// `libblkid/src/superblocks/zfs.c` uses `.magics = BLKID_NONE_MAGIC` and
92/// `probe_zfs` scans **four** 256-KiB vdev labels (two at the device start,
93/// two at the device *end*, so their offsets depend on the device size). The
94/// uberblock magic `0x00bab10c` (`ub_magic`, host-endian — both LE
95/// `0c b1 ba 00` and BE `00 ba b1 0c` occur) lives in each label's **uberblock
96/// ring** at label-offset `+128 KiB`, but the *active* slot is
97/// `txg % slot_count`, so the magic's byte position is **data-dependent**
98/// (on the OpenZFS `zol-0.6.1` real label the magic first appears at `0x21000`,
99/// not at the ring start `0x20000`, which is zeros — a fixed-offset entry would
100/// false-negative). ZFS is therefore detected by the structural [`detect_zfs`]
101/// scan, which [`detect_name`] falls through to after this table misses.
102/// Source:
103/// <https://raw.githubusercontent.com/util-linux/util-linux/master/libblkid/src/superblocks/zfs.c>
104pub const FILESYSTEM_SIGNATURES: &[FsSignature] = &[
105 FsSignature {
106 name: "ext2/3/4",
107 offset: 0x438,
108 magic: &[0x53, 0xEF],
109 },
110 FsSignature {
111 name: "NTFS",
112 offset: 3,
113 magic: b"NTFS ",
114 },
115 FsSignature {
116 name: "exFAT",
117 offset: 3,
118 magic: b"EXFAT ",
119 },
120 FsSignature {
121 name: "XFS",
122 offset: 0,
123 magic: b"XFSB",
124 },
125 FsSignature {
126 name: "LUKS",
127 offset: 0,
128 magic: b"LUKS\xba\xbe",
129 },
130 FsSignature {
131 name: "APFS",
132 offset: 32,
133 magic: b"NXSB",
134 },
135 FsSignature {
136 name: "FAT32",
137 offset: 0x52,
138 magic: b"FAT32 ",
139 },
140 FsSignature {
141 name: "FAT16",
142 offset: 0x36,
143 magic: b"FAT16 ",
144 },
145 FsSignature {
146 name: "FAT12",
147 offset: 0x36,
148 magic: b"FAT12 ",
149 },
150 FsSignature {
151 name: "Linux swap",
152 offset: 0xFF6,
153 magic: b"SWAPSPACE2",
154 },
155 FsSignature {
156 name: "LVM2",
157 offset: 0,
158 magic: b"LABELONE",
159 },
160 FsSignature {
161 name: "LVM2",
162 offset: 512,
163 magic: b"LABELONE",
164 },
165 FsSignature {
166 name: "ISO 9660",
167 offset: 0x8001,
168 magic: b"CD001",
169 },
170 FsSignature {
171 name: "HFS+",
172 offset: 0x400,
173 magic: b"H+",
174 },
175 FsSignature {
176 name: "Btrfs",
177 offset: 65600,
178 magic: b"_BHRfS_M",
179 },
180 FsSignature {
181 name: "UFS1",
182 offset: 9564,
183 magic: &[0x54, 0x19, 0x01, 0x00],
184 },
185 FsSignature {
186 name: "UFS2",
187 offset: 66908,
188 magic: &[0x19, 0x01, 0x54, 0x19],
189 },
190 FsSignature {
191 name: "ReFS",
192 offset: 0,
193 magic: b"\x00\x00\x00ReFS\x00",
194 },
195 FsSignature {
196 name: "UDF",
197 offset: 0x8801,
198 magic: b"NSR02",
199 },
200 FsSignature {
201 name: "UDF",
202 offset: 0x8801,
203 magic: b"NSR03",
204 },
205];
206
207/// Identify the filesystem from a volume's leading bytes, returning the first
208/// matching signature's name. Returns `None` when nothing matches (the slice may
209/// simply be too short to reach a deeper magic).
210///
211/// ZFS has no fixed-offset magic the [`FsSignature`] table can carry (see the
212/// [`FILESYSTEM_SIGNATURES`] doc comment), so after the fixed-offset table
213/// misses this falls through to the structural [`detect_zfs`] scan and reports
214/// `"ZFS"` on a hit.
215#[must_use]
216pub fn detect_name(data: &[u8]) -> Option<&'static str> {
217 FILESYSTEM_SIGNATURES
218 .iter()
219 .find_map(|sig| {
220 let end = sig.offset.checked_add(sig.magic.len())?;
221 (data.len() >= end && &data[sig.offset..end] == sig.magic).then_some(sig.name)
222 })
223 .or_else(|| detect_zfs(data).then_some("ZFS"))
224}
225
226/// Device offset of the L0 vdev label (labels L0/L1 sit at the device start).
227const ZFS_L0_LABEL_OFFSET: usize = 0;
228/// Offset of the uberblock ring within a vdev label (`VDEV_LABEL_NVPAIR` end).
229const ZFS_UBERBLOCK_RING_OFFSET: usize = 128 * 1024;
230/// A vdev label is 256 KiB (`VDEV_LABEL_SIZE`).
231const ZFS_VDEV_LABEL_SIZE: usize = 256 * 1024;
232/// Smallest uberblock slot (1 KiB at the default ashift); larger slots (up to
233/// 8 KiB) still start on a 1 KiB boundary, so scanning at this stride reaches
234/// every possible active slot.
235const ZFS_UBERBLOCK_MIN_SLOT: usize = 1024;
236/// ZFS uberblock magic `0x00bab10c` (`ub_magic`), little-endian byte order.
237const ZFS_UBERBLOCK_MAGIC_LE: [u8; 4] = [0x0c, 0xb1, 0xba, 0x00];
238/// ZFS uberblock magic `0x00bab10c` (`ub_magic`), big-endian byte order.
239const ZFS_UBERBLOCK_MAGIC_BE: [u8; 4] = [0x00, 0xba, 0xb1, 0x0c];
240
241/// Structural ZFS detector: scan the **L0 vdev label's uberblock ring** for the
242/// `0x00bab10c` uberblock magic (`ub_magic`) in **either** endianness.
243///
244/// ZFS writes no single fixed-offset magic (unlike the [`FsSignature`] table
245/// entries): a vdev label is 256 KiB and its uberblock ring begins at label
246/// offset `+128 KiB`. Uberblocks are slot-sized (1 KiB by default, up to 8 KiB
247/// by `ashift`) and the *active* slot is `txg % slot_count`, so the magic's byte
248/// position is **data-dependent** — a single fixed offset false-negatives (on a
249/// real OpenZFS `zol-0.6.1` label the magic first appears at `0x21000`, not at
250/// the ring start `0x20000`, which is zeros). So this scans the whole ring
251/// region — device offsets `0x20000..0x40000` (label end) — at a 1 KiB stride
252/// (the smallest slot boundary, which every larger slot also lands on) and
253/// returns `true` on the first magic in either byte order.
254///
255/// The magic is stored **host-endian**, so both little-endian (`0c b1 ba 00`)
256/// and big-endian (`00 ba b1 0c`) occur in the wild and both are matched. Every
257/// access is bounds-checked (via `slice::get`), so a short or truncated slice
258/// yields `false` rather than panicking, and no allocation is performed.
259///
260/// # Scope and known limitation
261///
262/// This is a **partial, heuristic** check: it inspects only the **L0 label at
263/// device offset 0** and matches the uberblock magic — it does **not** validate
264/// the XDR NVList header the way libblkid's `probe_zfs` does (`nvh_encoding ==
265/// 0x1`), so it is a lighter (but accepted) signal than a full NVList parse. A
266/// device whose front labels are wiped (leaving only the two labels ZFS mirrors
267/// at the *device end*, whose offsets depend on the total device size) is **not**
268/// covered here and would need an end-of-device label scan.
269///
270/// Cross-checked against util-linux `libblkid/src/superblocks/zfs.c`
271/// (`VDEV_LABEL_SIZE = 256 KiB`, `VDEV_LABEL_NVPAIR = 16 KiB`, four labels, the
272/// "128x1kB host-endian root blocks... #4 @ 132kB is the first one written"
273/// comment) and the OpenZFS on-disk-format `uberblock_t` / `ub_magic`
274/// definition. Source:
275/// <https://raw.githubusercontent.com/util-linux/util-linux/master/libblkid/src/superblocks/zfs.c>
276#[must_use]
277pub fn detect_zfs(data: &[u8]) -> bool {
278 let ring_start = ZFS_L0_LABEL_OFFSET + ZFS_UBERBLOCK_RING_OFFSET;
279 let ring_end = ZFS_L0_LABEL_OFFSET + ZFS_VDEV_LABEL_SIZE;
280 let mut off = ring_start;
281 while off < ring_end {
282 if let Some(slot) = data.get(off..off + 4) {
283 if slot == ZFS_UBERBLOCK_MAGIC_LE || slot == ZFS_UBERBLOCK_MAGIC_BE {
284 return true;
285 }
286 }
287 off += ZFS_UBERBLOCK_MIN_SLOT;
288 }
289 false
290}
291
292/// Canonical identity of a filesystem, content-addressed by a stable lowercase
293/// name.
294///
295/// A newtype over `&'static str` (not an enum) so the set is **open**: adding a
296/// filesystem is a new `const` here, never a breaking change to a downstream
297/// contract crate. Every filesystem is a uniform `const` — none is a
298/// first-class variant and none is a stringly-typed `Other`.
299///
300/// Intended as the single source of the identity that `forensic-vfs::FsKind`
301/// re-exports, retiring its named-variants-plus-`Other` enum.
302///
303/// The names here are the canonical *identity* labels; they intentionally
304/// differ from the human-facing detection names in [`FILESYSTEM_SIGNATURES`]
305/// (e.g. identity `ext` vs. the signature label `ext2/3/4`). Identity and
306/// detection are kept as two separate concerns; this type carries no magics.
307#[derive(Clone, Copy, PartialEq, Eq, Hash)]
308pub struct FsKind(&'static str);
309
310impl FsKind {
311 /// NTFS.
312 pub const NTFS: FsKind = FsKind("ntfs");
313 /// FAT (FAT12/16/32 family).
314 pub const FAT: FsKind = FsKind("fat");
315 /// exFAT.
316 pub const EXFAT: FsKind = FsKind("exfat");
317 /// ext2/3/4 family.
318 pub const EXT: FsKind = FsKind("ext");
319 /// XFS.
320 pub const XFS: FsKind = FsKind("xfs");
321 /// Apple APFS.
322 pub const APFS: FsKind = FsKind("apfs");
323 /// Apple HFS+.
324 pub const HFS_PLUS: FsKind = FsKind("hfsplus");
325 /// ISO 9660 optical filesystem.
326 pub const ISO9660: FsKind = FsKind("iso9660");
327 /// UDF optical filesystem.
328 pub const UDF: FsKind = FsKind("udf");
329 /// Btrfs.
330 pub const BTRFS: FsKind = FsKind("btrfs");
331 /// ZFS.
332 pub const ZFS: FsKind = FsKind("zfs");
333 /// UFS/FFS.
334 pub const UFS: FsKind = FsKind("ufs");
335 /// Microsoft ReFS.
336 pub const REFS: FsKind = FsKind("refs");
337 /// ZIP archive-as-container.
338 pub const ZIP: FsKind = FsKind("zip");
339 /// AccessData AD1 logical-image container.
340 pub const AD1: FsKind = FsKind("ad1");
341 /// DAR (Disk ARchive) container.
342 pub const DAR: FsKind = FsKind("dar");
343
344 /// Documented fallback for a name outside [`known`](FsKind::known). A
345 /// runtime string cannot be promoted to a `&'static str` without leaking, so
346 /// deserializing an unrecognized name collapses to this single sentinel
347 /// (the first registered kind) rather than allocating or panicking. Callers
348 /// needing strict validation compare the input against
349 /// [`known`](FsKind::known) before trusting a deserialized value.
350 pub const UNKNOWN_FALLBACK: FsKind = FsKind::NTFS;
351
352 /// The stable lowercase identifier — round-trips, safe for logs / JSON / URIs.
353 #[must_use]
354 pub const fn as_str(&self) -> &'static str {
355 self.0
356 }
357
358 /// Construct from a compile-time name. Returns a kind wrapping the given
359 /// static string; pass one of the const names to get a registered kind.
360 #[must_use]
361 pub const fn from_name(name: &'static str) -> FsKind {
362 FsKind(name)
363 }
364
365 /// All registered kinds — lets consumers enumerate/validate without a closed
366 /// enum.
367 ///
368 /// Not `const fn`: referencing a `static` from a `const fn` (`const_refs_to_static`)
369 /// only stabilized in Rust 1.83, and this crate's MSRV is 1.75. A plain `fn` returning
370 /// the `'static` slice works on every supported toolchain.
371 #[must_use]
372 pub fn known() -> &'static [FsKind] {
373 KNOWN
374 }
375}
376
377static KNOWN: &[FsKind] = &[
378 FsKind::NTFS,
379 FsKind::FAT,
380 FsKind::EXFAT,
381 FsKind::EXT,
382 FsKind::XFS,
383 FsKind::APFS,
384 FsKind::HFS_PLUS,
385 FsKind::ISO9660,
386 FsKind::UDF,
387 FsKind::BTRFS,
388 FsKind::ZFS,
389 FsKind::UFS,
390 FsKind::REFS,
391 FsKind::ZIP,
392 FsKind::AD1,
393 FsKind::DAR,
394];
395
396impl core::fmt::Display for FsKind {
397 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
398 f.write_str(self.0)
399 }
400}
401
402impl core::fmt::Debug for FsKind {
403 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
404 f.debug_tuple("FsKind").field(&self.0).finish()
405 }
406}
407
408#[cfg(feature = "serde")]
409impl serde::Serialize for FsKind {
410 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
411 serializer.serialize_str(self.0)
412 }
413}
414
415#[cfg(feature = "serde")]
416impl<'de> serde::Deserialize<'de> for FsKind {
417 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
418 // Borrow the bare string; resolve it against the registered kinds so the
419 // result holds a `&'static str`, never runtime-owned memory. An
420 // unrecognized name maps to `UNKNOWN_FALLBACK` (see its docs).
421 let name: &str = <&str as serde::Deserialize>::deserialize(deserializer)?;
422 Ok(KNOWN
423 .iter()
424 .copied()
425 .find(|k| k.0 == name)
426 .unwrap_or(FsKind::UNKNOWN_FALLBACK))
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 fn buf_with(offset: usize, magic: &[u8]) -> Vec<u8> {
435 let mut v = vec![0u8; offset + magic.len() + 4];
436 v[offset..offset + magic.len()].copy_from_slice(magic);
437 v
438 }
439
440 #[test]
441 fn detects_ext_at_0x438() {
442 assert_eq!(
443 detect_name(&buf_with(0x438, &[0x53, 0xEF])),
444 Some("ext2/3/4")
445 );
446 }
447
448 #[test]
449 fn detects_ntfs_and_exfat_oem() {
450 assert_eq!(detect_name(&buf_with(3, b"NTFS ")), Some("NTFS"));
451 assert_eq!(detect_name(&buf_with(3, b"EXFAT ")), Some("exFAT"));
452 }
453
454 #[test]
455 fn detects_luks_and_xfs_at_zero() {
456 assert_eq!(detect_name(&buf_with(0, b"LUKS\xba\xbe")), Some("LUKS"));
457 assert_eq!(detect_name(&buf_with(0, b"XFSB")), Some("XFS"));
458 }
459
460 #[test]
461 fn detects_apfs_at_offset_32() {
462 // libblkid: NXSB at sboff 32 (after the 32-byte obj_phys header).
463 assert_eq!(detect_name(&buf_with(32, b"NXSB")), Some("APFS"));
464 // NXSB at offset 0 is NOT APFS (the common off-by-32 mistake).
465 assert_eq!(detect_name(&buf_with(0, b"NXSB")), None);
466 }
467
468 #[test]
469 fn detects_btrfs_at_65600() {
470 // libblkid: _BHRfS_M at kboff 64 + sboff 0x40 = 65600.
471 assert_eq!(detect_name(&buf_with(65600, b"_BHRfS_M")), Some("Btrfs"));
472 assert_eq!(detect_name(&buf_with(65536, b"_BHRfS_M")), None);
473 }
474
475 #[test]
476 fn detects_lvm_at_sector_0_or_1() {
477 assert_eq!(detect_name(&buf_with(0, b"LABELONE")), Some("LVM2"));
478 // The default PV label is in sector 1 (offset 512) — the case mbr missed.
479 assert_eq!(detect_name(&buf_with(512, b"LABELONE")), Some("LVM2"));
480 }
481
482 #[test]
483 fn detects_ufs1_at_9564() {
484 // libblkid `ufs.c`: `fs_magic` (offset 1372 in `struct fs`) checked at
485 // superblock KiB positions {0,8,64,256}. The canonical UFS1 primary
486 // superblock is at 8192 (`SBLOCK_UFS1`), so `fs_magic` = 8192 + 1372 =
487 // 9564. Magic `UFS_MAGIC = 0x00011954`, little-endian on disk.
488 assert_eq!(
489 detect_name(&buf_with(9564, &[0x54, 0x19, 0x01, 0x00])),
490 Some("UFS1")
491 );
492 // Same magic at the wrong offset (e.g. the superblock start, not
493 // `fs_magic`) must not match.
494 assert_eq!(
495 detect_name(&buf_with(8192, &[0x54, 0x19, 0x01, 0x00])),
496 None
497 );
498 }
499
500 #[test]
501 fn detects_ufs2_at_66908() {
502 // libblkid `ufs.c`: UFS2 primary superblock at 65536 (`SBLOCK_UFS2`), so
503 // `fs_magic` = 65536 + 1372 = 66908. Magic `UFS2_MAGIC = 0x19540119`, LE.
504 assert_eq!(
505 detect_name(&buf_with(66908, &[0x19, 0x01, 0x54, 0x19])),
506 Some("UFS2")
507 );
508 assert_eq!(
509 detect_name(&buf_with(65536, &[0x19, 0x01, 0x54, 0x19])),
510 None
511 );
512 }
513
514 #[test]
515 fn detects_refs_at_zero() {
516 // libblkid `refs.c`: magic `"\0\0\0ReFS\0"` (len 8) at offset 0 — the
517 // OEM-ID field (offset 3, zeroed by NTFS/FAT) holds NUL-framed "ReFS".
518 assert_eq!(
519 detect_name(&buf_with(0, b"\x00\x00\x00ReFS\x00")),
520 Some("ReFS")
521 );
522 // "ReFS" at offset 3 WITHOUT the trailing NUL frame (e.g. a stray string)
523 // must not match the full 8-byte pattern.
524 assert_eq!(detect_name(&buf_with(3, b"ReFSxxxx")), None);
525 }
526
527 #[test]
528 fn detects_udf_nsr02_nsr03_at_0x8801() {
529 // libblkid `udf.c` / ECMA-167: the Volume Recognition Sequence begins at
530 // sector 16 (0x8000); each 2048-byte descriptor is `structType`(1) +
531 // `stdIdent[5]`. The UDF-defining NSR descriptor sits at sector 17, so its
532 // 5-byte id is at 0x8000 + 2048 + 1 = 0x8801 (34817). Verified on a real
533 // macOS `newfs_udf` image (NSR03 at 0x8801).
534 assert_eq!(detect_name(&buf_with(0x8801, b"NSR02")), Some("UDF"));
535 assert_eq!(detect_name(&buf_with(0x8801, b"NSR03")), Some("UDF"));
536 // A bare "NSR" prefix or wrong id at the same offset must not match.
537 assert_eq!(detect_name(&buf_with(0x8801, b"NSRxx")), None);
538 }
539
540 #[test]
541 fn empty_and_unknown_are_none() {
542 assert_eq!(detect_name(&[]), None);
543 assert_eq!(detect_name(&[0u8; 512]), None);
544 }
545
546 /// Build a 256 KiB L0 vdev label with the uberblock magic placed at a given
547 /// device offset, in the requested endianness.
548 fn zfs_label_with_magic(offset: usize, be: bool) -> Vec<u8> {
549 let mut v = vec![0u8; 256 * 1024];
550 let magic: [u8; 4] = if be {
551 [0x00, 0xba, 0xb1, 0x0c]
552 } else {
553 [0x0c, 0xb1, 0xba, 0x00]
554 };
555 v[offset..offset + 4].copy_from_slice(&magic);
556 v
557 }
558
559 #[test]
560 fn detect_zfs_finds_le_magic_in_uberblock_ring() {
561 // Tier-3 (synthetic): magic at 0x21000 (the real-label first-write slot),
562 // little-endian `0c b1 ba 00`. Ring start 0x20000 is zeros, proving the
563 // scan (not a single fixed offset) is what finds it.
564 let label = zfs_label_with_magic(0x21000, false);
565 assert!(detect_zfs(&label));
566 assert_eq!(detect_name(&label), Some("ZFS"));
567 }
568
569 #[test]
570 fn detect_zfs_finds_be_magic_in_uberblock_ring() {
571 // Tier-3 (synthetic): host-endian ZFS also writes big-endian
572 // `00 ba b1 0c`; both must match. Placed at ring start 0x20000.
573 let label = zfs_label_with_magic(0x20000, true);
574 assert!(detect_zfs(&label));
575 assert_eq!(detect_name(&label), Some("ZFS"));
576 }
577
578 #[test]
579 fn detect_zfs_finds_magic_at_ring_end() {
580 // Tier-3: last scanned 1 KiB slot before the label end (0x40000 - 0x400).
581 let label = zfs_label_with_magic(0x40000 - 0x400, false);
582 assert!(detect_zfs(&label));
583 }
584
585 #[test]
586 fn detect_zfs_rejects_zeros_and_wrong_magic() {
587 // Tier-3 negatives: all-zeros, and the magic sitting *before* the ring
588 // (at offset 0) must NOT be detected as ZFS.
589 assert!(!detect_zfs(&vec![0u8; 256 * 1024]));
590 let mut before_ring = vec![0u8; 256 * 1024];
591 before_ring[0..4].copy_from_slice(&[0x0c, 0xb1, 0xba, 0x00]);
592 assert!(!detect_zfs(&before_ring));
593 // Magic present but at a non-1-KiB-aligned position within the ring is
594 // not on any slot boundary, so the stride scan does not spuriously hit.
595 let mut misaligned = vec![0u8; 256 * 1024];
596 misaligned[0x21000 + 3..0x21000 + 7].copy_from_slice(&[0x0c, 0xb1, 0xba, 0x00]);
597 assert!(!detect_zfs(&misaligned));
598 }
599
600 #[test]
601 fn detect_zfs_does_not_panic_on_short_slices() {
602 // Slices shorter than the ring start (0x20000) simply yield false.
603 assert!(!detect_zfs(&[]));
604 assert!(!detect_zfs(&[0u8; 8]));
605 assert!(!detect_zfs(&vec![0u8; 0x20000]));
606 // A slice reaching just one byte past a slot boundary but not a full
607 // 4-byte magic must not panic and must return false.
608 assert!(!detect_zfs(&vec![0u8; 0x20001]));
609 }
610
611 // The tier-2 real-artifact assertion (OpenZFS `zol-0.6.1` vdev label, env-gated
612 // by `ZFS_LABEL_FIXTURE`) lives in `crates/core/tests/zfs_label_oracle.rs`,
613 // alongside the other env-gated oracle tests, so `--lib` coverage stays exact.
614
615 #[test]
616 fn short_slice_does_not_panic() {
617 // A slice shorter than a deep magic's offset must simply not match.
618 assert_eq!(detect_name(&[0u8; 8]), None);
619 }
620
621 #[test]
622 fn signatures_are_well_formed() {
623 for s in FILESYSTEM_SIGNATURES {
624 assert!(!s.magic.is_empty(), "{} has empty magic", s.name);
625 assert!(!s.name.is_empty());
626 }
627 }
628
629 #[test]
630 fn fskind_as_str_is_canonical_lowercase() {
631 assert_eq!(FsKind::XFS.as_str(), "xfs");
632 assert_eq!(FsKind::HFS_PLUS.as_str(), "hfsplus");
633 assert_eq!(FsKind::ISO9660.as_str(), "iso9660");
634 }
635
636 #[test]
637 fn fskind_from_name_round_trips_every_const() {
638 for &k in FsKind::known() {
639 assert_eq!(FsKind::from_name(k.as_str()), k);
640 }
641 }
642
643 #[test]
644 fn fskind_known_has_every_const_and_no_duplicate_name() {
645 let expected = [
646 FsKind::NTFS,
647 FsKind::FAT,
648 FsKind::EXFAT,
649 FsKind::EXT,
650 FsKind::XFS,
651 FsKind::APFS,
652 FsKind::HFS_PLUS,
653 FsKind::ISO9660,
654 FsKind::UDF,
655 FsKind::BTRFS,
656 FsKind::ZFS,
657 FsKind::UFS,
658 FsKind::REFS,
659 FsKind::ZIP,
660 FsKind::AD1,
661 FsKind::DAR,
662 ];
663 for k in expected {
664 assert!(FsKind::known().contains(&k), "known() missing {k}");
665 }
666 assert_eq!(FsKind::known().len(), expected.len());
667 let mut names: Vec<&str> = FsKind::known().iter().map(FsKind::as_str).collect();
668 let total = names.len();
669 names.sort_unstable();
670 names.dedup();
671 assert_eq!(names.len(), total, "duplicate as_str in known()");
672 }
673
674 #[test]
675 fn fskind_display_writes_as_str() {
676 assert_eq!(FsKind::BTRFS.to_string(), "btrfs");
677 assert_eq!(format!("{}", FsKind::ZFS), "zfs");
678 }
679
680 #[test]
681 fn fskind_debug_is_readable() {
682 let s = format!("{:?}", FsKind::APFS);
683 assert!(s.contains("FsKind"), "{s}");
684 assert!(s.contains("apfs"), "{s}");
685 }
686
687 #[cfg(feature = "serde")]
688 #[test]
689 fn fskind_serde_round_trips_bare_string() {
690 assert_eq!(serde_json::to_string(&FsKind::BTRFS).unwrap(), "\"btrfs\"");
691 let back: FsKind = serde_json::from_str("\"btrfs\"").unwrap();
692 assert_eq!(back, FsKind::BTRFS);
693 for &k in FsKind::known() {
694 let json = serde_json::to_string(&k).unwrap();
695 assert_eq!(json, format!("\"{}\"", k.as_str()));
696 assert_eq!(serde_json::from_str::<FsKind>(&json).unwrap(), k);
697 }
698 }
699
700 #[cfg(feature = "serde")]
701 #[test]
702 fn fskind_deserialize_unknown_name_maps_to_sentinel() {
703 let back: FsKind = serde_json::from_str("\"nonesuch-fs\"").unwrap();
704 assert_eq!(back, FsKind::UNKNOWN_FALLBACK);
705 }
706}