diff --git a/crates/superblock/src/fat.rs b/crates/superblock/src/fat.rs new file mode 100644 index 0000000..3ea3c17 --- /dev/null +++ b/crates/superblock/src/fat.rs @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers +// +// SPDX-License-Identifier: MPL-2.0 + +//! Fat32 +//! +//! This module implements parsing and access to the FAT32 filesystem boot sector, +//! which contains critical metadata about the filesystem including: +//! - Version information +//! - Volume name and UUID +//! - Encryption settings + +use std::io; + +use crate::{Detection, Error}; +use zerocopy::*; + +/// Starting position of superblock in bytes +pub const START_POSITION: u64 = 0; + +const MAGIC: [u8; 2] = [0x55, 0xAA]; + +#[repr(C, packed)] +#[derive(FromBytes, Unaligned, Debug)] +pub struct Fat { + /// Boot strap short or near jump + pub ignored: [u8; 3], + /// Name - can be used to special case partition manager volumes + pub system_id: [u8; 8], + /// Bytes per logical sector + pub sector_size: U16, + /// Sectors per cluster + pub sec_per_clus: u8, + /// Reserved sectors + pub _reserved: U16, + /// Number of FATs + pub fats: u8, + /// Root directory entries + pub dir_entries: U16, + /// Number of sectors + pub sectors: U16, + /// Media code + pub media: u8, + /// Sectors/FAT + pub fat_length: U16, + /// Sectors per track + pub secs_track: U16, + /// Number of heads + pub heads: U16, + /// Hidden sectors (unused) + pub hidden: U32, + /// Number of sectors (if sectors == 0) + pub total_sect: U32, + + // Shared memory region for FAT16 and FAT32 + // Best way is to use a union with zerocopy, however that requires having to use `--cfg zerocopy_derive_union_into_bytes` https://github.com/google/zerocopy/issues/1792` + pub shared: [u8; 54], // The size of the union fields in bytes +} + +#[derive(FromBytes, Unaligned)] +#[repr(C, packed)] +pub struct Fat16And32Fields { + // Physical drive number + pub drive_number: u8, + // Mount state + pub state: u8, + // Extended boot signature + pub signature: u8, + // Volume ID + pub vol_id: U32, + // Volume label + pub vol_label: [u8; 11], + // File system type + pub fs_type: [u8; 8], +} + +#[derive(FromBytes, Unaligned)] +#[repr(C, packed)] +pub struct Fat16Fields { + pub common: Fat16And32Fields, +} + +impl Fat16Fields {} + +#[derive(FromBytes, Unaligned)] +#[repr(C, packed)] +pub struct Fat32Fields { + // FAT32-specific fields + /// Sectors/FAT + pub fat32_length: U32, + /// FAT mirroring flags + pub fat32_flags: U16, + /// Major, minor filesystem version + pub fat32_version: [u8; 2], + /// First cluster in root directory + pub root_cluster: U32, + /// Filesystem info sector + pub info_sector: U16, + /// Backup boot sector + pub backup_boot: U16, + /// Unused + pub reserved2: [U16; 6], + + pub common: Fat16And32Fields, +} + +impl Detection for Fat { + type Magic = [u8; 2]; + + const OFFSET: u64 = START_POSITION; + + const MAGIC_OFFSET: u64 = 0x1FE; + + const SIZE: usize = std::mem::size_of::(); + + fn is_valid_magic(magic: &Self::Magic) -> bool { + *magic == MAGIC + } +} + +pub enum FatType { + Fat16, + Fat32, +} + +impl Fat { + pub fn fat_type(&self) -> Result { + // this is how the linux kernel does it in https://github.com/torvalds/linux/blob/master/fs/fat/inode.c + if self.fat_length == 0 && self.fat32()?.fat32_length != 0 { + Ok(FatType::Fat32) + } else { + Ok(FatType::Fat16) + } + } + + /// Returns the filesystem id + pub fn uuid(&self) -> Result { + match self.fat_type()? { + FatType::Fat16 => vol_id(self.fat16()?.common.vol_id), + FatType::Fat32 => vol_id(self.fat32()?.common.vol_id), + } + } + + /// Returns the volume label + pub fn label(&self) -> Result { + match self.fat_type()? { + FatType::Fat16 => vol_label(&self.fat16()?.common.vol_label), + FatType::Fat32 => vol_label(&self.fat32()?.common.vol_label), + } + } + + fn fat16(&self) -> Result { + Ok(Fat16Fields::read_from_bytes(&self.shared[..size_of::()]) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Error Reading FAT16 Superblock"))?) + } + + fn fat32(&self) -> Result { + Ok(Fat32Fields::read_from_bytes(&self.shared) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Error Reading FAT32 Superblock"))?) + } +} + +fn vol_label(vol_label: &[u8; 11]) -> Result { + Ok(String::from_utf8_lossy(vol_label).trim_end_matches(' ').to_string()) +} + +fn vol_id(vol_id: U32) -> Result { + Ok(format!("{:04X}-{:04X}", vol_id >> 16, vol_id & 0xFFFF)) +} diff --git a/crates/superblock/src/lib.rs b/crates/superblock/src/lib.rs index f022767..967ce94 100644 --- a/crates/superblock/src/lib.rs +++ b/crates/superblock/src/lib.rs @@ -15,6 +15,7 @@ use zerocopy::FromBytes; pub mod btrfs; pub mod ext4; pub mod f2fs; +pub mod fat; pub mod luks2; pub mod xfs; @@ -99,6 +100,8 @@ pub enum Kind { F2FS, /// XFS filesystem XFS, + /// FAT filesystem + FAT, } impl std::fmt::Display for Kind { @@ -109,6 +112,7 @@ impl std::fmt::Display for Kind { Kind::LUKS2 => f.write_str("luks2"), Kind::F2FS => f.write_str("f2fs"), Kind::XFS => f.write_str("xfs"), + Kind::FAT => f.write_str("fat"), } } } @@ -119,6 +123,7 @@ pub enum Superblock { F2FS(Box), LUKS2(Box), XFS(Box), + FAT(Box), } impl Superblock { @@ -130,6 +135,7 @@ impl Superblock { Superblock::F2FS(_) => Kind::F2FS, Superblock::LUKS2(_) => Kind::LUKS2, Superblock::XFS(_) => Kind::XFS, + Superblock::FAT(_) => Kind::FAT, } } @@ -141,6 +147,7 @@ impl Superblock { Superblock::F2FS(block) => block.uuid(), Superblock::LUKS2(block) => block.uuid(), Superblock::XFS(block) => block.uuid(), + Superblock::FAT(block) => block.uuid(), } } @@ -152,6 +159,7 @@ impl Superblock { Superblock::F2FS(block) => block.label(), Superblock::LUKS2(block) => block.label(), Superblock::XFS(block) => block.label(), + Superblock::FAT(block) => block.label(), } } } @@ -179,7 +187,9 @@ impl Superblock { if let Some(sb) = detect_superblock::(&mut cursor)? { return Ok(Self::LUKS2(Box::new(sb))); } - + if let Some(sb) = detect_superblock::(&mut cursor)? { + return Ok(Self::FAT(Box::new(sb))); + } Err(Error::UnknownSuperblock) } @@ -231,6 +241,8 @@ mod tests { ), ("luks+ext4", Kind::LUKS2, "", "be373cae-2bd1-4ad5-953f-3463b2e53e59"), ("xfs", Kind::XFS, "BLSFORME", "45e8a3bf-8114-400f-95b0-380d0fb7d42d"), + ("fat16", Kind::FAT, "TESTLABEL", "A1B2-C3D4"), + ("fat32", Kind::FAT, "TESTLABEL", "A1B2-C3D4"), ]; // Pre-allocate a buffer for determination tests diff --git a/crates/superblock/tests/README.md b/crates/superblock/tests/README.md index bd31038..d6482e6 100644 --- a/crates/superblock/tests/README.md +++ b/crates/superblock/tests/README.md @@ -32,3 +32,27 @@ Limited to 12-char volume name UUID : 45e8a3bf-8114-400f-95b0-380d0fb7d42d LABEL: BLSFORME + +## fat16.img.zst + + UUID : A1B2-C3D4 (volume id not a uuid) + LABEL: TESTLABEL + + created with commands : + + dd if=/dev/zero of=fat16.img bs=512 count=32768 + mkfs.fat -F 16 -n "TESTLABEL" -i A1B2C3D4 fat16.img + zstd fat16.img + rm fat16.img + +## fat32.img.zst + + UUID : A1B2-C3D4 (volume id not a uuid) + LABEL: TESTLABEL + + created with commands : + + dd if=/dev/zero of=fat32.img bs=512 count=32768 + mkfs.fat -F 32 -n "TESTLABEL" -i A1B2C3D4 fat32.img + zstd fat32.img + rm fat32.img \ No newline at end of file diff --git a/crates/superblock/tests/fat16.img b/crates/superblock/tests/fat16.img new file mode 100644 index 0000000..948c887 Binary files /dev/null and b/crates/superblock/tests/fat16.img differ diff --git a/crates/superblock/tests/fat16.img.zst b/crates/superblock/tests/fat16.img.zst new file mode 100644 index 0000000..54ab4ea Binary files /dev/null and b/crates/superblock/tests/fat16.img.zst differ diff --git a/crates/superblock/tests/fat32.img b/crates/superblock/tests/fat32.img new file mode 100644 index 0000000..e851067 Binary files /dev/null and b/crates/superblock/tests/fat32.img differ diff --git a/crates/superblock/tests/fat32.img.zst b/crates/superblock/tests/fat32.img.zst new file mode 100644 index 0000000..d97a844 Binary files /dev/null and b/crates/superblock/tests/fat32.img.zst differ