Skip to main content

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}