pub const ENTRY_SIZE: usize = 32;
pub const ATTR_READ_ONLY: u8 = 0x01;
pub const ATTR_HIDDEN: u8 = 0x02;
pub const ATTR_SYSTEM: u8 = 0x04;
pub const ATTR_VOLUME_ID: u8 = 0x08;
pub const ATTR_DIRECTORY: u8 = 0x10;
pub const ATTR_ARCHIVE: u8 = 0x20;
pub const ATTR_LFN: u8 = ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID;
pub const LFN_CHARS_PER_ENTRY: usize = 13;
pub const DOS_DATE_EPOCH: u16 = (1 << 5) | 1;
#[derive(Debug, Clone)]
pub struct DirEntry {
pub name_83: [u8; 11],
pub attr: u8,
pub first_cluster: u32,
pub file_size: u32,
}
impl DirEntry {
pub fn encode(&self) -> [u8; ENTRY_SIZE] {
let mut b = [0u8; ENTRY_SIZE];
b[0..11].copy_from_slice(&self.name_83);
b[11] = self.attr;
b[16..18].copy_from_slice(&DOS_DATE_EPOCH.to_le_bytes());
b[18..20].copy_from_slice(&DOS_DATE_EPOCH.to_le_bytes());
b[20..22].copy_from_slice(&((self.first_cluster >> 16) as u16).to_le_bytes());
b[24..26].copy_from_slice(&DOS_DATE_EPOCH.to_le_bytes());
b[26..28].copy_from_slice(&(self.first_cluster as u16).to_le_bytes());
b[28..32].copy_from_slice(&self.file_size.to_le_bytes());
b
}
pub fn decode(b: &[u8]) -> Option<Self> {
if b.len() < ENTRY_SIZE || b[0] == 0x00 || b[0] == 0xE5 {
return None;
}
let attr = b[11];
if attr & ATTR_LFN == ATTR_LFN {
return None; }
let mut name_83 = [0u8; 11];
name_83.copy_from_slice(&b[0..11]);
let hi = u16::from_le_bytes(b[20..22].try_into().unwrap()) as u32;
let lo = u16::from_le_bytes(b[26..28].try_into().unwrap()) as u32;
Some(Self {
name_83,
attr,
first_cluster: (hi << 16) | lo,
file_size: u32::from_le_bytes(b[28..32].try_into().unwrap()),
})
}
pub fn short_name_string(&self) -> String {
let base = String::from_utf8_lossy(&self.name_83[0..8])
.trim_end()
.to_string();
let ext = String::from_utf8_lossy(&self.name_83[8..11])
.trim_end()
.to_string();
let name = if ext.is_empty() {
base
} else {
format!("{base}.{ext}")
};
name.to_ascii_lowercase()
}
}
pub fn lfn_checksum(name_83: &[u8; 11]) -> u8 {
let mut sum: u8 = 0;
for &c in name_83 {
sum = ((sum & 1) << 7).wrapping_add(sum >> 1).wrapping_add(c);
}
sum
}
pub fn encode_lfn_run(name: &str, csum: u8) -> Vec<[u8; ENTRY_SIZE]> {
let mut units: Vec<u16> = name.encode_utf16().collect();
units.push(0x0000);
while units.len() % LFN_CHARS_PER_ENTRY != 0 {
units.push(0xFFFF);
}
let n_entries = units.len() / LFN_CHARS_PER_ENTRY;
let mut out = Vec::with_capacity(n_entries);
for seq in 1..=n_entries {
let chunk = &units[(seq - 1) * LFN_CHARS_PER_ENTRY..seq * LFN_CHARS_PER_ENTRY];
let mut e = [0u8; ENTRY_SIZE];
let mut order = seq as u8;
if seq == n_entries {
order |= 0x40; }
e[0] = order;
e[11] = ATTR_LFN;
e[13] = csum;
for (i, &u) in chunk[0..5].iter().enumerate() {
e[1 + i * 2..3 + i * 2].copy_from_slice(&u.to_le_bytes());
}
for (i, &u) in chunk[5..11].iter().enumerate() {
e[14 + i * 2..16 + i * 2].copy_from_slice(&u.to_le_bytes());
}
for (i, &u) in chunk[11..13].iter().enumerate() {
e[28 + i * 2..30 + i * 2].copy_from_slice(&u.to_le_bytes());
}
out.push(e);
}
out.reverse(); out
}
pub fn is_valid_83(name: &str) -> bool {
let bytes = name.as_bytes();
if bytes.is_empty() || bytes.len() > 12 {
return false;
}
let valid = |c: u8| {
c.is_ascii_uppercase()
|| c.is_ascii_digit()
|| matches!(
c,
b'$' | b'%'
| b'\''
| b'-'
| b'_'
| b'@'
| b'~'
| b'`'
| b'!'
| b'('
| b')'
| b'{'
| b'}'
| b'^'
| b'#'
| b'&'
)
};
let dots = bytes.iter().filter(|&&c| c == b'.').count();
match dots {
0 => bytes.len() <= 8 && bytes.iter().all(|&c| valid(c)),
1 => {
let dot = name.find('.').unwrap();
let (base, ext) = (&bytes[..dot], &bytes[dot + 1..]);
!base.is_empty()
&& base.len() <= 8
&& ext.len() <= 3
&& base.iter().all(|&c| valid(c))
&& ext.iter().all(|&c| valid(c))
}
_ => false,
}
}
pub fn pack_83(name: &str) -> [u8; 11] {
let mut out = [b' '; 11];
match name.find('.') {
Some(dot) => {
let base = &name.as_bytes()[..dot];
let ext = &name.as_bytes()[dot + 1..];
out[0..base.len()].copy_from_slice(base);
out[8..8 + ext.len()].copy_from_slice(ext);
}
None => {
let b = name.as_bytes();
out[0..b.len()].copy_from_slice(b);
}
}
out
}
#[derive(Debug, Clone)]
pub enum RawSlot {
End,
Deleted,
Lfn(LfnFragment),
ShortEntry(DirEntry),
}
#[derive(Debug, Clone)]
pub struct LfnFragment {
pub order: u8,
pub checksum: u8,
pub chars: [u16; LFN_CHARS_PER_ENTRY],
}
impl LfnFragment {
pub fn decode(b: &[u8; ENTRY_SIZE]) -> Self {
let mut chars = [0u16; LFN_CHARS_PER_ENTRY];
for (i, slot) in chars[0..5].iter_mut().enumerate() {
*slot = u16::from_le_bytes(b[1 + i * 2..3 + i * 2].try_into().unwrap());
}
for (i, slot) in chars[5..11].iter_mut().enumerate() {
*slot = u16::from_le_bytes(b[14 + i * 2..16 + i * 2].try_into().unwrap());
}
for (i, slot) in chars[11..13].iter_mut().enumerate() {
*slot = u16::from_le_bytes(b[28 + i * 2..30 + i * 2].try_into().unwrap());
}
Self {
order: b[0],
checksum: b[13],
chars,
}
}
}
pub fn classify_slot(b: &[u8; ENTRY_SIZE]) -> RawSlot {
if b[0] == 0x00 {
return RawSlot::End;
}
if b[0] == 0xE5 {
return RawSlot::Deleted;
}
if b[11] & ATTR_LFN == ATTR_LFN {
return RawSlot::Lfn(LfnFragment::decode(b));
}
RawSlot::ShortEntry(DirEntry::decode(b).expect("classified short entry decodes"))
}
pub fn assemble_lfn(fragments: &[LfnFragment], short_name_83: &[u8; 11]) -> Option<String> {
if fragments.is_empty() {
return None;
}
let expected_csum = lfn_checksum(short_name_83);
let first = &fragments[0];
let n = (first.order & 0x3F) as usize;
if n != fragments.len() || first.order & 0x40 == 0 {
return None;
}
for (i, f) in fragments.iter().enumerate() {
let want_seq = (n - i) as u8;
let mask = if i == 0 { 0x40 } else { 0x00 };
if f.order != want_seq | mask {
return None;
}
if f.checksum != expected_csum {
return None;
}
}
let mut units: Vec<u16> = Vec::with_capacity(n * LFN_CHARS_PER_ENTRY);
for f in fragments.iter().rev() {
units.extend_from_slice(&f.chars);
}
let end = units
.iter()
.position(|&u| u == 0x0000)
.unwrap_or(units.len());
String::from_utf16(&units[..end]).ok()
}
pub fn generate_83(long: &str, seq: u32) -> [u8; 11] {
let mut out = [b' '; 11];
let base = format!("FT{:06X}", seq & 0xFF_FFFF);
out[0..8].copy_from_slice(&base.as_bytes()[..8]);
if let Some(dot) = long.rfind('.') {
let ext: Vec<u8> = long[dot + 1..]
.bytes()
.filter(|c| c.is_ascii_alphanumeric())
.take(3)
.map(|c| c.to_ascii_uppercase())
.collect();
out[8..8 + ext.len()].copy_from_slice(&ext);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_roundtrip() {
let e = DirEntry {
name_83: *b"HELLO TXT",
attr: ATTR_ARCHIVE,
first_cluster: 0x0012_3456,
file_size: 4096,
};
let dec = DirEntry::decode(&e.encode()).unwrap();
assert_eq!(dec.name_83, *b"HELLO TXT");
assert_eq!(dec.first_cluster, 0x0012_3456);
assert_eq!(dec.file_size, 4096);
assert_eq!(dec.short_name_string(), "hello.txt");
}
#[test]
fn free_and_lfn_slots_decode_to_none() {
assert!(DirEntry::decode(&[0u8; ENTRY_SIZE]).is_none());
let mut deleted = [0x41u8; ENTRY_SIZE];
deleted[0] = 0xE5;
assert!(DirEntry::decode(&deleted).is_none());
let mut lfn = [0u8; ENTRY_SIZE];
lfn[0] = 0x41;
lfn[11] = ATTR_LFN;
assert!(DirEntry::decode(&lfn).is_none());
}
#[test]
fn lfn_checksum_known_value() {
let name = *b"HELLO TXT";
let mut sum: u8 = 0;
for &c in &name {
sum = ((sum & 1) << 7).wrapping_add(sum >> 1).wrapping_add(c);
}
assert_eq!(lfn_checksum(&name), sum);
}
#[test]
fn lfn_run_length_and_order() {
let run = encode_lfn_run("a-twenty-char-name!!", 0xAB);
assert_eq!(run.len(), 2);
assert_eq!(run[0][0], 0x40 | 2);
assert_eq!(run[1][0], 1);
for e in &run {
assert_eq!(e[11], ATTR_LFN);
assert_eq!(e[13], 0xAB);
}
}
#[test]
fn valid_83_classification() {
assert!(is_valid_83("README"));
assert!(is_valid_83("KERNEL.IMG"));
assert!(is_valid_83("A.B"));
assert!(!is_valid_83("readme")); assert!(!is_valid_83("toolongname.txt")); assert!(!is_valid_83("a.b.c")); assert!(!is_valid_83("with space")); }
#[test]
fn generate_83_is_8_3() {
let s = generate_83("some long name.tar.gz", 1);
assert_eq!(&s[0..2], b"FT");
assert_eq!(&s[8..11], b"GZ "); assert_ne!(generate_83("x", 1), generate_83("x", 2));
}
}