use std::io::{Read, Seek, SeekFrom};
use crate::tree::TreeNode;
#[derive(Debug)]
pub enum Error {
TooShort,
BadBootSector,
Io(std::io::Error),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::TooShort => write!(f, "image too short for a FAT boot sector"),
Error::BadBootSector => write!(f, "invalid FAT BPB / boot sector"),
Error::Io(e) => write!(f, "FAT I/O: {e}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
if let Error::Io(e) = self {
Some(e)
} else {
None
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FatType {
Fat12,
Fat16,
Fat32,
}
fn is_eoc(fat_type: FatType, cluster: u32) -> bool {
match fat_type {
FatType::Fat12 => cluster >= 0x0FF8,
FatType::Fat16 => cluster >= 0xFFF8,
FatType::Fat32 => (cluster & 0x0FFF_FFFF) >= 0x0FFF_FFF8,
}
}
fn is_bad_cluster(fat_type: FatType, cluster: u32) -> bool {
match fat_type {
FatType::Fat12 => cluster == 0x0FF7,
FatType::Fat16 => cluster == 0xFFF7,
FatType::Fat32 => (cluster & 0x0FFF_FFFF) == 0x0FFF_FFF7,
}
}
struct Context {
base_offset: u64,
fat_type: FatType,
bytes_per_cluster: u64,
fat_start_rel: u64,
root_dir_rel: u64,
root_dir_size_bytes: u64,
data_start_rel: u64,
root_cluster: u32,
total_clusters: u32,
}
impl Context {
fn cluster_abs(&self, cluster: u32) -> u64 {
self.base_offset + self.data_start_rel + (cluster as u64 - 2) * self.bytes_per_cluster
}
fn fat_entry<R: Read + Seek>(&self, file: &mut R, cluster: u32) -> Result<u32, Error> {
let fat_abs = self.base_offset + self.fat_start_rel;
match self.fat_type {
FatType::Fat12 => {
let byte_off = cluster as u64 + cluster as u64 / 2;
file.seek(SeekFrom::Start(fat_abs + byte_off))?;
let mut buf = [0u8; 2];
file.read_exact(&mut buf)?;
let word = u16::from_le_bytes(buf) as u32;
Ok(if cluster & 1 == 0 {
word & 0x0FFF
} else {
word >> 4
})
}
FatType::Fat16 => {
file.seek(SeekFrom::Start(fat_abs + cluster as u64 * 2))?;
let mut buf = [0u8; 2];
file.read_exact(&mut buf)?;
Ok(u16::from_le_bytes(buf) as u32)
}
FatType::Fat32 => {
file.seek(SeekFrom::Start(fat_abs + cluster as u64 * 4))?;
let mut buf = [0u8; 4];
file.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf) & 0x0FFF_FFFF)
}
}
}
fn cluster_chain<R: Read + Seek>(&self, file: &mut R, start: u32) -> Result<Vec<u32>, Error> {
let mut chain = Vec::new();
let mut cluster = start;
let max_valid = self.total_clusters.saturating_add(1);
loop {
if cluster < 2 || cluster > max_valid {
break;
}
if is_eoc(self.fat_type, cluster) || is_bad_cluster(self.fat_type, cluster) {
break;
}
chain.push(cluster);
if chain.len() > self.total_clusters as usize {
break;
}
cluster = self.fat_entry(file, cluster)?;
}
Ok(chain)
}
}
fn read_bpb<R: Read + Seek>(file: &mut R) -> Result<Context, Error> {
let base_offset = file.stream_position()?;
let mut sector = [0u8; 512];
let mut filled = 0usize;
while filled < 512 {
match file.read(&mut sector[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(Error::Io(e)),
}
}
if filled < 512 {
return Err(Error::TooShort);
}
if sector[510] != 0x55 || sector[511] != 0xAA {
return Err(Error::BadBootSector);
}
let bytes_per_sector = u16::from_le_bytes([sector[11], sector[12]]);
let sectors_per_cluster = sector[13];
let reserved_sectors = u16::from_le_bytes([sector[14], sector[15]]);
let num_fats = sector[16];
let root_entry_count = u16::from_le_bytes([sector[17], sector[18]]);
let total_sectors_16 = u16::from_le_bytes([sector[19], sector[20]]);
let fat_size_16 = u16::from_le_bytes([sector[22], sector[23]]);
let total_sectors_32 = u32::from_le_bytes([sector[32], sector[33], sector[34], sector[35]]);
let fat_size_32 = u32::from_le_bytes([sector[36], sector[37], sector[38], sector[39]]);
let root_cluster_32 = u32::from_le_bytes([sector[44], sector[45], sector[46], sector[47]]);
if !matches!(bytes_per_sector, 512 | 1024 | 2048 | 4096) {
return Err(Error::BadBootSector);
}
if sectors_per_cluster == 0 || !sectors_per_cluster.is_power_of_two() {
return Err(Error::BadBootSector);
}
if num_fats == 0 || num_fats > 2 {
return Err(Error::BadBootSector);
}
let bps = bytes_per_sector as u64;
let spc = sectors_per_cluster as u64;
let fat_size = if fat_size_16 != 0 {
fat_size_16 as u64
} else {
fat_size_32 as u64
};
let total_sectors = if total_sectors_16 != 0 {
total_sectors_16 as u64
} else {
total_sectors_32 as u64
};
if fat_size == 0 || total_sectors == 0 {
return Err(Error::BadBootSector);
}
let fat_start_rel = reserved_sectors as u64 * bps;
let root_dir_rel = fat_start_rel + num_fats as u64 * fat_size * bps;
let root_dir_entry_bytes = root_entry_count as u64 * 32;
let root_dir_sectors = root_dir_entry_bytes.div_ceil(bps);
let data_start_rel = root_dir_rel + root_dir_sectors * bps;
let data_sectors = total_sectors
.saturating_sub(reserved_sectors as u64 + num_fats as u64 * fat_size + root_dir_sectors);
let total_clusters = (data_sectors / spc) as u32;
let fat_type = if root_entry_count == 0 && fat_size_16 == 0 && root_cluster_32 >= 2 {
FatType::Fat32
} else if total_clusters < 4085 {
FatType::Fat12
} else if total_clusters < 65525 {
FatType::Fat16
} else {
FatType::Fat32
};
let root_cluster = if fat_type == FatType::Fat32 {
if root_cluster_32 < 2 {
return Err(Error::BadBootSector);
}
root_cluster_32
} else {
0 };
Ok(Context {
base_offset,
fat_type,
bytes_per_cluster: spc * bps,
fat_start_rel,
root_dir_rel,
root_dir_size_bytes: root_dir_entry_bytes,
data_start_rel,
root_cluster,
total_clusters,
})
}
fn read_dir_bytes<R: Read + Seek>(
ctx: &Context,
file: &mut R,
start_cluster: u32,
) -> Result<Vec<u8>, Error> {
if start_cluster == 0 {
file.seek(SeekFrom::Start(ctx.base_offset + ctx.root_dir_rel))?;
let mut buf = vec![0u8; ctx.root_dir_size_bytes as usize];
file.read_exact(&mut buf)?;
return Ok(buf);
}
let chain = ctx.cluster_chain(file, start_cluster)?;
let mut buf = Vec::with_capacity(chain.len() * ctx.bytes_per_cluster as usize);
for &cluster in &chain {
file.seek(SeekFrom::Start(ctx.cluster_abs(cluster)))?;
let start = buf.len();
buf.resize(start + ctx.bytes_per_cluster as usize, 0);
file.read_exact(&mut buf[start..])?;
}
Ok(buf)
}
fn lfn_chars(entry: &[u8]) -> [u16; 13] {
let mut chars = [0xFFFF_u16; 13];
let fields: &[(usize, usize)] = &[(1, 5), (14, 6), (28, 2)];
let mut idx = 0;
for &(start, count) in fields {
for j in 0..count {
let off = start + j * 2;
chars[idx] = u16::from_le_bytes([entry[off], entry[off + 1]]);
idx += 1;
}
}
chars
}
fn reassemble_lfn(pieces: &[(u8, [u16; 13])]) -> String {
let mut sorted: Vec<_> = pieces.to_vec();
sorted.sort_by_key(|(seq, _)| *seq);
let chars: Vec<u16> = sorted
.iter()
.flat_map(|(_, c)| c.iter().copied())
.take_while(|&c| c != 0x0000)
.filter(|&c| c != 0xFFFF)
.collect();
String::from_utf16_lossy(&chars).to_string()
}
fn short_name_83(entry: &[u8]) -> String {
let mut name_bytes = [0u8; 8];
name_bytes.copy_from_slice(&entry[..8]);
if name_bytes[0] == 0x05 {
name_bytes[0] = 0xE5;
}
let name = String::from_utf8_lossy(&name_bytes);
let name = name.trim_end_matches(' ');
let ext = String::from_utf8_lossy(&entry[8..11]);
let ext = ext.trim_end_matches(' ');
if ext.is_empty() {
name.to_string()
} else {
format!("{name}.{ext}")
}
}
struct RawEntry {
name: String,
is_dir: bool,
file_size: u32,
start_cluster: u32,
}
fn parse_dir_entries(dir_bytes: &[u8]) -> Vec<RawEntry> {
const ATTR_VOLUME_ID: u8 = 0x08;
const ATTR_DIRECTORY: u8 = 0x10;
const ATTR_LONG_NAME: u8 = 0x0F;
let mut out = Vec::new();
let mut lfn_pieces: Vec<(u8, [u16; 13])> = Vec::new();
for chunk in dir_bytes.chunks_exact(32) {
let first = chunk[0];
if first == 0x00 {
break; }
if first == 0xE5 {
lfn_pieces.clear(); continue;
}
let attr = chunk[11];
if attr & ATTR_LONG_NAME == ATTR_LONG_NAME && attr & ATTR_DIRECTORY == 0 {
let seq = chunk[0] & 0x3F;
lfn_pieces.push((seq, lfn_chars(chunk)));
continue;
}
if attr & ATTR_VOLUME_ID != 0 && attr & ATTR_DIRECTORY == 0 {
lfn_pieces.clear();
continue;
}
let name = if !lfn_pieces.is_empty() {
let n = reassemble_lfn(&lfn_pieces);
lfn_pieces.clear();
n
} else {
short_name_83(chunk)
};
if name == "." || name == ".." {
continue;
}
let is_dir = attr & ATTR_DIRECTORY != 0;
let file_size = u32::from_le_bytes([chunk[28], chunk[29], chunk[30], chunk[31]]);
let cluster_hi = u16::from_le_bytes([chunk[20], chunk[21]]) as u32;
let cluster_lo = u16::from_le_bytes([chunk[26], chunk[27]]) as u32;
let start_cluster = (cluster_hi << 16) | cluster_lo;
out.push(RawEntry {
name,
is_dir,
file_size,
start_cluster,
});
}
out
}
fn is_contiguous(chain: &[u32]) -> bool {
chain.windows(2).all(|w| w[1] == w[0] + 1)
}
fn build_tree<R: Read + Seek>(
ctx: &Context,
file: &mut R,
start_cluster: u32,
depth: u32,
) -> Result<Vec<TreeNode>, Error> {
if depth > 32 {
return Ok(Vec::new());
}
let dir_bytes = read_dir_bytes(ctx, file, start_cluster)?;
let entries = parse_dir_entries(&dir_bytes);
let mut nodes = Vec::with_capacity(entries.len());
for entry in entries {
if entry.is_dir {
let mut dir_node = TreeNode::new_directory(entry.name);
let children = if entry.start_cluster >= 2 {
build_tree(ctx, file, entry.start_cluster, depth + 1)?
} else {
Vec::new()
};
for child in children {
dir_node.add_child(child);
}
nodes.push(dir_node);
} else {
let node = if entry.start_cluster >= 2 && entry.file_size > 0 {
let chain = ctx.cluster_chain(file, entry.start_cluster)?;
let required_clusters =
(entry.file_size as u64).div_ceil(ctx.bytes_per_cluster) as usize;
if !chain.is_empty() && is_contiguous(&chain) && chain.len() >= required_clusters {
TreeNode::new_file_with_location(
entry.name,
entry.file_size as u64,
ctx.cluster_abs(chain[0]),
entry.file_size as u64,
)
} else {
TreeNode::new_file(entry.name, entry.file_size as u64)
}
} else {
TreeNode::new_file(entry.name, entry.file_size as u64)
};
nodes.push(node);
}
}
Ok(nodes)
}
pub fn detect<R: Read + Seek>(file: &mut R) -> bool {
let saved = match file.stream_position() {
Ok(p) => p,
Err(_) => return false,
};
let ok = read_bpb(file).is_ok();
let _ = file.seek(SeekFrom::Start(saved));
ok
}
pub fn detect_and_parse<R: Read + Seek>(file: &mut R) -> Result<TreeNode, Error> {
let ctx = read_bpb(file)?;
let root_cluster = match ctx.fat_type {
FatType::Fat12 | FatType::Fat16 => 0, FatType::Fat32 => ctx.root_cluster,
};
let mut root = TreeNode::new_directory("/".to_string());
let children = build_tree(&ctx, file, root_cluster, 0)?;
for child in children {
root.add_child(child);
}
root.calculate_directory_size();
Ok(root)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn make_fat12_image() -> Vec<u8> {
let mut img = vec![0u8; 512 * 6];
img[11..13].copy_from_slice(&512u16.to_le_bytes()); img[13] = 1; img[14..16].copy_from_slice(&1u16.to_le_bytes()); img[16] = 2; img[17..19].copy_from_slice(&16u16.to_le_bytes()); img[19..21].copy_from_slice(&6u16.to_le_bytes()); img[21] = 0xF8; img[22..24].copy_from_slice(&1u16.to_le_bytes()); img[510] = 0x55;
img[511] = 0xAA;
let f1 = 512usize;
img[f1] = 0xF8; img[f1 + 1] = 0xFF; img[f1 + 2] = 0xFF; img[f1 + 3] = 0xFF; img[f1 + 4] = 0x0F;
let f2 = 512 * 2;
let fat_copy = img[f1..f1 + 5].to_vec();
img[f2..f2 + 5].copy_from_slice(&fat_copy);
let rd = 512 * 3;
img[rd..rd + 8].copy_from_slice(b"README "); img[rd + 8..rd + 11].copy_from_slice(b"TXT");
img[rd + 11] = 0x20; img[rd + 26..rd + 28].copy_from_slice(&2u16.to_le_bytes()); img[rd + 28..rd + 32].copy_from_slice(&12u32.to_le_bytes());
let data = 512 * 4;
img[data..data + 12].copy_from_slice(b"hello world\n");
img
}
fn make_fat32_bpb_only() -> Vec<u8> {
let mut img = vec![0u8; 512 * 64];
img[11..13].copy_from_slice(&512u16.to_le_bytes()); img[13] = 1; img[14..16].copy_from_slice(&32u16.to_le_bytes()); img[16] = 2; img[22..24].copy_from_slice(&0u16.to_le_bytes()); img[32..36].copy_from_slice(&135_000u32.to_le_bytes()); img[36..40].copy_from_slice(&512u32.to_le_bytes()); img[44..48].copy_from_slice(&2u32.to_le_bytes()); img[510] = 0x55;
img[511] = 0xAA;
let f1 = 32 * 512usize;
img[f1] = 0xF8; img[f1 + 1] = 0xFF;
img[f1 + 2] = 0xFF;
img[f1 + 3] = 0x0F; img[f1 + 4] = 0xFF; img[f1 + 5] = 0xFF;
img[f1 + 6] = 0xFF;
img[f1 + 7] = 0x0F;
img[f1 + 8] = 0xFF;
img[f1 + 9] = 0xFF;
img[f1 + 10] = 0xFF;
img[f1 + 11] = 0x0F;
img
}
#[test]
fn detect_fat12_returns_true() {
let img = make_fat12_image();
let mut cursor = Cursor::new(&img);
assert!(detect(&mut cursor), "FAT12 image should be detected");
}
#[test]
fn detect_restores_position_on_success() {
let img = make_fat12_image();
let mut cursor = Cursor::new(&img);
cursor.seek(SeekFrom::Start(0)).unwrap();
detect(&mut cursor);
assert_eq!(cursor.position(), 0, "detect must restore position");
}
#[test]
fn detect_restores_position_on_failure() {
let img = vec![0u8; 512];
let mut cursor = Cursor::new(&img);
detect(&mut cursor);
assert_eq!(
cursor.position(),
0,
"detect must restore position on failure"
);
}
#[test]
fn detect_rejects_non_fat() {
let img = vec![0u8; 512]; let mut cursor = Cursor::new(&img);
assert!(!detect(&mut cursor));
}
#[test]
fn parse_fat12_tree_shape() {
let img = make_fat12_image();
let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
assert_eq!(tree.name, "/");
assert!(tree.is_directory);
assert_eq!(tree.children.len(), 1);
let file = &tree.children[0];
assert_eq!(file.name, "README.TXT");
assert_eq!(file.size, 12);
assert_eq!(file.file_length, Some(12));
assert!(!file.is_directory);
}
#[test]
fn fat12_file_location_points_at_cluster_data() {
let img = make_fat12_image();
let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
let file = &tree.children[0];
assert_eq!(file.file_location, Some(512 * 4));
let loc = file.file_location.unwrap();
let len = file.file_length.unwrap() as usize;
cursor.seek(SeekFrom::Start(loc)).unwrap();
let mut buf = vec![0u8; len];
cursor.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"hello world\n");
}
#[test]
fn empty_root_dir_parses_ok() {
let mut img = make_fat12_image();
let rd = 512 * 3;
img[rd..rd + 512].fill(0);
let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
assert_eq!(tree.children.len(), 0);
assert_eq!(tree.size, 0);
}
#[test]
fn deleted_entry_skipped() {
let mut img = make_fat12_image();
img[512 * 3] = 0xE5;
let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
assert_eq!(tree.children.len(), 0, "deleted entry must not appear");
}
#[test]
fn parse_fat32_bpb_detect() {
let img = make_fat32_bpb_only();
let mut cursor = Cursor::new(&img);
assert!(detect(&mut cursor), "FAT32 BPB should be detected");
}
#[test]
fn fat12_with_lfn_entry() {
let mut img = make_fat12_image();
let rd = 512 * 3;
let lfn_name_chars: Vec<u16> = "LongFile.txt"
.encode_utf16()
.chain(std::iter::once(0x0000)) .collect();
let lfn = &mut img[rd..rd + 32];
lfn[0] = 0x41; lfn[11] = 0x0F; let fields = [(1usize, 5usize), (14, 6), (28, 2)];
let mut ci = 0;
for (start, count) in fields {
for j in 0..count {
let ch = if ci < lfn_name_chars.len() {
lfn_name_chars[ci]
} else {
0xFFFF
};
lfn[start + j * 2] = (ch & 0xFF) as u8;
lfn[start + j * 2 + 1] = (ch >> 8) as u8;
ci += 1;
}
}
let e83 = &mut img[rd + 32..rd + 64];
e83[..8].copy_from_slice(b"LONGFI~1");
e83[8..11].copy_from_slice(b"TXT");
e83[11] = 0x20; e83[26..28].copy_from_slice(&2u16.to_le_bytes()); e83[28..32].copy_from_slice(&12u32.to_le_bytes());
let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
assert_eq!(tree.children.len(), 1);
assert_eq!(
tree.children[0].name, "LongFile.txt",
"LFN reassembly failed, got: {}",
tree.children[0].name
);
}
#[test]
fn too_short_image_errors() {
let img = vec![0u8; 256]; let mut cursor = Cursor::new(&img);
assert!(matches!(
detect_and_parse(&mut cursor),
Err(Error::TooShort)
));
}
#[test]
fn bad_bytes_per_sector_errors() {
let mut img = make_fat12_image();
img[11..13].copy_from_slice(&300u16.to_le_bytes());
let mut cursor = Cursor::new(&img);
assert!(matches!(
detect_and_parse(&mut cursor),
Err(Error::BadBootSector)
));
}
#[test]
fn bad_sectors_per_cluster_errors() {
let mut img = make_fat12_image();
img[13] = 3; let mut cursor = Cursor::new(&img);
assert!(matches!(
detect_and_parse(&mut cursor),
Err(Error::BadBootSector)
));
}
#[test]
fn read_bpb_num_fats_zero_returns_bad_boot_sector() {
let mut img = make_fat12_image();
img[16] = 0;
let mut cursor = Cursor::new(&img);
assert!(matches!(read_bpb(&mut cursor), Err(Error::BadBootSector)));
}
#[test]
fn read_bpb_num_fats_three_returns_bad_boot_sector() {
let mut img = make_fat12_image();
img[16] = 3;
let mut cursor = Cursor::new(&img);
assert!(matches!(read_bpb(&mut cursor), Err(Error::BadBootSector)));
}
#[test]
fn read_bpb_fat_size_zero_returns_bad_boot_sector() {
let mut img = make_fat12_image();
img[22..24].copy_from_slice(&0u16.to_le_bytes()); let mut cursor = Cursor::new(&img);
assert!(matches!(read_bpb(&mut cursor), Err(Error::BadBootSector)));
}
#[test]
fn read_bpb_total_sectors_zero_returns_bad_boot_sector() {
let mut img = make_fat12_image();
img[19..21].copy_from_slice(&0u16.to_le_bytes()); let mut cursor = Cursor::new(&img);
assert!(matches!(read_bpb(&mut cursor), Err(Error::BadBootSector)));
}
#[test]
fn fat16_detected_by_cluster_count() {
let mut img = vec![0u8; 512];
img[11..13].copy_from_slice(&512u16.to_le_bytes()); img[13] = 1; img[14..16].copy_from_slice(&1u16.to_le_bytes()); img[16] = 2; img[17..19].copy_from_slice(&16u16.to_le_bytes()); img[19..21].copy_from_slice(&10004u16.to_le_bytes()); img[22..24].copy_from_slice(&1u16.to_le_bytes()); img[510] = 0x55;
img[511] = 0xAA;
let mut cursor = Cursor::new(&img);
let ctx = read_bpb(&mut cursor).unwrap();
assert_eq!(ctx.fat_type, FatType::Fat16);
}
#[test]
fn fat32_bad_root_cluster_returns_bad_boot_sector() {
let mut img = vec![0u8; 512];
img[11..13].copy_from_slice(&512u16.to_le_bytes());
img[13] = 1;
img[14..16].copy_from_slice(&1u16.to_le_bytes()); img[16] = 2;
img[17..19].copy_from_slice(&16u16.to_le_bytes()); img[19..21].copy_from_slice(&0u16.to_le_bytes()); img[22..24].copy_from_slice(&1u16.to_le_bytes()); img[32..36].copy_from_slice(&70000u32.to_le_bytes()); img[44..48].copy_from_slice(&0u32.to_le_bytes()); img[510] = 0x55;
img[511] = 0xAA;
let mut cursor = Cursor::new(&img);
assert!(matches!(read_bpb(&mut cursor), Err(Error::BadBootSector)));
}
#[test]
fn short_name_83_e5_prefix() {
let mut entry = [b' '; 11];
entry[0] = 0x05; let name = short_name_83(&entry);
assert!(
!name.starts_with('\x05'),
"0x05 must be remapped, got: {name}"
);
}
#[test]
fn short_name_83_no_extension() {
let mut entry = [b' '; 11];
entry[..6].copy_from_slice(b"README");
let name = short_name_83(&entry);
assert_eq!(name, "README");
}
#[test]
fn parse_dir_entries_volume_id_skipped() {
let mut entry = [0u8; 32];
entry[0] = b'M'; entry[11] = 0x08; let mut dir = entry.to_vec();
dir.extend_from_slice(&[0u8; 32]); let entries = parse_dir_entries(&dir);
assert!(entries.is_empty(), "volume label must be skipped");
}
#[test]
fn fat12_fat_entry_odd_cluster() {
let mut fat = vec![0u8; 512];
fat[4] = 0x70;
fat[5] = 0x00;
let ctx = Context {
base_offset: 0,
fat_type: FatType::Fat12,
bytes_per_cluster: 512,
fat_start_rel: 0,
root_dir_rel: 512,
root_dir_size_bytes: 512,
data_start_rel: 1024,
root_cluster: 0,
total_clusters: 10,
};
let mut cursor = Cursor::new(fat);
let entry = ctx.fat_entry(&mut cursor, 3).unwrap();
assert_eq!(entry, 7);
}
#[test]
fn fat16_fat_entry() {
let mut fat = vec![0u8; 512];
fat[10] = 0x07;
fat[11] = 0x00;
let ctx = Context {
base_offset: 0,
fat_type: FatType::Fat16,
bytes_per_cluster: 512,
fat_start_rel: 0,
root_dir_rel: 512,
root_dir_size_bytes: 512,
data_start_rel: 1024,
root_cluster: 0,
total_clusters: 100,
};
let mut cursor = Cursor::new(fat);
let entry = ctx.fat_entry(&mut cursor, 5).unwrap();
assert_eq!(entry, 7);
}
#[test]
fn fat32_fat_entry_masks_reserved_bits() {
let mut fat = vec![0u8; 512];
fat[8] = 0xFF;
fat[9] = 0xFF;
fat[10] = 0xFF;
fat[11] = 0xFF;
let ctx = Context {
base_offset: 0,
fat_type: FatType::Fat32,
bytes_per_cluster: 512,
fat_start_rel: 0,
root_dir_rel: 512,
root_dir_size_bytes: 512,
data_start_rel: 1024,
root_cluster: 2,
total_clusters: 100,
};
let mut cursor = Cursor::new(fat);
let entry = ctx.fat_entry(&mut cursor, 2).unwrap();
assert_eq!(entry, 0x0FFF_FFFF);
}
#[test]
fn cluster_chain_stops_at_eoc() {
let mut fat = vec![0u8; 512];
fat[3] = 0xFF;
fat[4] = 0x0F;
let ctx = Context {
base_offset: 0,
fat_type: FatType::Fat12,
bytes_per_cluster: 512,
fat_start_rel: 0,
root_dir_rel: 512,
root_dir_size_bytes: 512,
data_start_rel: 1024,
root_cluster: 0,
total_clusters: 10,
};
let mut cursor = Cursor::new(fat);
let chain = ctx.cluster_chain(&mut cursor, 2).unwrap();
assert_eq!(chain, vec![2u32]);
}
#[test]
fn cluster_chain_cycle_detection() {
let mut fat = vec![0u8; 512];
fat[4] = 3; fat[6] = 2; let ctx = Context {
base_offset: 0,
fat_type: FatType::Fat16,
bytes_per_cluster: 512,
fat_start_rel: 0,
root_dir_rel: 512,
root_dir_size_bytes: 512,
data_start_rel: 1024,
root_cluster: 0,
total_clusters: 2,
};
let mut cursor = Cursor::new(fat);
let chain = ctx.cluster_chain(&mut cursor, 2).unwrap();
assert_eq!(chain.len(), 3);
}
#[test]
fn cluster_chain_stops_at_eoc_in_range() {
let mut fat = vec![0u8; 512];
fat[3] = 0xF8;
fat[4] = 0x0F; let ctx = Context {
base_offset: 0,
fat_type: FatType::Fat12,
bytes_per_cluster: 512,
fat_start_rel: 0,
root_dir_rel: 512,
root_dir_size_bytes: 512,
data_start_rel: 1024,
root_cluster: 0,
total_clusters: 0x1000, };
let mut cursor = Cursor::new(fat);
let chain = ctx.cluster_chain(&mut cursor, 2).unwrap();
assert_eq!(chain, vec![2u32]);
}
#[test]
fn parse_dir_entries_dot_entries_skipped() {
let mut buf = vec![0u8; 32 * 3];
buf[0] = b'.';
buf[1..8].fill(b' ');
buf[8..11].fill(b' ');
buf[11] = 0x10; buf[32] = b'.';
buf[33] = b'.';
buf[34..40].fill(b' ');
buf[40..43].fill(b' ');
buf[43] = 0x10;
let entries = parse_dir_entries(&buf);
assert!(entries.is_empty(), "dot and dotdot entries must be skipped");
}
#[test]
fn subdirectory_with_children_recurses() {
let mut img = vec![0u8; 512 * 6];
img[11..13].copy_from_slice(&512u16.to_le_bytes());
img[13] = 1; img[14..16].copy_from_slice(&1u16.to_le_bytes()); img[16] = 2; img[17..19].copy_from_slice(&16u16.to_le_bytes()); img[19..21].copy_from_slice(&6u16.to_le_bytes()); img[22..24].copy_from_slice(&1u16.to_le_bytes()); img[510] = 0x55;
img[511] = 0xAA;
let f = 512usize;
img[f] = 0xF8;
img[f + 1] = 0xFF;
img[f + 2] = 0xFF;
img[f + 3] = 0xFF; img[f + 4] = 0xFF; img[f + 5] = 0xFF; let fat_copy = img[512..518].to_vec();
img[1024..1024 + 6].copy_from_slice(&fat_copy);
let rd = 1536usize;
img[rd..rd + 8].copy_from_slice(b"SUBDIR ");
img[rd + 8..rd + 11].copy_from_slice(b" ");
img[rd + 11] = 0x10; img[rd + 26..rd + 28].copy_from_slice(&2u16.to_le_bytes());
let sd = 2048usize;
img[sd..sd + 8].copy_from_slice(b"CHILD ");
img[sd + 8..sd + 11].copy_from_slice(b"TXT");
img[sd + 11] = 0x20; img[sd + 26..sd + 28].copy_from_slice(&3u16.to_le_bytes()); img[sd + 28..sd + 32].copy_from_slice(&5u32.to_le_bytes());
img[2560..2565].copy_from_slice(b"hello");
let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
assert_eq!(tree.children.len(), 1);
let dir = &tree.children[0];
assert!(dir.is_directory);
assert_eq!(dir.name, "SUBDIR");
assert_eq!(dir.children.len(), 1);
let child = &dir.children[0];
assert_eq!(child.name, "CHILD.TXT");
assert_eq!(child.size, 5);
}
#[test]
fn build_tree_depth_limit_returns_empty() {
let img = make_fat12_image();
let mut cursor = Cursor::new(&img);
let ctx = read_bpb(&mut cursor).unwrap();
let result = build_tree(&ctx, &mut cursor, 0, 33).unwrap();
assert!(result.is_empty(), "depth > 32 must return empty vec");
}
#[test]
fn directory_entry_builds_dir_node() {
let mut img = make_fat12_image();
let rd = 512 * 3;
img[rd..rd + 8].copy_from_slice(b"SUBDIR ");
img[rd + 8..rd + 11].copy_from_slice(b" ");
img[rd + 11] = 0x10; img[rd + 26..rd + 28].copy_from_slice(&0u16.to_le_bytes()); img[rd + 28..rd + 32].copy_from_slice(&0u32.to_le_bytes()); let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
assert_eq!(tree.children.len(), 1);
let dir = &tree.children[0];
assert!(dir.is_directory);
assert_eq!(dir.name, "SUBDIR");
assert_eq!(dir.children.len(), 0);
}
#[test]
fn truncated_chain_file_has_no_location() {
let mut img = make_fat12_image();
let rd = 512 * 3;
img[rd + 28..rd + 32].copy_from_slice(&1000u32.to_le_bytes());
let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
let file = &tree.children[0];
assert!(
file.file_location.is_none(),
"truncated chain must have no location"
);
}
#[test]
fn zero_size_file_has_no_location() {
let mut img = make_fat12_image();
let rd = 512 * 3;
img[rd + 28..rd + 32].copy_from_slice(&0u32.to_le_bytes()); let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
let file = &tree.children[0];
assert_eq!(file.size, 0);
assert!(file.file_location.is_none());
}
#[test]
fn fat32_small_empty_root_parses_ok() {
let mut img = vec![0u8; 2048];
img[11..13].copy_from_slice(&512u16.to_le_bytes());
img[13] = 1; img[14..16].copy_from_slice(&1u16.to_le_bytes()); img[16] = 2; img[22..24].copy_from_slice(&0u16.to_le_bytes()); img[32..36].copy_from_slice(&4u32.to_le_bytes()); img[36..40].copy_from_slice(&1u32.to_le_bytes()); img[44..48].copy_from_slice(&2u32.to_le_bytes()); img[510] = 0x55;
img[511] = 0xAA;
img[512 + 8] = 0xFF;
img[512 + 9] = 0xFF;
img[512 + 10] = 0xFF;
img[512 + 11] = 0x0F;
let mut cursor = Cursor::new(&img);
let tree = detect_and_parse(&mut cursor).unwrap();
assert_eq!(tree.name, "/");
assert_eq!(tree.children.len(), 0);
}
#[test]
fn error_from_io_error() {
let io_err = std::io::Error::other("disk error");
let err: Error = Error::from(io_err);
assert!(matches!(err, Error::Io(_)));
}
#[test]
fn error_display_too_short() {
let msg = format!("{}", Error::TooShort);
assert!(msg.contains("short") || msg.contains("FAT"), "got: {msg}");
}
#[test]
fn error_display_bad_boot_sector() {
let msg = format!("{}", Error::BadBootSector);
assert!(
msg.contains("BPB") || msg.contains("boot") || msg.contains("FAT"),
"got: {msg}"
);
}
#[test]
fn error_display_io() {
let io = std::io::Error::other("disk");
let msg = format!("{}", Error::Io(io));
assert!(msg.contains("disk"), "got: {msg}");
}
#[test]
fn error_source_io() {
use std::error::Error as StdError;
assert!(Error::Io(std::io::Error::other("s")).source().is_some());
}
#[test]
fn error_source_non_io() {
use std::error::Error as StdError;
assert!(Error::TooShort.source().is_none());
assert!(Error::BadBootSector.source().is_none());
}
#[test]
fn is_eoc_fat16_and_fat32() {
assert!(is_eoc(FatType::Fat16, 0xFFF8));
assert!(is_eoc(FatType::Fat16, 0xFFFF));
assert!(!is_eoc(FatType::Fat16, 0xFFF7));
assert!(is_eoc(FatType::Fat32, 0x0FFF_FFF8));
assert!(is_eoc(FatType::Fat32, 0x0FFF_FFFF));
assert!(!is_eoc(FatType::Fat32, 0x0FFF_FFF7));
}
#[test]
fn is_bad_cluster_fat16_and_fat32() {
assert!(is_bad_cluster(FatType::Fat16, 0xFFF7));
assert!(!is_bad_cluster(FatType::Fat16, 0xFFF8));
assert!(is_bad_cluster(FatType::Fat32, 0x0FFF_FFF7));
assert!(!is_bad_cluster(FatType::Fat32, 0x0FFF_FFF8));
}
}