I should probably make more commits

This commit is contained in:
Moritz Gmeiner 2025-07-26 15:51:21 +02:00
commit 99bb1e25c2
15 changed files with 2074 additions and 9 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target /target
/tests

130
Cargo.lock generated Normal file
View file

@ -0,0 +1,130 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"num-traits",
]
[[package]]
name = "enum_dispatch"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "fat-rs"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags",
"chrono",
"enum_dispatch",
"static_assertions",
"thiserror",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"

View file

@ -3,4 +3,14 @@ name = "fat-rs"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bin]]
name = "dump"
path = "src/dump.rs"
[dependencies] [dependencies]
anyhow = "1.0.98"
bitflags = "2.9.1"
chrono = { version = "0.4.41", default-features = false, features = ["alloc", "std"] }
enum_dispatch = "0.3.13"
static_assertions = "1.1.0"
thiserror = "2.0.12"

1
README.md Normal file
View file

@ -0,0 +1 @@
specification: [https://academy.cba.mit.edu/classes/networking_communications/SD/FAT.pdf](https://academy.cba.mit.edu/classes/networking_communications/SD/FAT.pdf)

15
rustfmt.toml Normal file
View file

@ -0,0 +1,15 @@
imports_granularity = "Module"
group_imports = "StdExternalCrate"
fn_call_width = 80
# Activation of features, almost objectively better ;)
use_try_shorthand = true
use_field_init_shorthand = true
# Nightly only. Will not run in CI, but please format with these locally
wrap_comments = true
normalize_comments = true
comment_width = 100
condense_wildcard_suffixes = true

618
src/bpb.rs Normal file
View file

@ -0,0 +1,618 @@
use std::fmt::Display;
use crate::FatType;
use crate::utils::{load_u16_le, load_u32_le};
#[derive(Debug)]
pub enum ExtBpb {
ExtBpb16(ExtBpb16),
ExtBpb32(ExtBpb32),
}
impl Display for ExtBpb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtBpb::ExtBpb16(ext_bpb16) => write!(f, "{}", ext_bpb16),
ExtBpb::ExtBpb32(ext_bpb32) => write!(f, "{}", ext_bpb32),
}
}
}
#[derive(Debug)]
pub struct Bpb {
fat_type: FatType,
jmp_boot: [u8; 3],
oem_name: [u8; 8],
bytes_per_sector: u16,
sectors_per_cluster: u8,
reserved_sector_count: u16,
num_fats: u8,
root_entry_count: u16,
total_sectors_16: u16,
media: u8,
fat_size_16: u16,
sectors_per_track: u16,
num_heads: u16,
hidden_sectors: u32,
total_sectors_32: u32,
ext_bpb: ExtBpb,
}
impl Display for Bpb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Bpb {{")?;
match self.fat_type {
FatType::Fat12 => writeln!(f, " FAT12")?,
FatType::Fat16 => writeln!(f, " FAT16")?,
FatType::Fat32 => writeln!(f, " FAT32")?,
}
writeln!(f, "")?;
writeln!(
f,
" jmp_boot: [{:#X}, {:#X}, {:#X}]",
self.jmp_boot[0], self.jmp_boot[1], self.jmp_boot[2]
)?;
writeln!(f, " oem name: \"{}\"", self.oem_name_str().unwrap_or(""))?;
writeln!(f, " bytes per sector: {}", self.bytes_per_sector())?;
writeln!(f, " sectors per cluster: {}", self.sectors_per_cluster())?;
writeln!(f, " reserved sector count: {}", self.reserved_sector_count())?;
writeln!(f, " num_fats: {}", self.num_fats())?;
writeln!(f, " root entry count: {}", self.root_entry_count())?;
writeln!(f, " total sectors: {}", self.total_sectors_16())?;
writeln!(f, " media: {:#X}", self.media())?;
writeln!(f, " fat_size_16: {}", self.fat_size_16())?;
writeln!(f, " sectors per track: {}", self.sectors_per_track())?;
writeln!(f, " num_heads: {}", self.num_heads())?;
writeln!(f, " hidden_sectors: {}", self.hidden_sectors())?;
writeln!(f, " total sectors 32: {}", self.total_sectors_32())?;
writeln!(f, "")?;
let ext_bpb_str = format!("{}", self.ext_bpb);
for line in ext_bpb_str.lines() {
writeln!(f, " {}", line)?;
}
writeln!(f, "}}")?;
Ok(())
}
}
impl Bpb {
pub fn load(bytes: &[u8]) -> anyhow::Result<Bpb> {
anyhow::ensure!(bytes.len() >= 512, "invalid BPB of len {}", bytes.len());
let jmp_boot = bytes[..3].try_into().unwrap();
let oem_name = bytes[3..][..8].try_into().unwrap();
let bytes_per_sector = load_u16_le(&bytes[11..][..2]);
if !&[512, 1024, 2048, 4096].contains(&bytes_per_sector) {
anyhow::bail!("invalid bytes per sector: {}", bytes_per_sector);
}
let sectors_per_cluster = bytes[13];
if !&[1, 2, 4, 8, 16, 32, 64, 128].contains(&sectors_per_cluster) {
anyhow::bail!("invalid sectors per cluster: {}", sectors_per_cluster);
}
let reserved_sector_count = load_u16_le(&bytes[14..][..2]);
anyhow::ensure!(reserved_sector_count != 0, "reserved sector count can't be zero");
let num_fats = bytes[16];
let root_entry_count = load_u16_le(&bytes[17..][..2]);
let total_sectors_16 = load_u16_le(&bytes[19..][..2]);
let media = bytes[21];
if !&[0xF0, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF].contains(&media) {
anyhow::bail!("invalid media: {}", media);
}
let fat_size_16 = load_u16_le(&bytes[22..][..2]);
let sectors_per_track = load_u16_le(&bytes[24..][..2]);
let num_heads = load_u16_le(&bytes[26..][..2]);
let hidden_sectors = load_u32_le(&bytes[28..][..4]);
let total_sectors_32 = load_u32_le(&bytes[32..][..4]);
let (fat_type, ext_bpb) = if fat_size_16 == 0 {
// FAT32?
anyhow::ensure!(
total_sectors_16 == 0,
"fat_size_16 is 0, but total sectors is {} instead of 0",
total_sectors_16
);
let ext_bpb = ExtBpb32::load(bytes)?;
(FatType::Fat32, ExtBpb::ExtBpb32(ext_bpb))
} else {
// FAT16?
let ext_bpb = ExtBpb16::load(bytes)?;
(FatType::Fat16, ExtBpb::ExtBpb16(ext_bpb))
};
let mut bpb = Bpb {
fat_type,
jmp_boot,
oem_name,
bytes_per_sector,
sectors_per_cluster,
reserved_sector_count,
num_fats,
root_entry_count,
total_sectors_16,
media,
fat_size_16,
sectors_per_track,
num_heads,
hidden_sectors,
total_sectors_32,
ext_bpb,
};
let count_of_clusters = bpb.count_of_clusters();
if count_of_clusters < 4085 {
anyhow::ensure!(
bpb.fat_type == FatType::Fat16,
"{:?} should actually be FAT12",
bpb.fat_type
);
// actually FAT12 instead of FAT16
bpb.fat_type = FatType::Fat12;
} else if count_of_clusters < 65525 {
anyhow::ensure!(
bpb.fat_type == FatType::Fat16,
"{:?} should actually be FAT16",
bpb.fat_type
);
} else {
anyhow::ensure!(
bpb.fat_type == FatType::Fat32,
"{:?} should actually be FAT32",
bpb.fat_type
);
}
Ok(bpb)
}
/// number of sectors usable for data
pub fn num_data_sectors(&self) -> u32 {
let data_sectors = self.total_sectors()
- (self.reserved_sector_count() as u32
+ (self.num_fats() as u32 * self.fat_size())
+ self.root_dir_sectors());
data_sectors
}
/// total number of clusters on this volume
pub fn num_clusters(&self) -> u32 {
self.total_sectors() / self.sectors_per_cluster() as u32
}
/// number of bytes per cluster
pub fn bytes_per_cluster(&self) -> usize {
self.sectors_per_cluster() as usize * self.bytes_per_sector() as usize
}
/// count of *data* clusters
pub fn count_of_clusters(&self) -> u32 {
self.num_data_sectors() / self.sectors_per_cluster as u32
}
/// convert a given sector to an byte offset
fn sector_to_offset(&self, sector: u32) -> u64 {
sector as u64 * self.bytes_per_sector() as u64
}
/// byte offset of the first FAT
pub fn fat_offset(&self) -> u64 {
self.sector_to_offset(self.reserved_sector_count() as u32)
}
/// FAT size in bytes
pub fn fat_len_bytes(&self) -> usize {
self.bytes_per_sector() as usize * self.fat_size() as usize
}
/// byte offset of the root directory; None for FAT32
pub fn root_directory_offset(&self) -> Option<u64> {
if self.fat_type() == FatType::Fat32 {
return None;
}
Some(self.fat_offset() + self.sector_to_offset(self.num_fats() as u32 * self.fat_size()))
}
/// number of sectors for root dir (only FAT12 and FAT16)
pub fn root_dir_sectors(&self) -> u32 {
(32 * self.root_entry_count() as u32).div_ceil(self.bytes_per_sector() as u32)
}
/// byte size of the root directory
pub fn root_dir_len_bytes(&self) -> usize {
self.root_dir_sectors() as usize * self.bytes_per_sector() as usize
}
/// first data sector
pub fn first_data_sector(&self) -> u32 {
println!("reserved sectors: {}", self.reserved_sector_count());
println!("fat sectors: {}", self.num_fats() as u32 * self.fat_size());
println!("root dir sectors: {}", self.root_dir_sectors());
self.reserved_sector_count() as u32
+ (self.num_fats() as u32 + self.fat_size())
+ self.root_dir_sectors() as u32
}
pub fn data_offset(&self) -> u64 {
// if let Some(root_dir_offset) = self.root_directory_offset() {
// // has root directory (FAT12 or FAT16)
// return root_dir_offset + self.sector_to_offset(self.root_entry_count() as u32);
// }
// self.fat_offset() + self.sector_to_offset(self.num_fats() as u32 * self.fat_size())
self.sector_to_offset(self.first_data_sector())
}
/// byte size of the data section
pub fn data_len_bytes(&self) -> usize {
self.num_data_sectors() as usize * self.bytes_per_sector() as usize
}
/// FAT type (FAT12, FAT16, or FAT32)
pub fn fat_type(&self) -> FatType {
self.fat_type
}
/// number of sectors per FAT
pub fn fat_size(&self) -> u32 {
match &self.ext_bpb {
ExtBpb::ExtBpb16(_ext_bpb16) => self.fat_size_16() as u32,
ExtBpb::ExtBpb32(ext_bpb32) => ext_bpb32.fat_size_32(),
}
}
/// get root cluster for FAT32
pub fn root_cluster(&self) -> Option<u32> {
if let ExtBpb::ExtBpb32(ext_bpb32) = &self.ext_bpb {
Some(ext_bpb32.root_cluster())
} else {
None
}
}
/// total number of sectors in this device
///
/// uses total_sectors_16 or total_sectors_32
pub fn total_sectors(&self) -> u32 {
// match &self.ext_bpb {
// ExtBpb::ExtBpb16(_) => self.total_sectors_16() as u32,
// ExtBpb::ExtBpb32(_) => self.total_sectors_32(),
// }
match self.total_sectors_16() {
0 => self.total_sectors_32(),
n => n as u32,
}
}
pub fn oem_name(&self) -> &[u8] {
&self.oem_name
}
pub fn oem_name_str(&self) -> Option<&str> {
std::str::from_utf8(&self.oem_name()).ok()
}
pub fn bytes_per_sector(&self) -> u16 {
self.bytes_per_sector
}
pub fn sectors_per_cluster(&self) -> u8 {
self.sectors_per_cluster
}
pub fn reserved_sector_count(&self) -> u16 {
self.reserved_sector_count
}
pub fn num_fats(&self) -> u8 {
self.num_fats
}
// number of 32 byte dir entries in the root directory
pub fn root_entry_count(&self) -> u16 {
self.root_entry_count
}
pub fn total_sectors_16(&self) -> u16 {
self.total_sectors_16
}
pub fn media(&self) -> u8 {
self.media
}
pub fn fat_size_16(&self) -> u16 {
self.fat_size_16
}
pub fn sectors_per_track(&self) -> u16 {
self.sectors_per_track
}
pub fn num_heads(&self) -> u16 {
self.num_heads
}
pub fn hidden_sectors(&self) -> u32 {
self.hidden_sectors
}
pub fn total_sectors_32(&self) -> u32 {
self.total_sectors_32
}
}
#[derive(Debug)]
pub struct ExtBpb16 {
drive_number: u8,
boot_sig: u8,
volume_serial_number: u32,
volume_label: [u8; 11],
file_sys_type: [u8; 8],
}
impl Display for ExtBpb16 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "ExtBpb16 {{")?;
writeln!(f, " drive number: {}", self.drive_number())?;
writeln!(f, " boot_sig: {:#x}", self.boot_sig())?;
writeln!(f, " volume serial number: {}", self.volume_serial_number())?;
writeln!(f, " volume label: {}", self.volume_label_str().unwrap_or(""))?;
writeln!(f, " file sys type: {}", self.file_sys_type_str())?;
writeln!(f, "}}")?;
Ok(())
}
}
impl ExtBpb16 {
pub fn load(bytes: &[u8]) -> anyhow::Result<ExtBpb16> {
let drive_number = bytes[36];
if !&[0x80, 0x00].contains(&drive_number) {
anyhow::bail!("invalid drive number: {}", drive_number);
}
let boot_sig = bytes[38];
let volume_serial_number = load_u32_le(&bytes[39..][..4]);
let volume_label = bytes[43..][..11].try_into().unwrap();
if volume_serial_number != 0 || volume_label != [0; 11] {
anyhow::ensure!(
boot_sig == 0x29,
"volume serial number and volume label are not both empty, but boot sig is {:#x}
instead of 0x29",
boot_sig
);
}
let file_sys_type: [u8; 8] = bytes[54..][..8].try_into().unwrap();
let Some(s) = std::str::from_utf8(&file_sys_type).ok() else {
anyhow::bail!("invalid file sys type: {:X?}", file_sys_type);
};
if !&["FAT12 ", "FAT16 ", "FAT "].contains(&s) {
anyhow::bail!("invalid file sys type: {}", s);
}
let signature_word = &bytes[510..512];
anyhow::ensure!(
signature_word == &[0x55, 0xAA],
"invalid signature word: [{:#X}, {:#X}] instead of [0x55, 0xAA]",
bytes[510],
bytes[511]
);
Ok(ExtBpb16 {
drive_number,
boot_sig,
volume_serial_number,
volume_label,
file_sys_type,
})
}
pub fn drive_number(&self) -> u8 {
self.drive_number
}
pub fn boot_sig(&self) -> u8 {
self.boot_sig
}
pub fn volume_serial_number(&self) -> u32 {
self.volume_serial_number
}
pub fn volume_label(&self) -> &[u8] {
&self.volume_label
}
pub fn volume_label_str(&self) -> Option<&str> {
std::str::from_utf8(&self.volume_label).ok()
}
pub fn file_sys_type(&self) -> &[u8] {
&self.file_sys_type
}
pub fn file_sys_type_str(&self) -> &str {
std::str::from_utf8(&self.file_sys_type).unwrap()
}
}
#[derive(Debug)]
pub struct ExtBpb32 {
fat_size_32: u32,
ext_flags: u16,
root_cluster: u32,
fs_info: u16,
bk_boot_sector: u16,
drive_number: u8,
boot_sig: u8,
volume_serial_number: u32,
volume_label: [u8; 11],
}
impl Display for ExtBpb32 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "ExtBpb32 {{")?;
writeln!(f, " fat_size_32: {}", self.fat_size_32)?;
writeln!(f, " ext_flags: {}", self.ext_flags)?;
writeln!(f, " root_cluster: {}", self.root_cluster)?;
writeln!(f, " fs_info: {}", self.fs_info)?;
writeln!(f, " bk_boot_sector: {}", self.bk_boot_sector)?;
writeln!(f, " drive_number: {}", self.drive_number)?;
writeln!(f, " boot_sig: {:#X}", self.boot_sig)?;
writeln!(f, " volume serial number: {}", self.volume_serial_number)?;
writeln!(f, " volume label: {}", self.volume_label_str().unwrap_or(""))?;
writeln!(f, "}}")?;
Ok(())
}
}
impl ExtBpb32 {
pub fn load(bytes: &[u8]) -> anyhow::Result<ExtBpb32> {
let fat_size_32 = load_u32_le(&bytes[36..][..4]);
anyhow::ensure!(fat_size_32 != 0, "fat_size_32 is zero");
let ext_flags = load_u16_le(&bytes[40..][..2]);
let fs_ver = load_u16_le(&bytes[42..][..2]);
anyhow::ensure!(fs_ver == 0x0, "invalid FSVer: {}", fs_ver);
let root_cluster = load_u32_le(&bytes[44..][..4]);
let fs_info = load_u16_le(&bytes[48..][..2]);
let bk_boot_sector = load_u16_le(&bytes[50..][..2]);
anyhow::ensure!(
&[0, 6].contains(&bk_boot_sector),
"invalid BkBootSector: {}",
bk_boot_sector
);
let reserved = &bytes[52..][..12];
anyhow::ensure!(reserved == &[0; 12], "reserved is not zeroed");
let drive_number = bytes[64];
let reserved1 = bytes[65];
anyhow::ensure!(reserved1 == 0, "reserved1 is not zeroed");
let boot_sig = bytes[66];
let volume_serial_number = load_u32_le(&bytes[67..][..4]);
let volume_label = bytes[71..][..11].try_into().unwrap();
if volume_serial_number != 0 || &volume_label != &[0; 11] {
anyhow::ensure!(
boot_sig == 0x29,
"VollID or VolLab is set, but BootSig is {} instead of 0x29",
boot_sig
);
}
let file_sys_type = &bytes[82..][..8];
anyhow::ensure!(
std::str::from_utf8(file_sys_type) == Ok("FAT32 "),
"invalid file sys type"
);
let signature_word = &bytes[510..][..2];
anyhow::ensure!(
signature_word == &[0x55, 0xAA],
"invalid signature word [{:#X}, {:#X}] instead of [0x55, 0xAA]",
signature_word[0],
signature_word[1]
);
Ok(ExtBpb32 {
fat_size_32,
ext_flags,
root_cluster,
fs_info,
bk_boot_sector,
drive_number,
boot_sig,
volume_serial_number,
volume_label,
})
}
pub fn fat_size_32(&self) -> u32 {
self.fat_size_32
}
pub fn ext_flags(&self) -> u16 {
self.ext_flags
}
pub fn root_cluster(&self) -> u32 {
self.root_cluster
}
pub fn fs_info(&self) -> u16 {
self.fs_info
}
pub fn bk_boot_sector(&self) -> u16 {
self.bk_boot_sector
}
pub fn drive_number(&self) -> u8 {
self.drive_number
}
pub fn boot_sig(&self) -> u8 {
self.boot_sig
}
pub fn volume_serial_number(&self) -> u32 {
self.volume_serial_number
}
pub fn volume_label(&self) -> &[u8] {
&self.volume_label
}
pub fn volume_label_str(&self) -> Option<&str> {
std::str::from_utf8(self.volume_label()).ok()
}
}

112
src/datetime.rs Normal file
View file

@ -0,0 +1,112 @@
use std::time::SystemTime;
use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Timelike, Utc};
#[derive(Debug)]
pub struct Date {
repr: u16,
}
impl Date {
pub fn new(repr: u16) -> anyhow::Result<Date> {
let date = Date { repr };
anyhow::ensure!(date.day() <= 31, "invalid day for date: {} (0x{:#X})", date.day(), repr);
anyhow::ensure!(
date.month() <= 12,
"invalid month for date: {} (0x{:#X})",
date.month(),
repr
);
Ok(date)
}
pub fn from_day_month_year(day: u8, month: u8, year: u16) -> anyhow::Result<Date> {
anyhow::ensure!(day <= 31, "invalid day: {}", day);
anyhow::ensure!(month <= 12, "invalid month: {}", month);
anyhow::ensure!(1980 <= year && year <= 2107, "invalid year: {}", year);
let repr = day as u16 | (month as u16) << 4 | (year - 1980) << 8;
Ok(Date { repr })
}
pub fn from_system_time(time: SystemTime) -> anyhow::Result<Date> {
let datetime: DateTime<Utc> = time.into();
let date = datetime.date_naive();
Date::from_day_month_year(
date.day() as u8,
date.month0() as u8 + 1,
date.year_ce().1 as u16,
)
}
pub fn day(&self) -> u8 {
(self.repr & 0x1F) as u8
}
pub fn month(&self) -> u8 {
((self.repr & 0x1E0) >> 5) as u8
}
pub fn year(&self) -> u16 {
((self.repr & 0xFE00) >> 9) as u16 + 1980
}
pub fn to_naive_date(&self) -> NaiveDate {
NaiveDate::from_ymd_opt(self.year() as i32, self.month() as u32, self.day() as u32).unwrap()
}
}
#[derive(Debug)]
pub struct Time {
repr: u16,
}
impl Time {
pub fn new(time: u16) -> anyhow::Result<Time> {
let time = Time { repr: time };
Ok(time)
}
pub fn from_seconds_minutes_hours(seconds: u8, minutes: u8, hours: u8) -> anyhow::Result<Time> {
anyhow::ensure!(seconds <= 58 && seconds % 2 == 0, "invalid seconds: {}", seconds);
anyhow::ensure!(minutes <= 59, "invalid minutes: {}", minutes);
anyhow::ensure!(hours <= 23, "invalid hours: {}", hours);
let repr = (seconds >> 1) as u16 | (minutes as u16) << 5 | (hours as u16) << 11;
Ok(Time { repr })
}
pub fn from_system_time(time: SystemTime) -> anyhow::Result<Time> {
let datetime: DateTime<Utc> = time.into();
let time = datetime.time();
let seconds = (time.second() as u8) & !0x01;
Time::from_seconds_minutes_hours(seconds, time.minute() as u8, time.hour() as u8)
}
pub fn second(&self) -> u8 {
2 * (self.repr & 0x1F) as u8
}
pub fn minute(&self) -> u8 {
((self.repr >> 5) & 0x3F) as u8
}
pub fn hour(&self) -> u8 {
((self.repr >> 11) & 0x1F) as u8
}
pub fn to_naive_time(&self) -> NaiveTime {
NaiveTime::from_hms_opt(self.hour() as u32, self.minute() as u32, self.second() as u32)
.unwrap()
}
}

285
src/dir.rs Normal file
View file

@ -0,0 +1,285 @@
use std::fmt::Display;
use std::io::Read;
use bitflags::bitflags;
use chrono::{NaiveDate, NaiveDateTime, TimeDelta};
use crate::datetime::{Date, Time};
use crate::utils::{load_u16_le, load_u32_le};
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Attr: u8 {
const ReadOnly = 0x01;
const Hidden = 0x02;
const System = 0x04;
const VolumeId = 0x08;
const Directory = 0x10;
const Archive = 0x20;
// const _ = !0;
// ReadOnly + Hidden + System + Volumeid
const LongName = 0x01 | 0x02 | 0x04 | 0x08;
}
}
#[derive(Debug)]
pub struct RegularDirEntry {
name: [u8; 11],
attr: Attr,
create_time_tenths: u8,
create_time: Time,
create_date: Date,
last_access_date: Date,
first_cluster: u32,
write_time: Time,
write_date: Date,
file_size: u32,
long_name: Option<String>,
}
impl Display for RegularDirEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut name = self.name_string().unwrap_or_else(|| "<unknown>".to_owned());
if self.attr.contains(Attr::Directory) {
name.push('/');
}
write!(
f,
"DirEntry {{ {: <16} created: {} modified: {} }}",
name,
self.create_time(),
self.write_time()
)?;
Ok(())
}
}
impl RegularDirEntry {
pub fn load(bytes: &[u8]) -> anyhow::Result<RegularDirEntry> {
assert_eq!(bytes.len(), 32);
let name = bytes[..11].try_into().unwrap();
let attr = Attr::from_bits_truncate(bytes[11]);
let create_time_tenths = bytes[13];
anyhow::ensure!(
create_time_tenths <= 199,
"invalid DIR_CrtTimeTenth: {}",
create_time_tenths
);
let create_time = Time::new(load_u16_le(&bytes[14..][..2]))?;
let create_date = Date::new(load_u16_le(&bytes[16..][..2]))?;
let last_access_date = Date::new(load_u16_le(&bytes[18..][..2]))?;
let write_time = Time::new(load_u16_le(&bytes[22..][..2]))?;
let write_date = Date::new(load_u16_le(&bytes[24..][..2]))?;
let file_size = load_u32_le(&bytes[28..][..4]);
let first_cluster_hi = load_u16_le(&bytes[20..][..2]);
let first_cluster_lo = load_u16_le(&bytes[26..][..2]);
let first_cluster = first_cluster_lo as u32 | ((first_cluster_hi as u32) << 16);
if attr.contains(Attr::VolumeId) {
anyhow::ensure!(
first_cluster == 0,
"DirEntry has volume id attribute set, but first cluster is {}, not zero",
first_cluster
);
}
if attr.contains(Attr::Directory) {
anyhow::ensure!(
file_size == 0,
"DirEntry has directory attribute set, but file size is {}, not zero",
file_size
)
}
Ok(RegularDirEntry {
name,
attr,
create_time_tenths,
create_time,
create_date,
last_access_date,
first_cluster,
write_time,
write_date,
file_size,
long_name: None,
})
}
/// indicates this DirEntry is empty
///
/// can be either simply empty (0xe5) or the sentinel (0x00) that indicates that all following
/// DirEntries are empty as well
pub fn is_empty(&self) -> bool {
self.name[0] == 0xe5 || self.name[0] == 0x00
}
/// indicates this and all following DisEntries are empty
pub fn is_sentinel(&self) -> bool {
self.name[0] == 0x00
}
pub fn name(&self) -> &[u8] {
&self.name
}
pub fn name_string(&self) -> Option<String> {
// std::str::from_utf8(self.name()).ok()
let name = std::str::from_utf8(&self.name[..8]).ok()?.trim_ascii_end();
let ext = std::str::from_utf8(&self.name[8..]).ok()?.trim_ascii_end();
let mut s = String::new();
if self.attr.contains(Attr::Hidden) {
s.push('.');
}
s += name;
if !ext.is_empty() {
s.push('.');
s += ext;
}
Some(s)
}
pub fn long_name(&self) -> Option<&str> {
self.long_name.as_deref()
}
pub fn set_long_name(&mut self, long_name: String) {
self.long_name = Some(long_name);
}
pub fn attr(&self) -> Attr {
self.attr
}
pub fn create_time(&self) -> NaiveDateTime {
let date = self.create_date.to_naive_date();
let time = self.create_time.to_naive_time();
let time_frac = TimeDelta::try_milliseconds(100 * self.create_time_tenths as i64).unwrap();
let time = time.overflowing_add_signed(time_frac).0;
NaiveDateTime::new(date, time)
}
pub fn last_access_date(&self) -> NaiveDate {
self.last_access_date.to_naive_date()
}
pub fn first_cluster(&self) -> u32 {
self.first_cluster
}
pub fn write_time(&self) -> NaiveDateTime {
let time = self.write_time.to_naive_time();
let date = self.write_date.to_naive_date();
NaiveDateTime::new(date, time)
}
pub fn file_size(&self) -> u32 {
self.file_size
}
}
pub struct LongNameDirEntry {}
impl LongNameDirEntry {
pub fn load(bytes: &[u8]) -> anyhow::Result<LongNameDirEntry> {
assert_eq!(bytes.len(), 32);
Ok(LongNameDirEntry {})
}
}
pub enum DirEntry {
Regular(RegularDirEntry),
LongName(LongNameDirEntry),
}
impl DirEntry {
pub fn load(bytes: &[u8]) -> anyhow::Result<DirEntry> {
assert_eq!(bytes.len(), 32);
let attr = Attr::from_bits_truncate(bytes[11]);
let dir_entry = if attr == Attr::LongName {
DirEntry::LongName(LongNameDirEntry::load(bytes)?)
} else {
DirEntry::Regular(RegularDirEntry::load(bytes)?)
};
Ok(dir_entry)
}
}
pub struct DirIter<R: Read> {
reader: R,
}
impl<R: Read> DirIter<R> {
pub fn new(reader: R) -> DirIter<R> {
DirIter { reader }
}
}
impl<R: Read> Iterator for DirIter<R> {
type Item = RegularDirEntry;
fn next(&mut self) -> Option<Self::Item> {
let mut chunk = [0; 32];
self.reader.read_exact(&mut chunk).ok()?;
// let Ok(dir_entry) = DirEntry::load(&chunk) else {
// return self.next();
// };
let dir_entry = match DirEntry::load(&chunk) {
Ok(dir_entry) => dir_entry,
Err(e) => {
// if loading fails: print error and try next entry
eprintln!("failed to load dir entry: {e}");
return self.next();
}
};
let DirEntry::Regular(dir_entry) = dir_entry else {
// simply skip long name entries for now
return self.next();
};
if dir_entry.is_sentinel() {
return None;
}
if dir_entry.is_empty() {
return self.next();
}
Some(dir_entry)
}
}

42
src/dump.rs Normal file
View file

@ -0,0 +1,42 @@
use std::io::Read;
use fat_rs::FatFs;
use fat_rs::fat::Fatty as _;
pub fn main() -> anyhow::Result<()> {
let args = std::env::args();
if args.len() != 2 {
anyhow::bail!("usage: dump <path>");
}
let file = std::fs::File::open(args.skip(1).next().unwrap())?;
// let mut buf = [0; 512];
// file.read_exact(&mut buf)?;
// let bpb = Bpb::load(&buf)?;
// println!("{}", bpb);
let mut fat_fs = FatFs::load(file)?;
println!("{}", fat_fs.bpb());
println!();
println!("{}", fat_fs.fat());
println!();
println!(
"free clusters: {} ({} bytes)",
fat_fs.fat().count_free_clusters(),
fat_fs.fat().count_free_clusters()
* fat_fs.bpb().bytes_per_sector() as usize
* fat_fs.bpb().sectors_per_cluster() as usize
);
for dir_entry in fat_fs.root_dir_iter() {
println!("{}", dir_entry);
}
Ok(())
}

376
src/fat.rs Normal file
View file

@ -0,0 +1,376 @@
use std::fmt::Display;
use std::mem::MaybeUninit;
use std::ops::RangeInclusive;
use enum_dispatch::enum_dispatch;
use crate::FatType;
#[derive(Debug, thiserror::Error)]
pub enum FatError {
#[error("can't get next cluster of free cluster")]
FreeCluster,
#[error("cluster {0} is reserved")]
ReservedCluster(u32),
#[error("cluster is defective")]
DefectiveCluster,
#[error("invalid next cluster 0x{0:0X}")]
InvalidEntry(u32),
}
#[enum_dispatch]
pub trait Fatty {
// get the next cluster
// assumes the cluster is valid, i.e. allocated
fn get_entry(&self, cluster: u32) -> u32;
fn get_valid_clusters(&self) -> RangeInclusive<u32>;
fn get_reserved_clusters(&self) -> RangeInclusive<u32>;
fn get_defective_cluster(&self) -> u32;
fn get_reserved_eof_clusters(&self) -> RangeInclusive<u32>;
fn get_eof_cluster(&self) -> u32;
fn count_free_clusters(&self) -> usize {
self.get_valid_clusters()
.map(|cluster| self.get_entry(cluster))
.filter(|&entry| entry == 0)
.count()
}
}
#[enum_dispatch(Fatty)]
pub enum Fat {
Fat12(Fat12),
Fat16(Fat16),
Fat32(Fat32),
}
impl Display for Fat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
Fat::Fat12(fat12) => write!(f, "{}", fat12),
Fat::Fat16(fat16) => write!(f, "{}", fat16),
Fat::Fat32(fat32) => write!(f, "{}", fat32),
}
}
}
impl Fat {
pub fn new(fat_type: FatType, bytes: &[u8], max: u32) -> Fat {
match fat_type {
FatType::Fat12 => Fat::Fat12(Fat12::new(bytes, max)),
FatType::Fat16 => Fat::Fat16(Fat16::new(bytes, max)),
FatType::Fat32 => Fat::Fat32(Fat32::new(bytes, max)),
}
}
pub fn get_next_cluster(&self, cluster: u32) -> Result<Option<u32>, FatError> {
if cluster == 0x000 {
// can't get next cluster for free cluster
return Err(FatError::FreeCluster);
}
if self.get_reserved_clusters().contains(&cluster) {
// can't get next cluster for reserved cluster
return Err(FatError::ReservedCluster(cluster));
}
// defective cluster
if cluster == self.get_defective_cluster() {
// can't get next cluster for defective cluster
return Err(FatError::DefectiveCluster);
}
if self.get_reserved_eof_clusters().contains(&cluster) {
// Reserved and should not be used. May be interpreted as an allocated cluster and the
// final cluster in the file (indicating end-of-file condition).
//
// can't get next entry for reserved cluster
// return Ok(None);
return Err(FatError::ReservedCluster(cluster));
}
let entry = self.get_entry(cluster);
// interpret second reserved block as EOF here
if entry == self.get_eof_cluster() || self.get_reserved_eof_clusters().contains(&entry) {
return Ok(None);
}
// entry should be in the valid cluster range here; otherwise something went wrong
if !self.get_valid_clusters().contains(&entry) {
return Err(FatError::InvalidEntry(entry));
}
Ok(Some(entry))
}
}
pub struct Fat12 {
max: u32,
next_sectors: Box<[u16]>,
}
impl Display for Fat12 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Fat 12 {{")?;
for (i, &x) in self.next_sectors.iter().enumerate() {
if x != 0 {
writeln!(f, " 0x{:03X} -> 0x{:03X}", i, x)?;
}
}
writeln!(f, "}}")?;
Ok(())
}
}
impl Fat12 {
pub fn new(bytes: &[u8], max: u32) -> Fat12 {
// for FAT12 max is always less than 4085
assert!(max <= 4085);
let mut next_sectors: Box<[MaybeUninit<u16>]> = Box::new_uninit_slice(max as usize + 1);
for elem in next_sectors.iter_mut() {
elem.write(0);
}
let mut next_sectors = unsafe { next_sectors.assume_init() };
// assume bytes.len() is multiple of 3
// TODO: fix later
assert_eq!(bytes.len() % 3, 0);
let (chunks, rem) = bytes.as_chunks::<3>();
assert_eq!(rem.len(), 0);
// TODO: correctly handle cases where max is larger than #FAT entries and v.v.
for (idx, triple) in chunks.iter().take(max as usize / 2).enumerate() {
// first (even) entry gets truncated
let first = u16::from_le_bytes(triple[..2].try_into().unwrap()) & 0xFFF;
// second (odd) entry gets shifted
let second = u16::from_le_bytes(triple[1..].try_into().unwrap()) >> 4;
assert!(idx + 1 < next_sectors.len());
next_sectors[2 * idx] = first;
next_sectors[2 * idx + 1] = second;
}
if max % 2 == 1 {
// odd max: need to fix last cluster
let idx = max as usize / 2 + 1;
let triple = chunks[idx];
let first = u16::from_le_bytes(triple[..2].try_into().unwrap()) & 0xFFF;
next_sectors[2 * idx] = first;
}
Fat12 { max, next_sectors }
}
}
impl Fatty for Fat12 {
fn get_entry(&self, cluster: u32) -> u32 {
let cluster = cluster as usize;
assert!(cluster < self.next_sectors.len());
self.next_sectors[cluster] as u32
}
fn get_valid_clusters(&self) -> RangeInclusive<u32> {
2..=self.max
}
fn get_reserved_clusters(&self) -> RangeInclusive<u32> {
(self.max as u32 + 1)..=0xFF6
}
fn get_defective_cluster(&self) -> u32 {
0xFF7
}
fn get_reserved_eof_clusters(&self) -> RangeInclusive<u32> {
0xFF8..=0xFFE
}
fn get_eof_cluster(&self) -> u32 {
0xFFF
}
}
pub struct Fat16 {
max: u32,
next_sectors: Box<[u16]>,
}
impl Display for Fat16 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Fat 16 {{")?;
for (i, &x) in self.next_sectors.iter().enumerate() {
if x != 0 {
writeln!(f, " 0x{:03X} -> 0x{:03X}", i, x)?;
}
}
writeln!(f, "}}")?;
Ok(())
}
}
impl Fat16 {
pub fn new(bytes: &[u8], max: u32) -> Fat16 {
// for FAT12 max is always less than 4085
assert!(4085 < max && max <= 65525);
let mut next_sectors: Box<[MaybeUninit<u16>]> = Box::new_uninit_slice(max as usize + 1);
for elem in next_sectors.iter_mut() {
elem.write(0);
}
let mut next_sectors = unsafe { next_sectors.assume_init() };
// assume bytes.len() is multiple of 2
// TODO: fix later
assert_eq!(bytes.len() % 2, 0);
let (chunks, rem) = bytes.as_chunks::<2>();
assert_eq!(rem.len(), 0);
// TODO: correctly handle cases where max is larger than #FAT entries and v.v.
for (idx, chunk) in chunks.iter().take(max as usize / 2).enumerate() {
// first (even) entry gets truncated
let entry = u16::from_le_bytes(chunk[..2].try_into().unwrap());
next_sectors[idx] = entry;
}
Fat16 { max, next_sectors }
}
}
impl Fatty for Fat16 {
fn get_entry(&self, cluster: u32) -> u32 {
let cluster = cluster as usize;
assert!(cluster < self.next_sectors.len());
self.next_sectors[cluster] as u32
}
fn get_valid_clusters(&self) -> RangeInclusive<u32> {
2..=self.max
}
fn get_reserved_clusters(&self) -> RangeInclusive<u32> {
(self.max as u32 + 1)..=0xFFF6
}
fn get_defective_cluster(&self) -> u32 {
0xFFF7
}
fn get_reserved_eof_clusters(&self) -> RangeInclusive<u32> {
0xFFF8..=0xFFFE
}
fn get_eof_cluster(&self) -> u32 {
0xFFFF
}
}
pub struct Fat32 {
max: u32,
next_sectors: Box<[u32]>,
}
impl Display for Fat32 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Fat 32 {{")?;
for (i, &x) in self.next_sectors.iter().enumerate() {
if x != 0 {
writeln!(f, " 0x{:03X} -> 0x{:03X}", i, x)?;
}
}
writeln!(f, "}}")?;
Ok(())
}
}
impl Fat32 {
pub fn new(bytes: &[u8], max: u32) -> Fat32 {
// for FAT12 max is always less than 4085
assert!(65525 < max);
let mut next_sectors: Box<[MaybeUninit<u32>]> = Box::new_uninit_slice(max as usize + 1);
for elem in next_sectors.iter_mut() {
elem.write(0);
}
let mut next_sectors = unsafe { next_sectors.assume_init() };
// assume bytes.len() is multiple of 4
// TODO: fix later
assert_eq!(bytes.len() % 4, 0);
let (chunks, rem) = bytes.as_chunks::<4>();
assert_eq!(rem.len(), 0);
// TODO: correctly handle cases where max is larger than #FAT entries and v.v.
for (idx, chunk) in chunks.iter().take(max as usize / 2).enumerate() {
// first (even) entry gets truncated
let entry = u32::from_le_bytes(chunk[..4].try_into().unwrap());
next_sectors[idx] = entry;
}
Fat32 { max, next_sectors }
}
}
impl Fatty for Fat32 {
fn get_entry(&self, cluster: u32) -> u32 {
let cluster = cluster as usize;
assert!(cluster < self.next_sectors.len());
self.next_sectors[cluster] as u32
}
fn get_valid_clusters(&self) -> RangeInclusive<u32> {
2..=self.max
}
fn get_reserved_clusters(&self) -> RangeInclusive<u32> {
(self.max + 1)..=0xFFFFFFF6
}
fn get_defective_cluster(&self) -> u32 {
0xFFFFFFF7
}
fn get_reserved_eof_clusters(&self) -> RangeInclusive<u32> {
0xFFFFFFF8..=0xFFFFFFFE
}
fn get_eof_cluster(&self) -> u32 {
0xFFFFFFFF
}
}

42
src/fs_info.rs Normal file
View file

@ -0,0 +1,42 @@
use crate::utils::load_u32_le;
pub struct FsInfo {
free_count: u32,
next_free: u32,
}
impl FsInfo {
pub fn load(bytes: &[u8]) -> anyhow::Result<FsInfo> {
let lead_sig = load_u32_le(&bytes[..4]);
anyhow::ensure!(
lead_sig == 0x41615252,
"invalid lead signature: 0x{:#08X} instead of 0x41615252",
lead_sig
);
let struct_sig = load_u32_le(&bytes[484..][..4]);
anyhow::ensure!(
struct_sig == 0x61417272,
"invalid structural signature: 0x{:#08X} instead of 0x61417272",
struct_sig
);
let trail_sig = load_u32_le(&bytes[508..][..4]);
anyhow::ensure!(
trail_sig == 0xAA550000,
"invalid trailing signature: 0x{:#08X} instead of 0xAA550000",
trail_sig
);
let free_count = load_u32_le(&bytes[488..][..4]);
let next_free = load_u32_le(&bytes[492..][..4]);
Ok(FsInfo {
free_count,
next_free,
})
}
}

53
src/iter.rs Normal file
View file

@ -0,0 +1,53 @@
use std::io::Read;
use crate::fat::Fatty;
use crate::subslice::SubSlice;
use crate::utils::replace;
use crate::{FatFs, SliceLike};
pub struct ClusterChainReader<'a, S: SliceLike> {
sub_slice: SubSlice<'a, S>,
next_cluster: Option<u32>,
}
impl<'a, S: SliceLike> ClusterChainReader<'a, S> {
pub fn new(fat_fs: &'a mut FatFs<S>, first_cluster: u32) -> ClusterChainReader<'a, S> {
let next_cluster = fat_fs.next_cluster(first_cluster).unwrap_or(None);
let sub_slice = fat_fs.cluster_as_subslice(first_cluster);
ClusterChainReader {
sub_slice,
next_cluster,
}
}
fn next_cluster(&mut self) -> bool {
let Some(next_cluster) = self.next_cluster else {
return false;
};
replace(&mut self.sub_slice, |sub_slice| {
let fat_fs = sub_slice.release();
self.next_cluster = fat_fs.next_cluster(next_cluster).unwrap_or(None);
fat_fs.cluster_as_subslice(next_cluster)
});
true
}
}
impl<'a, S: SliceLike> Read for ClusterChainReader<'a, S> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.sub_slice.is_empty() {
if !self.next_cluster() {
return Ok(0);
}
}
self.sub_slice.read(buf)
}
}

View file

@ -1,14 +1,251 @@
pub fn add(left: u64, right: u64) -> u64 { use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
left + right
use crate::dir::{DirIter, RegularDirEntry};
use crate::fat::{FatError, Fatty};
use crate::subslice::{SubSlice, SubSliceMut};
pub mod bpb;
mod datetime;
pub mod dir;
pub mod fat;
pub mod fs_info;
mod iter;
mod subslice;
mod utils;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FatType {
Fat12,
Fat16,
Fat32,
} }
#[cfg(test)] pub trait SliceLike {
mod tests { fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()>;
use super::*;
#[test] fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()>;
fn it_works() {
let result = add(2, 2); fn read_array_at_offset<const N: usize>(&mut self, offset: u64) -> std::io::Result<[u8; N]> {
assert_eq!(result, 4); let mut buf = [0; N];
self.read_at_offset(offset, &mut buf)?;
Ok(buf)
}
}
impl SliceLike for &mut [u8] {
fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
// anyhow::ensure!(
// offset as usize + buf.len() <= self.len(),
// "reading {} bytes at offset {} is out of bounds for slice of len {}",
// buf.len(),
// offset,
// self.len()
// );
if offset as usize + buf.len() > self.len() {
return Err(std::io::Error::other(anyhow::anyhow!(
"reading {} bytes at offset {} is out of bounds for slice of len {}",
buf.len(),
offset,
self.len()
)));
}
buf.copy_from_slice(&self[offset as usize..][..buf.len()]);
Ok(())
}
fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> {
if offset as usize + bytes.len() > self.len() {
return Err(std::io::Error::other(anyhow::anyhow!(
"writing {} bytes at offset {} is out of bounds for slice of len {}",
bytes.len(),
offset,
self.len()
)));
}
self[offset as usize..][..bytes.len()].copy_from_slice(bytes);
Ok(())
}
}
impl SliceLike for std::fs::File {
fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
self.seek(SeekFrom::Start(offset))?;
self.read_exact(buf)?;
Ok(())
}
fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> {
self.seek(SeekFrom::Start(offset))?;
self.write_all(bytes)?;
Ok(())
}
}
#[allow(dead_code)]
pub struct FatFs<S: SliceLike> {
data: S,
fat_offset: u64,
fat_size: usize,
root_dir_offset: Option<u64>,
root_dir_size: usize,
pub data_offset: u64,
data_size: usize,
bytes_per_cluster: usize,
bpb: bpb::Bpb,
fat: fat::Fat,
}
impl<S: SliceLike> FatFs<S> {
pub fn load(mut data: S) -> anyhow::Result<FatFs<S>> {
let bpb_bytes: [u8; 512] = data.read_array_at_offset(0)?;
let bpb = bpb::Bpb::load(&bpb_bytes)?;
let mut fat_buf = vec![0; bpb.fat_len_bytes()];
data.read_at_offset(bpb.fat_offset(), &mut fat_buf)?;
let fat = fat::Fat::new(bpb.fat_type(), &fat_buf, bpb.count_of_clusters());
// {
// let eof = fat.get_eof_cluster();
// if fat.get_entry(0) != (eof & !0xFF) | bpb.media() as u32 {
// eprintln!("warning: first sector entry should have media in lowest byte");
// }
// if fat.get_entry(1) != eof {
// eprintln!(
// "warning: second sector entry should be EOF, not {:#X}",
// fat.get_entry(1)
// );
// }
// }
let fat_offset = bpb.fat_offset();
let fat_size = bpb.fat_len_bytes();
let root_dir_offset = bpb.root_directory_offset();
let root_dir_size = bpb.root_dir_len_bytes();
let data_offset = bpb.data_offset();
let data_size = bpb.data_len_bytes();
let bytes_per_cluster = bpb.bytes_per_cluster();
Ok(FatFs {
data,
fat_offset,
fat_size,
root_dir_offset,
root_dir_size,
data_offset,
data_size,
bytes_per_cluster,
bpb,
fat,
})
}
pub fn bpb(&self) -> &bpb::Bpb {
&self.bpb
}
pub fn fat(&self) -> &fat::Fat {
&self.fat
}
/// byte offset of data cluster
pub fn data_cluster_to_offset(&self, cluster: u32) -> u64 {
// assert!(cluster >= 2);
assert!(self.fat().get_valid_clusters().contains(&cluster));
self.data_offset + (cluster - 2) as u64 * self.bytes_per_cluster as u64
}
/// next data cluster or None is cluster is EOF
///
/// giving an invalid cluster (free, reserved, or defective) returns an appropriate error
pub fn next_cluster(&self, cluster: u32) -> Result<Option<u32>, FatError> {
self.fat().get_next_cluster(cluster)
}
pub fn cluster_as_subslice_mut(&mut self, cluster: u32) -> SubSliceMut<'_, S> {
let offset = self.data_cluster_to_offset(cluster);
SubSliceMut::new(self, offset, self.bytes_per_cluster)
}
pub fn cluster_as_subslice(&mut self, cluster: u32) -> SubSlice<'_, S> {
let offset = self.data_cluster_to_offset(cluster);
SubSlice::new(self, offset, self.bytes_per_cluster)
}
pub fn root_dir_bytes(&mut self) -> std::io::Result<Vec<u8>> {
if let Some(root_dir_offset) = self.root_dir_offset {
let mut data = Vec::new();
let mut subslice = SubSliceMut::new(self, root_dir_offset, self.root_dir_size);
subslice.read_to_end(&mut data)?;
return Ok(data);
}
let mut cluster = self.bpb().root_cluster().unwrap();
let mut data = vec![0; self.bytes_per_cluster];
self.data
.read_at_offset(self.data_cluster_to_offset(cluster), &mut data)?;
while let Ok(Some(next_cluster)) = self.next_cluster(cluster) {
cluster = next_cluster;
self.data
.read_at_offset(self.data_cluster_to_offset(cluster), &mut data)?;
}
Ok(data)
}
pub fn root_dir_iter(&mut self) -> Box<dyn Iterator<Item = RegularDirEntry> + '_> {
// TODO: maybe wrap this in another RootDirIter enum, so we don't have to Box<dyn>
if let Some(root_dir_offset) = self.root_dir_offset {
// FAT12/FAT16
let sub_slice = SubSlice::new(self, root_dir_offset, self.root_dir_size);
return Box::new(DirIter::new(sub_slice));
}
// FAT32
// can't fail; we're in the FAT32 case
let root_cluster = self.bpb().root_cluster().unwrap();
let cluster_iter = iter::ClusterChainReader::new(self, root_cluster);
Box::new(DirIter::new(cluster_iter))
} }
} }

117
src/subslice.rs Normal file
View file

@ -0,0 +1,117 @@
use std::io::{Read, Write};
use crate::{FatFs, SliceLike};
pub struct SubSliceMut<'a, S: SliceLike> {
fat_fs: &'a mut FatFs<S>,
offset: u64,
len: usize,
}
impl<S: SliceLike> SubSliceMut<'_, S> {
pub fn new(fat_fs: &mut FatFs<S>, offset: u64, len: usize) -> SubSliceMut<'_, S> {
SubSliceMut {
fat_fs,
offset,
len,
}
}
}
impl<S: SliceLike> SubSliceMut<'_, S> {
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl<S: SliceLike> Read for SubSliceMut<'_, S> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let bytes_to_read = self.len.min(buf.len());
self.fat_fs
.data
.read_at_offset(self.offset, &mut buf[..bytes_to_read])?;
self.offset += bytes_to_read as u64;
self.len -= bytes_to_read;
Ok(bytes_to_read)
}
}
impl<S: SliceLike> Write for SubSliceMut<'_, S> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let bytes_to_write = self.len.min(buf.len());
self.fat_fs
.data
.write_at_offset(self.offset, &buf[..bytes_to_write])?;
self.offset += bytes_to_write as u64;
self.len -= bytes_to_write;
Ok(bytes_to_write)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub struct SubSlice<'a, S: SliceLike> {
fat_fs: &'a mut FatFs<S>,
offset: u64,
len: usize,
}
impl<S: SliceLike> SubSlice<'_, S> {
pub fn new(fat_fs: &mut FatFs<S>, offset: u64, len: usize) -> SubSlice<'_, S> {
SubSlice {
fat_fs,
offset,
len,
}
}
pub fn fat_fs(&self) -> &FatFs<S> {
self.fat_fs
}
pub fn fat_fs_mut(&self) -> &FatFs<S> {
self.fat_fs
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn len(&self) -> usize {
self.len
}
}
impl<'a, S: SliceLike> SubSlice<'a, S> {
pub fn release(self) -> &'a mut FatFs<S> {
self.fat_fs
}
}
impl<S: SliceLike> Read for SubSlice<'_, S> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let bytes_to_read = self.len.min(buf.len());
self.fat_fs
.data
.read_at_offset(self.offset, &mut buf[..bytes_to_read])?;
self.offset += bytes_to_read as u64;
self.len -= bytes_to_read;
Ok(bytes_to_read)
}
}

26
src/utils.rs Normal file
View file

@ -0,0 +1,26 @@
pub fn load_u16_le(bytes: &[u8]) -> u16 {
assert_eq!(bytes.len(), 2);
u16::from_le_bytes(bytes.try_into().unwrap())
}
pub fn load_u32_le(bytes: &[u8]) -> u32 {
assert_eq!(bytes.len(), 4);
u32::from_le_bytes(bytes.try_into().unwrap())
}
/// replace the value at x with f(x)
///
/// SAFETY:
/// should be safe, I guess? MIRI didn't complain about it
pub fn replace<T>(x: &mut T, f: impl FnOnce(T) -> T) {
unsafe {
let x_ptr = x as *mut T;
let old_x = std::ptr::read(x_ptr);
let new_x = f(old_x);
std::ptr::write(x_ptr, new_x);
}
}