#[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",
},
];
#[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)
})
}
#[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 const 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 empty_and_unknown_are_none() {
assert_eq!(detect_name(&[]), None);
assert_eq!(detect_name(&[0u8; 512]), None);
}
#[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);
}
}