2025-07-26 15:51:21 +02:00
|
|
|
use std::fmt::Display;
|
2025-08-01 18:09:45 +02:00
|
|
|
use std::io::{Read, Write};
|
2025-08-02 17:22:32 +02:00
|
|
|
use std::time::SystemTime;
|
2025-07-26 15:51:21 +02:00
|
|
|
|
|
|
|
|
use bitflags::bitflags;
|
2025-08-02 17:22:32 +02:00
|
|
|
use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, TimeDelta, Timelike};
|
2025-07-30 21:39:45 +02:00
|
|
|
use compact_str::CompactString;
|
2025-08-02 17:22:32 +02:00
|
|
|
use log::debug;
|
2025-07-26 15:51:21 +02:00
|
|
|
|
2025-08-01 22:42:13 +02:00
|
|
|
use crate::FatFs;
|
2025-07-26 15:51:21 +02:00
|
|
|
use crate::datetime::{Date, Time};
|
2025-08-01 22:42:13 +02:00
|
|
|
use crate::iter::ClusterChainReader;
|
|
|
|
|
use crate::subslice::SubSliceMut;
|
2025-07-26 15:51:21 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 16:46:39 +02:00
|
|
|
impl Display for Attr {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
let mut if_has_attr = |attr: Attr, c: char| {
|
|
|
|
|
if self.contains(attr) {
|
|
|
|
|
write!(f, "{}", c)
|
|
|
|
|
} else {
|
|
|
|
|
write!(f, "-")
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if_has_attr(Attr::ReadOnly, 'R')?;
|
|
|
|
|
if_has_attr(Attr::Hidden, 'H')?;
|
|
|
|
|
if_has_attr(Attr::System, 'S')?;
|
|
|
|
|
if_has_attr(Attr::VolumeId, 'V')?;
|
|
|
|
|
if_has_attr(Attr::Directory, 'D')?;
|
|
|
|
|
if_has_attr(Attr::Archive, 'A')?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
/// represents an entry in a diectory
|
2025-07-26 15:51:21 +02:00
|
|
|
#[derive(Debug)]
|
2025-07-27 14:38:17 +02:00
|
|
|
pub struct DirEntry {
|
2025-08-02 17:22:32 +02:00
|
|
|
name: [u8; 11],
|
2025-07-26 15:51:21 +02:00
|
|
|
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,
|
|
|
|
|
|
2025-07-30 21:39:45 +02:00
|
|
|
checksum: u8,
|
|
|
|
|
long_name: Option<CompactString>,
|
2025-08-01 22:42:13 +02:00
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
n_longname_slots: u8,
|
|
|
|
|
|
2025-08-01 22:42:13 +02:00
|
|
|
offset: u64,
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
impl Display for DirEntry {
|
2025-07-26 15:51:21 +02:00
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
2025-07-31 23:47:45 +02:00
|
|
|
let mut name = self.name_string();
|
2025-07-26 15:51:21 +02:00
|
|
|
|
|
|
|
|
if self.attr.contains(Attr::Directory) {
|
|
|
|
|
name.push('/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
write!(
|
|
|
|
|
f,
|
2025-07-27 02:04:21 +02:00
|
|
|
"{} {}",
|
2025-07-26 16:47:12 +02:00
|
|
|
self.attr,
|
2025-07-27 02:04:21 +02:00
|
|
|
// self.create_time().format("%a %b %d %H:%M:%S%.3f %Y"),
|
|
|
|
|
// self.write_time().format("%a %b %d %H:%M:%S%.3f %Y"),
|
2025-07-26 15:51:21 +02:00
|
|
|
name,
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
impl DirEntry {
|
2025-08-01 22:42:13 +02:00
|
|
|
pub fn load(bytes: &[u8], offset: u64) -> anyhow::Result<DirEntry> {
|
2025-07-26 15:51:21 +02:00
|
|
|
assert_eq!(bytes.len(), 32);
|
|
|
|
|
|
|
|
|
|
let attr = Attr::from_bits_truncate(bytes[11]);
|
|
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
let name = bytes[..11].try_into().unwrap();
|
2025-07-30 21:39:45 +02:00
|
|
|
|
2025-07-26 15:51:21 +02:00
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
Ok(DirEntry {
|
2025-07-26 15:51:21 +02:00
|
|
|
name,
|
|
|
|
|
attr,
|
|
|
|
|
create_time_tenths,
|
|
|
|
|
create_time,
|
|
|
|
|
create_date,
|
|
|
|
|
last_access_date,
|
|
|
|
|
first_cluster,
|
|
|
|
|
write_time,
|
|
|
|
|
write_date,
|
|
|
|
|
file_size,
|
|
|
|
|
long_name: None,
|
2025-08-02 17:22:32 +02:00
|
|
|
n_longname_slots: 0,
|
2025-07-30 21:39:45 +02:00
|
|
|
checksum: Self::checksum(&bytes[..11]),
|
2025-08-01 22:42:13 +02:00
|
|
|
offset,
|
2025-07-26 15:51:21 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
pub fn create(name: &str, attr: Attr) -> anyhow::Result<Self> {
|
|
|
|
|
let now: DateTime<Local> = SystemTime::now().into();
|
|
|
|
|
|
|
|
|
|
let create_date = Date::from_datetime(now)?;
|
|
|
|
|
let create_time = Time::from_datetime(now)?;
|
|
|
|
|
let create_time_tenths = (now.time().nanosecond() / 100_000_000) as u8;
|
|
|
|
|
|
|
|
|
|
let name = [0; 11];
|
|
|
|
|
|
|
|
|
|
Ok(DirEntry {
|
|
|
|
|
name,
|
|
|
|
|
attr,
|
|
|
|
|
create_time_tenths,
|
|
|
|
|
create_time,
|
|
|
|
|
create_date,
|
|
|
|
|
last_access_date: create_date,
|
|
|
|
|
first_cluster: 0,
|
|
|
|
|
write_time: create_time,
|
|
|
|
|
write_date: create_date,
|
|
|
|
|
file_size: 0,
|
|
|
|
|
checksum: Self::checksum(&name),
|
|
|
|
|
long_name: None,
|
|
|
|
|
n_longname_slots: 0,
|
|
|
|
|
offset: !0,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-01 22:42:13 +02:00
|
|
|
fn write(&self, mut writer: impl Write) -> std::io::Result<()> {
|
2025-08-01 18:09:45 +02:00
|
|
|
let mut buf = [0; 32];
|
|
|
|
|
|
2025-08-01 22:42:13 +02:00
|
|
|
// fill name with 0x20
|
|
|
|
|
buf[..11].copy_from_slice(&[0x20; 11]);
|
|
|
|
|
|
2025-08-01 18:09:45 +02:00
|
|
|
let mut name = self.name();
|
|
|
|
|
|
|
|
|
|
if name[0] == b'.' && self.is_hidden() {
|
|
|
|
|
name = &name[1..];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some((idx, _)) = name
|
|
|
|
|
.iter()
|
|
|
|
|
.copied()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.rev()
|
|
|
|
|
.find(|&(_n, u)| u == b'.')
|
|
|
|
|
{
|
|
|
|
|
let (stem, ext) = name.split_at(idx);
|
|
|
|
|
|
|
|
|
|
buf[..8][..stem.len()].copy_from_slice(stem);
|
|
|
|
|
buf[8..][..ext.len()].copy_from_slice(ext);
|
|
|
|
|
|
|
|
|
|
// (stem, Some(ext))
|
|
|
|
|
} else {
|
|
|
|
|
// all stem, no ext
|
|
|
|
|
buf[..8][..name.len()].copy_from_slice(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buf[11] = self.attr().bits();
|
|
|
|
|
|
|
|
|
|
buf[12] = 0;
|
|
|
|
|
|
|
|
|
|
buf[13] = self.create_time_tenths;
|
|
|
|
|
buf[13..15].copy_from_slice(&self.create_time.repr().to_le_bytes());
|
|
|
|
|
|
|
|
|
|
buf[16..18].copy_from_slice(&self.create_date.repr().to_le_bytes());
|
|
|
|
|
|
|
|
|
|
buf[18..20].copy_from_slice(&self.last_access_date.repr().to_le_bytes());
|
|
|
|
|
|
|
|
|
|
buf[20..22].copy_from_slice(&((self.first_cluster() >> 16) as u16).to_le_bytes());
|
|
|
|
|
|
|
|
|
|
buf[22..24].copy_from_slice(&self.write_time.repr().to_le_bytes());
|
|
|
|
|
buf[24..26].copy_from_slice(&self.write_date.repr().to_le_bytes());
|
|
|
|
|
|
|
|
|
|
buf[26..28].copy_from_slice(&(self.first_cluster as u16).to_le_bytes());
|
|
|
|
|
|
|
|
|
|
buf[28..].copy_from_slice(&self.file_size.to_le_bytes());
|
|
|
|
|
|
2025-08-01 22:42:13 +02:00
|
|
|
eprintln!("writing new dir entry: {:?}", buf);
|
|
|
|
|
|
2025-08-01 18:09:45 +02:00
|
|
|
writer.write_all(&buf)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-01 22:42:13 +02:00
|
|
|
/// write this DisEntry back to the underlying data
|
2025-08-02 17:22:32 +02:00
|
|
|
pub fn write_back(&self, fat_fs: &FatFs) -> std::io::Result<()> {
|
2025-08-01 22:42:13 +02:00
|
|
|
eprintln!("making new SubSliceMut at offset {:#X}", self.offset);
|
|
|
|
|
let sub_slice = SubSliceMut::new(fat_fs.inner.clone(), self.offset, 32);
|
|
|
|
|
|
|
|
|
|
self.write(sub_slice)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
/// erase this DirEntry
|
|
|
|
|
pub fn erase(self, fat_fs: &FatFs) -> std::io::Result<()> {
|
|
|
|
|
let mut sub_slice = SubSliceMut::new(fat_fs.inner.clone(), self.offset, 32);
|
|
|
|
|
|
|
|
|
|
// set first byte to 0xE5 (free)
|
|
|
|
|
sub_slice.write_all(&[0xe5])?;
|
|
|
|
|
|
|
|
|
|
// paste over with zeros
|
|
|
|
|
sub_slice.write_all(&[0; 31])
|
2025-08-02 17:22:32 +02:00
|
|
|
|
|
|
|
|
// TODO: erase long filename entries as well
|
2025-08-02 00:36:35 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-26 15:51:21 +02:00
|
|
|
/// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 16:47:36 +02:00
|
|
|
pub fn is_file(&self) -> bool {
|
|
|
|
|
!self
|
|
|
|
|
.attr
|
|
|
|
|
.intersects(Attr::Directory | Attr::System | Attr::VolumeId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_dir(&self) -> bool {
|
|
|
|
|
self.attr.contains(Attr::Directory) && !self.attr.intersects(Attr::System | Attr::VolumeId)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 00:22:27 +02:00
|
|
|
pub fn is_dot(&self) -> bool {
|
|
|
|
|
if !self.is_dir() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.name[0] == b'.' && &self.name[1..] == &[b' '; 10]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_dotdot(&self) -> bool {
|
|
|
|
|
if !self.is_dir() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&self.name[..2] == &[b'.', b'.'] && &self.name[2..] == &[b' '; 9]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_hidden(&self) -> bool {
|
|
|
|
|
self.is_dot() || self.is_dotdot() || self.attr.contains(Attr::Hidden)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 23:15:28 +02:00
|
|
|
pub fn is_readonly(&self) -> bool {
|
|
|
|
|
self.attr.contains(Attr::ReadOnly)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 15:51:21 +02:00
|
|
|
pub fn name(&self) -> &[u8] {
|
2025-08-02 17:22:32 +02:00
|
|
|
&self.name
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-27 23:15:28 +02:00
|
|
|
pub fn stem(&self) -> &[u8] {
|
|
|
|
|
&self.name()[..8]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn stem_str(&self) -> Option<&str> {
|
|
|
|
|
std::str::from_utf8(self.stem()).ok()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn extension(&self) -> &[u8] {
|
|
|
|
|
&self.name()[8..]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn extension_str(&self) -> Option<&str> {
|
|
|
|
|
std::str::from_utf8(self.extension()).ok()
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-31 23:47:45 +02:00
|
|
|
pub fn name_string(&self) -> CompactString {
|
2025-07-30 21:39:45 +02:00
|
|
|
// use a CompactString here to allow inlining of short names
|
|
|
|
|
// maybe switch to a Cow instead? has disadvantage that we need to alloc for short names
|
|
|
|
|
|
2025-07-31 23:47:45 +02:00
|
|
|
// can't be empty
|
|
|
|
|
assert!(!self.is_empty());
|
|
|
|
|
|
2025-07-27 02:04:21 +02:00
|
|
|
if let Some(long_filename) = self.long_name() {
|
2025-07-31 23:47:45 +02:00
|
|
|
return long_filename.into();
|
2025-07-27 02:04:21 +02:00
|
|
|
}
|
2025-07-26 15:51:21 +02:00
|
|
|
|
2025-07-31 23:47:45 +02:00
|
|
|
let name = &self.name[..8];
|
|
|
|
|
let ext = &self.name[8..];
|
2025-07-26 15:51:21 +02:00
|
|
|
|
2025-07-30 21:39:45 +02:00
|
|
|
let mut s = CompactString::const_new("");
|
2025-07-26 15:51:21 +02:00
|
|
|
|
|
|
|
|
if self.attr.contains(Attr::Hidden) {
|
|
|
|
|
s.push('.');
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-31 23:47:45 +02:00
|
|
|
const VALID_SYMBOLS: &[u8] = &[
|
|
|
|
|
b'$', b'%', b'\'', b'-', b'_', b'@', b'~', b'`', b'!', b'(', b')', b'{', b'}', b'^',
|
|
|
|
|
b'#', b'&',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
fn map_chars(c: u8) -> char {
|
|
|
|
|
if !c.is_ascii()
|
|
|
|
|
|| c < 0x20
|
|
|
|
|
|| !(c.is_ascii_alphanumeric() || VALID_SYMBOLS.contains(&c))
|
|
|
|
|
{
|
|
|
|
|
'?'
|
|
|
|
|
} else {
|
2025-08-02 17:22:32 +02:00
|
|
|
(c as char).to_ascii_uppercase()
|
2025-07-31 23:47:45 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.extend(name.trim_ascii_end().iter().copied().map(map_chars));
|
2025-07-26 15:51:21 +02:00
|
|
|
|
|
|
|
|
if !ext.is_empty() {
|
|
|
|
|
s.push('.');
|
|
|
|
|
|
2025-07-31 23:47:45 +02:00
|
|
|
s.extend(ext.trim_ascii_end().iter().copied().map(map_chars));
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-31 23:47:45 +02:00
|
|
|
s
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn long_name(&self) -> Option<&str> {
|
|
|
|
|
self.long_name.as_deref()
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
pub fn set_long_name(&mut self, long_name: CompactString, n_slots: u8) {
|
2025-07-26 15:51:21 +02:00
|
|
|
self.long_name = Some(long_name);
|
2025-08-02 17:22:32 +02:00
|
|
|
self.n_longname_slots = n_slots;
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
pub fn update_last_access_date(
|
|
|
|
|
&mut self,
|
|
|
|
|
time: impl Into<DateTime<Local>>,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
self.last_access_date = Date::from_datetime(time.into())?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 15:51:21 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
pub fn update_write_time(&mut self, time: impl Into<DateTime<Local>>) -> anyhow::Result<()> {
|
|
|
|
|
let time = time.into();
|
|
|
|
|
|
|
|
|
|
self.write_date = Date::from_datetime(time)?;
|
|
|
|
|
self.write_time = Time::from_datetime(time)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 15:51:21 +02:00
|
|
|
pub fn file_size(&self) -> u32 {
|
|
|
|
|
self.file_size
|
|
|
|
|
}
|
2025-07-27 02:04:21 +02:00
|
|
|
|
2025-08-01 22:42:13 +02:00
|
|
|
pub fn update_file_size(&mut self, file_size: u32) {
|
|
|
|
|
self.file_size = file_size
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 21:39:45 +02:00
|
|
|
pub fn checksum(name: &[u8]) -> u8 {
|
2025-07-27 02:04:21 +02:00
|
|
|
let mut checksum: u8 = 0;
|
|
|
|
|
|
2025-07-30 21:39:45 +02:00
|
|
|
for &x in name {
|
2025-07-27 02:04:21 +02:00
|
|
|
checksum = checksum.rotate_right(1).wrapping_add(x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
checksum
|
|
|
|
|
}
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
/// long filename entry in a directory
|
|
|
|
|
///
|
|
|
|
|
/// this should not be exposed to end users, only for internal consumption in the DirIter
|
2025-08-02 17:22:32 +02:00
|
|
|
#[derive(Debug)]
|
2025-07-27 14:38:17 +02:00
|
|
|
struct LongNameDirEntry {
|
2025-07-27 02:04:21 +02:00
|
|
|
ordinal: u8,
|
|
|
|
|
is_last: bool,
|
|
|
|
|
name: [u16; 13],
|
|
|
|
|
checksum: u8,
|
|
|
|
|
}
|
2025-07-26 15:51:21 +02:00
|
|
|
|
|
|
|
|
impl LongNameDirEntry {
|
|
|
|
|
pub fn load(bytes: &[u8]) -> anyhow::Result<LongNameDirEntry> {
|
|
|
|
|
assert_eq!(bytes.len(), 32);
|
|
|
|
|
|
2025-07-27 02:04:21 +02:00
|
|
|
let ordinal = bytes[0] & !0x40;
|
|
|
|
|
let is_last = (bytes[0] & 0x40) != 0;
|
|
|
|
|
|
|
|
|
|
let name1 = &bytes[1..][..10];
|
|
|
|
|
|
|
|
|
|
let attr = Attr::from_bits_retain(bytes[11]);
|
|
|
|
|
|
|
|
|
|
anyhow::ensure!(attr.contains(Attr::LongName), "not a long name entry");
|
|
|
|
|
anyhow::ensure!(bytes[12] == 0, "LDIR_Type must be 0, not {}", bytes[12]);
|
|
|
|
|
|
|
|
|
|
let checksum = bytes[13];
|
|
|
|
|
|
|
|
|
|
let name2 = &bytes[14..][..12];
|
|
|
|
|
|
|
|
|
|
anyhow::ensure!(
|
|
|
|
|
&bytes[26..][..2] == &[0, 0],
|
|
|
|
|
"LDIR_FstClusLO must be zero, not 0x{:04X}",
|
|
|
|
|
load_u32_le(&bytes[26..][..2])
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let name3 = &bytes[28..][..4];
|
|
|
|
|
|
|
|
|
|
let mut name = [0; 13];
|
|
|
|
|
|
|
|
|
|
for (x, y) in name1
|
|
|
|
|
.chunks_exact(2)
|
|
|
|
|
.chain(name2.chunks_exact(2))
|
|
|
|
|
.chain(name3.chunks_exact(2))
|
|
|
|
|
.map(|x| load_u16_le(x))
|
|
|
|
|
.zip(name.iter_mut())
|
|
|
|
|
{
|
|
|
|
|
*y = x;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(LongNameDirEntry {
|
|
|
|
|
ordinal,
|
|
|
|
|
is_last,
|
|
|
|
|
name,
|
|
|
|
|
checksum,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn ordinal(&self) -> u8 {
|
|
|
|
|
self.ordinal
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
pub fn is_last(&self) -> bool {
|
2025-07-27 02:04:21 +02:00
|
|
|
self.is_last
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn name(&self) -> &[u16] {
|
|
|
|
|
&self.name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn checksum(&self) -> u8 {
|
|
|
|
|
self.checksum
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
/// wraps both Regular DirEntry and LongNameDirEntry
|
|
|
|
|
///
|
|
|
|
|
/// should not be exposed publicly, end users only see DirEntries
|
|
|
|
|
/// just for making the bytes -> DirEntry loading a bit easier
|
2025-08-02 17:22:32 +02:00
|
|
|
#[derive(Debug)]
|
2025-07-27 14:38:17 +02:00
|
|
|
enum DirEntryWrapper {
|
|
|
|
|
Regular(DirEntry),
|
2025-07-26 15:51:21 +02:00
|
|
|
LongName(LongNameDirEntry),
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
impl DirEntryWrapper {
|
2025-08-01 22:42:13 +02:00
|
|
|
pub fn load(bytes: &[u8], offset: u64) -> anyhow::Result<DirEntryWrapper> {
|
2025-07-26 15:51:21 +02:00
|
|
|
assert_eq!(bytes.len(), 32);
|
|
|
|
|
|
|
|
|
|
let attr = Attr::from_bits_truncate(bytes[11]);
|
|
|
|
|
|
|
|
|
|
let dir_entry = if attr == Attr::LongName {
|
2025-07-27 14:38:17 +02:00
|
|
|
DirEntryWrapper::LongName(LongNameDirEntry::load(bytes)?)
|
2025-07-26 15:51:21 +02:00
|
|
|
} else {
|
2025-08-01 22:42:13 +02:00
|
|
|
DirEntryWrapper::Regular(DirEntry::load(bytes, offset)?)
|
2025-07-26 15:51:21 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(dir_entry)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 02:04:21 +02:00
|
|
|
#[derive(Debug, Default)]
|
|
|
|
|
struct LongFilenameBuf {
|
|
|
|
|
rev_buf: Vec<u16>,
|
|
|
|
|
checksum: Option<u8>,
|
|
|
|
|
last_ordinal: Option<u8>,
|
2025-08-02 17:22:32 +02:00
|
|
|
n_slots: u8,
|
2025-07-27 02:04:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl LongFilenameBuf {
|
|
|
|
|
pub fn reset(&mut self) {
|
|
|
|
|
self.rev_buf.clear();
|
|
|
|
|
self.checksum = None;
|
|
|
|
|
self.last_ordinal = None;
|
2025-08-02 17:22:32 +02:00
|
|
|
self.n_slots = 0;
|
2025-07-27 02:04:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn next(&mut self, dir_entry: LongNameDirEntry) -> anyhow::Result<()> {
|
2025-07-27 14:38:17 +02:00
|
|
|
if dir_entry.is_last() {
|
2025-07-27 02:04:21 +02:00
|
|
|
// first/lasts entry
|
|
|
|
|
|
2025-07-31 23:47:45 +02:00
|
|
|
anyhow::ensure!(
|
|
|
|
|
dir_entry.ordinal() <= 20,
|
|
|
|
|
"can't have more than 20 long filename dir entries"
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-27 02:04:21 +02:00
|
|
|
let mut name = dir_entry.name();
|
|
|
|
|
|
|
|
|
|
while name.last() == Some(&0xFFFF) {
|
|
|
|
|
name = &name[..name.len() - 1];
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-31 00:22:16 +02:00
|
|
|
if name.last() == Some(&0) {
|
|
|
|
|
name = &name[..name.len() - 1];
|
|
|
|
|
} else {
|
|
|
|
|
// no null terminator only for names that are multiples of 13, i.e. perfectly fit
|
|
|
|
|
assert_eq!(name.len(), 13);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 02:04:21 +02:00
|
|
|
assert!(!name.is_empty());
|
|
|
|
|
|
|
|
|
|
self.extend_name(name);
|
|
|
|
|
self.checksum = Some(dir_entry.checksum());
|
|
|
|
|
self.last_ordinal = Some(dir_entry.ordinal());
|
|
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
self.n_slots += 1;
|
|
|
|
|
|
2025-07-27 02:04:21 +02:00
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-31 23:47:45 +02:00
|
|
|
assert!(self.checksum.is_some() && self.last_ordinal.is_some());
|
2025-07-27 02:04:21 +02:00
|
|
|
|
|
|
|
|
anyhow::ensure!(
|
|
|
|
|
self.checksum == Some(dir_entry.checksum()),
|
|
|
|
|
"checksum doesn't match previous"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
anyhow::ensure!(
|
|
|
|
|
self.last_ordinal.unwrap() != 1,
|
|
|
|
|
"last ordinal was 1, but found more entries"
|
|
|
|
|
);
|
|
|
|
|
anyhow::ensure!(
|
|
|
|
|
self.last_ordinal.unwrap() - 1 == dir_entry.ordinal,
|
|
|
|
|
"expected ordinal {}, but found {} instead",
|
|
|
|
|
self.last_ordinal.unwrap() - 1,
|
|
|
|
|
dir_entry.ordinal()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self.extend_name(dir_entry.name());
|
|
|
|
|
self.last_ordinal = Some(dir_entry.ordinal());
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn extend_name(&mut self, name: &[u16]) {
|
|
|
|
|
self.rev_buf.extend(name.iter().rev());
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
pub fn get_buf(
|
|
|
|
|
&mut self,
|
|
|
|
|
checksum: u8,
|
|
|
|
|
) -> anyhow::Result<Option<(impl Iterator<Item = u16>, u8)>> {
|
2025-07-27 02:04:21 +02:00
|
|
|
if self.checksum.is_none() {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
anyhow::ensure!(
|
|
|
|
|
self.last_ordinal.is_some() && self.checksum.is_some(),
|
|
|
|
|
"long filename buffer is empty"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
anyhow::ensure!(
|
|
|
|
|
self.last_ordinal.unwrap() == 1,
|
|
|
|
|
"last ordinal is {}, not 1",
|
|
|
|
|
self.last_ordinal.unwrap()
|
|
|
|
|
);
|
|
|
|
|
anyhow::ensure!(
|
|
|
|
|
self.checksum.unwrap() == checksum,
|
|
|
|
|
"given checksum 0x{:02X} doesn't match previous checksum 0x{:02X}",
|
|
|
|
|
checksum,
|
|
|
|
|
self.checksum.unwrap()
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-31 23:47:45 +02:00
|
|
|
anyhow::ensure!(self.rev_buf.len() <= 255, "long filename too long");
|
|
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
Ok(Some((self.rev_buf.iter().copied().rev(), self.n_slots)))
|
2025-07-27 02:04:21 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-01 22:42:13 +02:00
|
|
|
pub struct DirIter<'a> {
|
|
|
|
|
reader: ClusterChainReader<'a>,
|
2025-07-27 02:04:21 +02:00
|
|
|
|
|
|
|
|
long_filename_buf: LongFilenameBuf,
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-01 22:42:13 +02:00
|
|
|
impl<'a> DirIter<'a> {
|
|
|
|
|
pub fn new(reader: ClusterChainReader<'a>) -> Self {
|
2025-07-27 02:04:21 +02:00
|
|
|
DirIter {
|
|
|
|
|
reader,
|
|
|
|
|
long_filename_buf: Default::default(),
|
|
|
|
|
}
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
pub fn find_by_name(&mut self, name: &str) -> Option<DirEntry> {
|
|
|
|
|
self.find(|dir_entry| &dir_entry.name_string() == name)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-01 22:42:13 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
impl Iterator for DirIter<'_> {
|
|
|
|
|
type Item = DirEntry;
|
2025-07-26 15:51:21 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
|
|
|
fn next_impl(me: &mut DirIter<'_>) -> anyhow::Result<Option<DirEntry>> {
|
|
|
|
|
let offset = me.reader.current_offset();
|
2025-07-26 15:51:21 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
let mut chunk = [0; 32];
|
2025-07-27 14:38:17 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
if me.reader.read_exact(&mut chunk).is_err() {
|
|
|
|
|
// nothing we can do here since we might be in an invalid state after a partial read
|
|
|
|
|
anyhow::bail!("read failed");
|
2025-07-27 02:04:21 +02:00
|
|
|
}
|
2025-07-26 15:51:21 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
let dir_entry = DirEntryWrapper::load(&chunk, offset)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("failed to load dir entry: {e}"))?;
|
2025-07-26 15:51:21 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
let mut dir_entry = match dir_entry {
|
|
|
|
|
DirEntryWrapper::Regular(dir_entry) => dir_entry,
|
|
|
|
|
DirEntryWrapper::LongName(long_name) => {
|
|
|
|
|
me.long_filename_buf.next(long_name).map_err(|e| {
|
|
|
|
|
me.long_filename_buf.reset();
|
|
|
|
|
anyhow::anyhow!("invalid long filename entry: {e}")
|
|
|
|
|
})?;
|
2025-07-26 15:51:21 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
return next_impl(me);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-07-27 02:04:21 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
if dir_entry.is_sentinel() {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
2025-07-27 02:04:21 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
if dir_entry.is_empty() {
|
|
|
|
|
return next_impl(me);
|
|
|
|
|
}
|
2025-07-27 14:38:17 +02:00
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
if let Some((iter, n_slots)) = me
|
2025-08-02 00:36:35 +02:00
|
|
|
.long_filename_buf
|
|
|
|
|
.get_buf(dir_entry.checksum)
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
anyhow::anyhow!(
|
|
|
|
|
"failed to get long filename for {}: {}",
|
|
|
|
|
dir_entry.name_string(),
|
|
|
|
|
e
|
|
|
|
|
)
|
|
|
|
|
})?
|
|
|
|
|
{
|
|
|
|
|
// attach long filename to dir_entry
|
2025-07-27 14:38:17 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
let long_filename: CompactString =
|
|
|
|
|
char::decode_utf16(iter).filter_map(|x| x.ok()).collect();
|
2025-07-27 14:38:17 +02:00
|
|
|
|
2025-08-02 17:22:32 +02:00
|
|
|
dir_entry.set_long_name(long_filename, n_slots);
|
2025-08-02 00:36:35 +02:00
|
|
|
}
|
2025-07-27 14:38:17 +02:00
|
|
|
|
2025-08-02 00:36:35 +02:00
|
|
|
me.long_filename_buf.reset();
|
|
|
|
|
|
|
|
|
|
Ok(Some(dir_entry))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match next_impl(self) {
|
2025-07-27 14:38:17 +02:00
|
|
|
Ok(x) => x,
|
2025-07-27 02:04:21 +02:00
|
|
|
Err(e) => {
|
2025-07-30 21:39:45 +02:00
|
|
|
// print error message, try next
|
2025-08-02 17:22:32 +02:00
|
|
|
debug!("{}", e);
|
2025-07-27 14:38:17 +02:00
|
|
|
|
|
|
|
|
self.next()
|
2025-07-27 02:04:21 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
// let mut chunk = [0; 32];
|
|
|
|
|
// self.reader.read_exact(&mut chunk).ok()?;
|
|
|
|
|
|
|
|
|
|
// let dir_entry = match DirEntryWrapper::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 mut dir_entry = match dir_entry {
|
|
|
|
|
// DirEntryWrapper::Regular(dir_entry) => dir_entry,
|
|
|
|
|
// DirEntryWrapper::LongName(long_name) => {
|
|
|
|
|
// if let Err(e) = self.long_filename_buf.next(long_name) {
|
|
|
|
|
// self.long_filename_buf.reset();
|
|
|
|
|
//
|
|
|
|
|
// eprintln!("invalid long filename entry: {}", e);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// return self.next();
|
|
|
|
|
// }
|
|
|
|
|
// };
|
2025-07-27 02:04:21 +02:00
|
|
|
|
2025-07-27 14:38:17 +02:00
|
|
|
// if dir_entry.is_sentinel() {
|
|
|
|
|
// return None;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// if dir_entry.is_empty() {
|
|
|
|
|
// return self.next();
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// match self.long_filename_buf.get_buf(dir_entry.checksum()) {
|
|
|
|
|
// Ok(Some(iter)) => {
|
|
|
|
|
// // attach long filename to dir_entry
|
|
|
|
|
|
|
|
|
|
// let long_filename: String =
|
|
|
|
|
// char::decode_utf16(iter).filter_map(|x| x.ok()).collect();
|
|
|
|
|
|
|
|
|
|
// dir_entry.set_long_name(long_filename);
|
|
|
|
|
// }
|
|
|
|
|
// Ok(None) => {} // no long filename -> do nothing
|
|
|
|
|
// Err(e) => {
|
|
|
|
|
// eprintln!(
|
|
|
|
|
// "failed to get long filename for {}: {}",
|
|
|
|
|
// dir_entry.name_string().as_deref().unwrap_or("<invalid>"),
|
|
|
|
|
// e
|
|
|
|
|
// );
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// self.long_filename_buf.reset();
|
|
|
|
|
|
|
|
|
|
// Some(dir_entry)
|
2025-07-26 15:51:21 +02:00
|
|
|
}
|
|
|
|
|
}
|