#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FsSignature {
pub name: &'static str,
pub offset: usize,
pub magic: &'static [u8],
}
pub const FILESYSTEM_SIGNATURES: &[FsSignature] = &[
FsSignature {
name: "ext2/3/4",
offset: 0x438,
magic: &[0x53, 0xEF],
},
FsSignature {
name: "NTFS",
offset: 3,
magic: b"NTFS ",
},
FsSignature {
name: "exFAT",
offset: 3,
magic: b"EXFAT ",
},
FsSignature {
name: "XFS",
offset: 0,
magic: b"XFSB",
},
FsSignature {
name: "LUKS",
offset: 0,
magic: b"LUKS\xba\xbe",
},
FsSignature {
name: "APFS",
offset: 32,
magic: b"NXSB",
},
FsSignature {
name: "FAT32",
offset: 0x52,
magic: b"FAT32 ",
},
FsSignature {
name: "FAT16",
offset: 0x36,
magic: b"FAT16 ",
},
FsSignature {
name: "FAT12",
offset: 0x36,
magic: b"FAT12 ",
},
FsSignature {
name: "Linux swap",
offset: 0xFF6,
magic: b"SWAPSPACE2",
},
FsSignature {
name: "LVM2",
offset: 0,
magic: b"LABELONE",
},
FsSignature {
name: "LVM2",
offset: 512,
magic: b"LABELONE",
},
FsSignature {
name: "ISO 9660",
offset: 0x8001,
magic: b"CD001",
},
FsSignature {
name: "HFS+",
offset: 0x400,
magic: b"H+",
},
FsSignature {
name: "Btrfs",
offset: 65600,
magic: b"_BHRfS_M",
},
FsSignature {
name: "UFS1",
offset: 9564,
magic: &[0x54, 0x19, 0x01, 0x00],
},
FsSignature {
name: "UFS2",
offset: 66908,
magic: &[0x19, 0x01, 0x54, 0x19],
},
FsSignature {
name: "ReFS",
offset: 0,
magic: b"\x00\x00\x00ReFS\x00",
},
FsSignature {
name: "UDF",
offset: 0x8801,
magic: b"NSR02",
},
FsSignature {
name: "UDF",
offset: 0x8801,
magic: b"NSR03",
},
];
#[must_use]
pub fn detect_name(data: &[u8]) -> Option<&'static str> {
FILESYSTEM_SIGNATURES
.iter()
.find_map(|sig| {
let end = sig.offset.checked_add(sig.magic.len())?;
(data.len() >= end && &data[sig.offset..end] == sig.magic).then_some(sig.name)
})
.or_else(|| detect_zfs(data).then_some("ZFS"))
}
const ZFS_L0_LABEL_OFFSET: usize = 0;
const ZFS_UBERBLOCK_RING_OFFSET: usize = 128 * 1024;
const ZFS_VDEV_LABEL_SIZE: usize = 256 * 1024;
const ZFS_UBERBLOCK_MIN_SLOT: usize = 1024;
const ZFS_UBERBLOCK_MAGIC_LE: [u8; 4] = [0x0c, 0xb1, 0xba, 0x00];
const ZFS_UBERBLOCK_MAGIC_BE: [u8; 4] = [0x00, 0xba, 0xb1, 0x0c];
#[must_use]
pub fn detect_zfs(data: &[u8]) -> bool {
let ring_start = ZFS_L0_LABEL_OFFSET + ZFS_UBERBLOCK_RING_OFFSET;
let ring_end = ZFS_L0_LABEL_OFFSET + ZFS_VDEV_LABEL_SIZE;
let mut off = ring_start;
while off < ring_end {
if let Some(slot) = data.get(off..off + 4) {
if slot == ZFS_UBERBLOCK_MAGIC_LE || slot == ZFS_UBERBLOCK_MAGIC_BE {
return true;
}
}
off += ZFS_UBERBLOCK_MIN_SLOT;
}
false
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct FsKind(&'static str);
impl FsKind {
pub const NTFS: FsKind = FsKind("ntfs");
pub const FAT: FsKind = FsKind("fat");
pub const EXFAT: FsKind = FsKind("exfat");
pub const EXT: FsKind = FsKind("ext");
pub const XFS: FsKind = FsKind("xfs");
pub const APFS: FsKind = FsKind("apfs");
pub const HFS_PLUS: FsKind = FsKind("hfsplus");
pub const ISO9660: FsKind = FsKind("iso9660");
pub const UDF: FsKind = FsKind("udf");
pub const BTRFS: FsKind = FsKind("btrfs");
pub const ZFS: FsKind = FsKind("zfs");
pub const UFS: FsKind = FsKind("ufs");
pub const REFS: FsKind = FsKind("refs");
pub const ZIP: FsKind = FsKind("zip");
pub const AD1: FsKind = FsKind("ad1");
pub const DAR: FsKind = FsKind("dar");
pub const UNKNOWN_FALLBACK: FsKind = FsKind::NTFS;
#[must_use]
pub const fn as_str(&self) -> &'static str {
self.0
}
#[must_use]
pub const fn from_name(name: &'static str) -> FsKind {
FsKind(name)
}
#[must_use]
pub fn known() -> &'static [FsKind] {
KNOWN
}
}
static KNOWN: &[FsKind] = &[
FsKind::NTFS,
FsKind::FAT,
FsKind::EXFAT,
FsKind::EXT,
FsKind::XFS,
FsKind::APFS,
FsKind::HFS_PLUS,
FsKind::ISO9660,
FsKind::UDF,
FsKind::BTRFS,
FsKind::ZFS,
FsKind::UFS,
FsKind::REFS,
FsKind::ZIP,
FsKind::AD1,
FsKind::DAR,
];
impl core::fmt::Display for FsKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.0)
}
}
impl core::fmt::Debug for FsKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("FsKind").field(&self.0).finish()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for FsKind {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.0)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for FsKind {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let name: &str = <&str as serde::Deserialize>::deserialize(deserializer)?;
Ok(KNOWN
.iter()
.copied()
.find(|k| k.0 == name)
.unwrap_or(FsKind::UNKNOWN_FALLBACK))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn buf_with(offset: usize, magic: &[u8]) -> Vec<u8> {
let mut v = vec![0u8; offset + magic.len() + 4];
v[offset..offset + magic.len()].copy_from_slice(magic);
v
}
#[test]
fn detects_ext_at_0x438() {
assert_eq!(
detect_name(&buf_with(0x438, &[0x53, 0xEF])),
Some("ext2/3/4")
);
}
#[test]
fn detects_ntfs_and_exfat_oem() {
assert_eq!(detect_name(&buf_with(3, b"NTFS ")), Some("NTFS"));
assert_eq!(detect_name(&buf_with(3, b"EXFAT ")), Some("exFAT"));
}
#[test]
fn detects_luks_and_xfs_at_zero() {
assert_eq!(detect_name(&buf_with(0, b"LUKS\xba\xbe")), Some("LUKS"));
assert_eq!(detect_name(&buf_with(0, b"XFSB")), Some("XFS"));
}
#[test]
fn detects_apfs_at_offset_32() {
assert_eq!(detect_name(&buf_with(32, b"NXSB")), Some("APFS"));
assert_eq!(detect_name(&buf_with(0, b"NXSB")), None);
}
#[test]
fn detects_btrfs_at_65600() {
assert_eq!(detect_name(&buf_with(65600, b"_BHRfS_M")), Some("Btrfs"));
assert_eq!(detect_name(&buf_with(65536, b"_BHRfS_M")), None);
}
#[test]
fn detects_lvm_at_sector_0_or_1() {
assert_eq!(detect_name(&buf_with(0, b"LABELONE")), Some("LVM2"));
assert_eq!(detect_name(&buf_with(512, b"LABELONE")), Some("LVM2"));
}
#[test]
fn detects_ufs1_at_9564() {
assert_eq!(
detect_name(&buf_with(9564, &[0x54, 0x19, 0x01, 0x00])),
Some("UFS1")
);
assert_eq!(
detect_name(&buf_with(8192, &[0x54, 0x19, 0x01, 0x00])),
None
);
}
#[test]
fn detects_ufs2_at_66908() {
assert_eq!(
detect_name(&buf_with(66908, &[0x19, 0x01, 0x54, 0x19])),
Some("UFS2")
);
assert_eq!(
detect_name(&buf_with(65536, &[0x19, 0x01, 0x54, 0x19])),
None
);
}
#[test]
fn detects_refs_at_zero() {
assert_eq!(
detect_name(&buf_with(0, b"\x00\x00\x00ReFS\x00")),
Some("ReFS")
);
assert_eq!(detect_name(&buf_with(3, b"ReFSxxxx")), None);
}
#[test]
fn detects_udf_nsr02_nsr03_at_0x8801() {
assert_eq!(detect_name(&buf_with(0x8801, b"NSR02")), Some("UDF"));
assert_eq!(detect_name(&buf_with(0x8801, b"NSR03")), Some("UDF"));
assert_eq!(detect_name(&buf_with(0x8801, b"NSRxx")), None);
}
#[test]
fn empty_and_unknown_are_none() {
assert_eq!(detect_name(&[]), None);
assert_eq!(detect_name(&[0u8; 512]), None);
}
fn zfs_label_with_magic(offset: usize, be: bool) -> Vec<u8> {
let mut v = vec![0u8; 256 * 1024];
let magic: [u8; 4] = if be {
[0x00, 0xba, 0xb1, 0x0c]
} else {
[0x0c, 0xb1, 0xba, 0x00]
};
v[offset..offset + 4].copy_from_slice(&magic);
v
}
#[test]
fn detect_zfs_finds_le_magic_in_uberblock_ring() {
let label = zfs_label_with_magic(0x21000, false);
assert!(detect_zfs(&label));
assert_eq!(detect_name(&label), Some("ZFS"));
}
#[test]
fn detect_zfs_finds_be_magic_in_uberblock_ring() {
let label = zfs_label_with_magic(0x20000, true);
assert!(detect_zfs(&label));
assert_eq!(detect_name(&label), Some("ZFS"));
}
#[test]
fn detect_zfs_finds_magic_at_ring_end() {
let label = zfs_label_with_magic(0x40000 - 0x400, false);
assert!(detect_zfs(&label));
}
#[test]
fn detect_zfs_rejects_zeros_and_wrong_magic() {
assert!(!detect_zfs(&vec![0u8; 256 * 1024]));
let mut before_ring = vec![0u8; 256 * 1024];
before_ring[0..4].copy_from_slice(&[0x0c, 0xb1, 0xba, 0x00]);
assert!(!detect_zfs(&before_ring));
let mut misaligned = vec![0u8; 256 * 1024];
misaligned[0x21000 + 3..0x21000 + 7].copy_from_slice(&[0x0c, 0xb1, 0xba, 0x00]);
assert!(!detect_zfs(&misaligned));
}
#[test]
fn detect_zfs_does_not_panic_on_short_slices() {
assert!(!detect_zfs(&[]));
assert!(!detect_zfs(&[0u8; 8]));
assert!(!detect_zfs(&vec![0u8; 0x20000]));
assert!(!detect_zfs(&vec![0u8; 0x20001]));
}
#[test]
fn short_slice_does_not_panic() {
assert_eq!(detect_name(&[0u8; 8]), None);
}
#[test]
fn signatures_are_well_formed() {
for s in FILESYSTEM_SIGNATURES {
assert!(!s.magic.is_empty(), "{} has empty magic", s.name);
assert!(!s.name.is_empty());
}
}
#[test]
fn fskind_as_str_is_canonical_lowercase() {
assert_eq!(FsKind::XFS.as_str(), "xfs");
assert_eq!(FsKind::HFS_PLUS.as_str(), "hfsplus");
assert_eq!(FsKind::ISO9660.as_str(), "iso9660");
}
#[test]
fn fskind_from_name_round_trips_every_const() {
for &k in FsKind::known() {
assert_eq!(FsKind::from_name(k.as_str()), k);
}
}
#[test]
fn fskind_known_has_every_const_and_no_duplicate_name() {
let expected = [
FsKind::NTFS,
FsKind::FAT,
FsKind::EXFAT,
FsKind::EXT,
FsKind::XFS,
FsKind::APFS,
FsKind::HFS_PLUS,
FsKind::ISO9660,
FsKind::UDF,
FsKind::BTRFS,
FsKind::ZFS,
FsKind::UFS,
FsKind::REFS,
FsKind::ZIP,
FsKind::AD1,
FsKind::DAR,
];
for k in expected {
assert!(FsKind::known().contains(&k), "known() missing {k}");
}
assert_eq!(FsKind::known().len(), expected.len());
let mut names: Vec<&str> = FsKind::known().iter().map(FsKind::as_str).collect();
let total = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total, "duplicate as_str in known()");
}
#[test]
fn fskind_display_writes_as_str() {
assert_eq!(FsKind::BTRFS.to_string(), "btrfs");
assert_eq!(format!("{}", FsKind::ZFS), "zfs");
}
#[test]
fn fskind_debug_is_readable() {
let s = format!("{:?}", FsKind::APFS);
assert!(s.contains("FsKind"), "{s}");
assert!(s.contains("apfs"), "{s}");
}
#[cfg(feature = "serde")]
#[test]
fn fskind_serde_round_trips_bare_string() {
assert_eq!(serde_json::to_string(&FsKind::BTRFS).unwrap(), "\"btrfs\"");
let back: FsKind = serde_json::from_str("\"btrfs\"").unwrap();
assert_eq!(back, FsKind::BTRFS);
for &k in FsKind::known() {
let json = serde_json::to_string(&k).unwrap();
assert_eq!(json, format!("\"{}\"", k.as_str()));
assert_eq!(serde_json::from_str::<FsKind>(&json).unwrap(), k);
}
}
#[cfg(feature = "serde")]
#[test]
fn fskind_deserialize_unknown_name_maps_to_sentinel() {
let back: FsKind = serde_json::from_str("\"nonesuch-fs\"").unwrap();
assert_eq!(back, FsKind::UNKNOWN_FALLBACK);
}
}