use crate::fat::VolumeName;
use crate::trace;
#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
#[derive(Debug, Clone)]
pub enum FilenameError {
InvalidCharacter,
FilenameEmpty,
NameTooLong,
MisplacedPeriod,
Utf8Error,
}
pub trait ToShortFileName {
fn to_short_filename(self) -> Result<ShortFileName, FilenameError>;
}
impl ToShortFileName for ShortFileName {
fn to_short_filename(self) -> Result<ShortFileName, FilenameError> {
Ok(self)
}
}
impl ToShortFileName for &ShortFileName {
fn to_short_filename(self) -> Result<ShortFileName, FilenameError> {
Ok(self.clone())
}
}
impl ToShortFileName for &str {
fn to_short_filename(self) -> Result<ShortFileName, FilenameError> {
ShortFileName::create_from_str(self)
}
}
#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
#[derive(PartialEq, Eq, Clone)]
pub struct ShortFileName {
pub(crate) contents: [u8; Self::TOTAL_LEN],
}
impl ShortFileName {
const BASE_LEN: usize = 8;
const TOTAL_LEN: usize = 11;
pub const fn parent_dir() -> Self {
Self {
contents: *b".. ",
}
}
pub const fn this_dir() -> Self {
Self {
contents: *b". ",
}
}
pub fn base_name(&self) -> &[u8] {
Self::bytes_before_space(&self.contents[..Self::BASE_LEN])
}
pub fn extension(&self) -> &[u8] {
Self::bytes_before_space(&self.contents[Self::BASE_LEN..])
}
fn bytes_before_space(bytes: &[u8]) -> &[u8] {
bytes.split(|b| *b == b' ').next().unwrap_or(&[])
}
pub fn create_from_str(name: &str) -> Result<ShortFileName, FilenameError> {
let mut sfn = ShortFileName {
contents: [b' '; Self::TOTAL_LEN],
};
if name == ".." {
return Ok(ShortFileName::parent_dir());
}
if name.is_empty() || name == "." {
return Ok(ShortFileName::this_dir());
}
let mut idx = 0;
let mut seen_dot = false;
for ch in name.chars() {
match ch {
'\u{0000}'..='\u{001F}'
| '"'
| '*'
| '+'
| ','
| '/'
| ':'
| ';'
| '<'
| '='
| '>'
| '?'
| '['
| '\\'
| ']'
| ' '
| '|' => {
return Err(FilenameError::InvalidCharacter);
}
x if x > '\u{00FF}' => {
return Err(FilenameError::InvalidCharacter);
}
'.' => {
if (1..=Self::BASE_LEN).contains(&idx) {
idx = Self::BASE_LEN;
seen_dot = true;
} else {
return Err(FilenameError::MisplacedPeriod);
}
}
_ => {
let b = ch.to_ascii_uppercase() as u8;
if seen_dot {
if (Self::BASE_LEN..Self::TOTAL_LEN).contains(&idx) {
sfn.contents[idx] = b;
} else {
return Err(FilenameError::NameTooLong);
}
} else if idx < Self::BASE_LEN {
sfn.contents[idx] = b;
} else {
return Err(FilenameError::NameTooLong);
}
idx += 1;
}
}
}
if idx == 0 {
return Err(FilenameError::FilenameEmpty);
}
Ok(sfn)
}
pub unsafe fn to_volume_label(self) -> VolumeName {
VolumeName {
contents: self.contents,
}
}
pub fn csum(&self) -> u8 {
let mut result = 0u8;
for b in self.contents.iter() {
result = result.rotate_right(1).wrapping_add(*b);
}
result
}
}
impl core::fmt::Display for ShortFileName {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut printed = 0;
for (i, &c) in self.contents.iter().enumerate() {
if c != b' ' {
if i == Self::BASE_LEN {
write!(f, ".")?;
printed += 1;
}
write!(f, "{}", c as char)?;
printed += 1;
}
}
if let Some(mut width) = f.width() {
if width > printed {
width -= printed;
for _ in 0..width {
write!(f, "{}", f.fill())?;
}
}
}
Ok(())
}
}
impl core::fmt::Debug for ShortFileName {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "ShortFileName(\"{}\")", self)
}
}
#[derive(Debug)]
pub struct LfnBuffer<'a> {
inner: &'a mut [u8],
free: usize,
overflow: bool,
unpaired_surrogate: Option<u16>,
}
impl<'a> LfnBuffer<'a> {
pub fn new(storage: &'a mut [u8]) -> LfnBuffer<'a> {
let len = storage.len();
LfnBuffer {
inner: storage,
free: len,
overflow: false,
unpaired_surrogate: None,
}
}
pub fn clear(&mut self) {
self.free = self.inner.len();
self.overflow = false;
self.unpaired_surrogate = None;
}
pub fn push(&mut self, buffer: &[u16; 13]) {
let null_idx = buffer
.iter()
.position(|&b| b == 0x0000)
.unwrap_or(buffer.len());
let buffer = &buffer[0..null_idx];
let mut char_vec: heapless::Vec<char, 13> = heapless::Vec::new();
let mut is_first = true;
for ch in char::decode_utf16(
buffer
.iter()
.cloned()
.chain(self.unpaired_surrogate.take().iter().cloned()),
) {
match ch {
Ok(ch) => {
char_vec.push(ch).expect("Vec was full!?");
}
Err(e) => {
if is_first {
trace!("LFN saved {:?}", e.unpaired_surrogate());
self.unpaired_surrogate = Some(e.unpaired_surrogate());
} else {
trace!("LFN replaced {:?}", e.unpaired_surrogate());
char_vec.push('\u{fffd}').expect("Vec was full?!");
}
}
}
is_first = false;
}
for ch in char_vec.iter().rev() {
trace!("LFN push {:?}", ch);
let mut encoded_ch = [0u8; 4];
let encoded_ch = ch.encode_utf8(&mut encoded_ch);
if self.free < encoded_ch.len() {
self.overflow = true;
return;
}
for b in encoded_ch.bytes().rev() {
self.free -= 1;
self.inner[self.free] = b;
}
}
}
pub fn as_str(&self) -> &str {
if self.overflow {
""
} else {
unsafe { core::str::from_utf8_unchecked(&self.inner[self.free..]) }
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn filename_no_extension() {
let sfn = ShortFileName {
contents: *b"HELLO ",
};
assert_eq!(format!("{}", &sfn), "HELLO");
assert_eq!(sfn, ShortFileName::create_from_str("HELLO").unwrap());
assert_eq!(sfn, ShortFileName::create_from_str("hello").unwrap());
assert_eq!(sfn, ShortFileName::create_from_str("HeLlO").unwrap());
assert_eq!(sfn, ShortFileName::create_from_str("HELLO.").unwrap());
}
#[test]
fn filename_extension() {
let sfn = ShortFileName {
contents: *b"HELLO TXT",
};
assert_eq!(format!("{}", &sfn), "HELLO.TXT");
assert_eq!(sfn, ShortFileName::create_from_str("HELLO.TXT").unwrap());
}
#[test]
fn filename_get_extension() {
let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap();
assert_eq!(sfn.extension(), "TXT".as_bytes());
sfn = ShortFileName::create_from_str("hello").unwrap();
assert_eq!(sfn.extension(), "".as_bytes());
sfn = ShortFileName::create_from_str("hello.a").unwrap();
assert_eq!(sfn.extension(), "A".as_bytes());
}
#[test]
fn filename_get_base_name() {
let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap();
assert_eq!(sfn.base_name(), "HELLO".as_bytes());
sfn = ShortFileName::create_from_str("12345678").unwrap();
assert_eq!(sfn.base_name(), "12345678".as_bytes());
sfn = ShortFileName::create_from_str("1").unwrap();
assert_eq!(sfn.base_name(), "1".as_bytes());
}
#[test]
fn filename_fulllength() {
let sfn = ShortFileName {
contents: *b"12345678TXT",
};
assert_eq!(format!("{}", &sfn), "12345678.TXT");
assert_eq!(sfn, ShortFileName::create_from_str("12345678.TXT").unwrap());
}
#[test]
fn filename_short_extension() {
let sfn = ShortFileName {
contents: *b"12345678C ",
};
assert_eq!(format!("{}", &sfn), "12345678.C");
assert_eq!(sfn, ShortFileName::create_from_str("12345678.C").unwrap());
}
#[test]
fn filename_short() {
let sfn = ShortFileName {
contents: *b"1 C ",
};
assert_eq!(format!("{}", &sfn), "1.C");
assert_eq!(sfn, ShortFileName::create_from_str("1.C").unwrap());
}
#[test]
fn filename_empty() {
assert_eq!(
ShortFileName::create_from_str("").unwrap(),
ShortFileName::this_dir()
);
}
#[test]
fn filename_bad() {
assert!(ShortFileName::create_from_str(" ").is_err());
assert!(ShortFileName::create_from_str("123456789").is_err());
assert!(ShortFileName::create_from_str("12345678.ABCD").is_err());
}
#[test]
fn checksum() {
assert_eq!(
0xB3,
ShortFileName::create_from_str("UNARCH~1.DAT")
.unwrap()
.csum()
);
}
#[test]
fn one_piece() {
let mut storage = [0u8; 64];
let mut buf: LfnBuffer = LfnBuffer::new(&mut storage);
buf.push(&[
0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0xFFFF, 0xFFFF,
]);
assert_eq!(buf.as_str(), "0123∂");
}
#[test]
fn two_piece() {
let mut storage = [0u8; 64];
let mut buf: LfnBuffer = LfnBuffer::new(&mut storage);
buf.push(&[
0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0xFFFF, 0xFFFF,
]);
buf.push(&[
0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004a, 0x004b,
0x004c, 0x004d,
]);
assert_eq!(buf.as_str(), "ABCDEFGHIJKLM0123∂");
}
#[test]
fn two_piece_split_surrogate() {
let mut storage = [0u8; 64];
let mut buf: LfnBuffer = LfnBuffer::new(&mut storage);
buf.push(&[
0xde00, 0x002e, 0x0074, 0x0078, 0x0074, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
0xffff, 0xffff,
]);
buf.push(&[
0xd83d, 0xde00, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038,
0x0039, 0xd83d,
]);
assert_eq!(buf.as_str(), "😀0123456789😀.txt");
}
}