Skip to main content

embedded_sdmmc/filesystem/
filename.rs

1//! Filename related types
2
3use crate::fat::VolumeName;
4use crate::trace;
5
6/// Various filename related errors that can occur.
7#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum FilenameError {
10    /// Tried to create a file with an invalid character.
11    InvalidCharacter,
12    /// Tried to create a file with no file name.
13    FilenameEmpty,
14    /// Given name was too long (we are limited to 8.3).
15    NameTooLong,
16    /// Can't start a file with a period, or after 8 characters.
17    MisplacedPeriod,
18    /// Can't extract utf8 from file name
19    Utf8Error,
20}
21
22/// Describes things we can convert to short 8.3 filenames
23pub trait ToShortFileName {
24    /// Try and convert this value into a [`ShortFileName`].
25    fn to_short_filename(self) -> Result<ShortFileName, FilenameError>;
26}
27
28impl ToShortFileName for ShortFileName {
29    fn to_short_filename(self) -> Result<ShortFileName, FilenameError> {
30        Ok(self)
31    }
32}
33
34impl ToShortFileName for &ShortFileName {
35    fn to_short_filename(self) -> Result<ShortFileName, FilenameError> {
36        Ok(*self)
37    }
38}
39
40impl ToShortFileName for &str {
41    fn to_short_filename(self) -> Result<ShortFileName, FilenameError> {
42        ShortFileName::create_from_str(self)
43    }
44}
45
46/// An MS-DOS 8.3 filename.
47///
48/// ISO-8859-1 encoding is assumed. All lower-case is converted to upper-case by
49/// default.
50#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
51#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
52pub struct ShortFileName {
53    pub(crate) contents: [u8; Self::TOTAL_LEN],
54}
55
56impl ShortFileName {
57    const BASE_LEN: usize = 8;
58    const TOTAL_LEN: usize = 11;
59
60    /// Get a short file name containing "..", which means "parent directory".
61    pub const fn parent_dir() -> Self {
62        Self {
63            contents: *b"..         ",
64        }
65    }
66
67    /// Get a short file name containing ".", which means "this directory".
68    pub const fn this_dir() -> Self {
69        Self {
70            contents: *b".          ",
71        }
72    }
73
74    /// Get base name (without extension) of the file.
75    pub fn base_name(&self) -> &[u8] {
76        Self::bytes_before_space(&self.contents[..Self::BASE_LEN])
77    }
78
79    /// Get extension of the file (without base name).
80    pub fn extension(&self) -> &[u8] {
81        Self::bytes_before_space(&self.contents[Self::BASE_LEN..])
82    }
83
84    fn bytes_before_space(bytes: &[u8]) -> &[u8] {
85        bytes.split(|b| *b == b' ').next().unwrap_or(&[])
86    }
87
88    /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry.
89    ///
90    /// The output uses ISO-8859-1 encoding.
91    pub fn create_from_str(name: &str) -> Result<ShortFileName, FilenameError> {
92        let mut sfn = ShortFileName {
93            contents: [b' '; Self::TOTAL_LEN],
94        };
95
96        // Special case `..`, which means "parent directory".
97        if name == ".." {
98            return Ok(ShortFileName::parent_dir());
99        }
100
101        // Special case `.` (or blank), which means "this directory".
102        if name.is_empty() || name == "." {
103            return Ok(ShortFileName::this_dir());
104        }
105
106        let mut idx = 0;
107        let mut seen_dot = false;
108        for ch in name.chars() {
109            match ch {
110                // Microsoft say these are the invalid characters
111                '\u{0000}'..='\u{001F}'
112                | '"'
113                | '*'
114                | '+'
115                | ','
116                | '/'
117                | ':'
118                | ';'
119                | '<'
120                | '='
121                | '>'
122                | '?'
123                | '['
124                | '\\'
125                | ']'
126                | ' '
127                | '|' => {
128                    return Err(FilenameError::InvalidCharacter);
129                }
130                x if x > '\u{00FF}' => {
131                    // We only handle ISO-8859-1 which is Unicode Code Points
132                    // \U+0000 to \U+00FF. This is above that.
133                    return Err(FilenameError::InvalidCharacter);
134                }
135                '.' => {
136                    // Denotes the start of the file extension
137                    if (1..=Self::BASE_LEN).contains(&idx) {
138                        idx = Self::BASE_LEN;
139                        seen_dot = true;
140                    } else {
141                        return Err(FilenameError::MisplacedPeriod);
142                    }
143                }
144                _ => {
145                    let b = ch.to_ascii_uppercase() as u8;
146                    if seen_dot {
147                        if (Self::BASE_LEN..Self::TOTAL_LEN).contains(&idx) {
148                            sfn.contents[idx] = b;
149                        } else {
150                            return Err(FilenameError::NameTooLong);
151                        }
152                    } else if idx < Self::BASE_LEN {
153                        sfn.contents[idx] = b;
154                    } else {
155                        return Err(FilenameError::NameTooLong);
156                    }
157                    idx += 1;
158                }
159            }
160        }
161        if idx == 0 {
162            return Err(FilenameError::FilenameEmpty);
163        }
164        Ok(sfn)
165    }
166
167    /// Convert a Short File Name to a Volume Label.
168    ///
169    /// # Safety
170    ///
171    /// Volume Labels can contain things that Short File Names cannot, so only
172    /// do this conversion if you have the name of a directory entry with the
173    /// 'Volume Label' attribute.
174    pub unsafe fn to_volume_label(self) -> VolumeName {
175        VolumeName {
176            contents: self.contents,
177        }
178    }
179
180    /// Get the LFN checksum for this short filename
181    pub fn csum(&self) -> u8 {
182        let mut result = 0u8;
183        for b in self.contents.iter() {
184            result = result.rotate_right(1).wrapping_add(*b);
185        }
186        result
187    }
188}
189
190impl core::fmt::Display for ShortFileName {
191    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
192        let mut printed = 0;
193        for (i, &c) in self.contents.iter().enumerate() {
194            if c != b' ' {
195                if i == Self::BASE_LEN {
196                    write!(f, ".")?;
197                    printed += 1;
198                }
199                // converting a byte to a codepoint means you are assuming
200                // ISO-8859-1 encoding, because that's how Unicode was designed.
201                write!(f, "{}", c as char)?;
202                printed += 1;
203            }
204        }
205        if let Some(mut width) = f.width() {
206            if width > printed {
207                width -= printed;
208                for _ in 0..width {
209                    write!(f, "{}", f.fill())?;
210                }
211            }
212        }
213        Ok(())
214    }
215}
216
217impl core::fmt::Debug for ShortFileName {
218    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
219        write!(f, "ShortFileName(\"{}\")", self)
220    }
221}
222
223/// Used to store a Long File Name
224#[derive(Debug)]
225pub struct LfnBuffer<'a> {
226    /// We fill this buffer in from the back
227    inner: &'a mut [u8],
228    /// How many bytes are free.
229    ///
230    /// This is also the byte index the string starts from.
231    free: u16,
232    /// Did we overflow?
233    overflow: bool,
234    /// If a surrogate-pair is split over two directory entries, remember half of it here.
235    unpaired_surrogate: Option<u16>,
236}
237
238impl<'a> LfnBuffer<'a> {
239    /// Create a new, empty, LFN Buffer using the given mutable slice as its storage.
240    pub fn new(storage: &'a mut [u8]) -> Self {
241        // Because `free` is a `u16`, we keep at most `u16::MAX` bytes of the buffer.
242        // It is enough to hold all LFN because a LFN has at most 255 characters.
243        // A UTF-8 character takes at most 3 bytes.
244        // Thus, a buffer of 765 (255*3) bytes is able to represent any LFN.
245        let len = storage.len().min(usize::from(u16::MAX));
246        LfnBuffer {
247            inner: &mut storage[..len],
248            free: len as u16,
249            overflow: false,
250            unpaired_surrogate: None,
251        }
252    }
253
254    /// Returns [`Self::free`] casted to `usize`.
255    fn free(&self) -> usize {
256        usize::from(self.free)
257    }
258
259    /// Empty out this buffer
260    pub fn clear(&mut self) {
261        self.free = self.inner.len() as u16;
262        self.overflow = false;
263        self.unpaired_surrogate = None;
264    }
265
266    /// Push the 13 UTF-16 codepoints into this string.
267    ///
268    /// We assume they are pushed last-chunk-first, as you would find
269    /// them on disk.
270    ///
271    /// Any chunk starting with a half of a surrogate pair has that saved for the next call.
272    ///
273    /// ```text
274    /// [de00, 002e, 0074, 0078, 0074, 0000, ffff, ffff, ffff, ffff, ffff, ffff, ffff]
275    /// [0041, 0042, 0030, 0031, 0032, 0033, 0034, 0035, 0036, 0037, 0038, 0039, d83d]
276    ///
277    /// Would map to
278    ///
279    /// 0041 0042 0030 0031 0032 0033 0034 0035 0036 0037 0038 0039 1f600 002e 0074 0078 0074, or
280    ///
281    /// "AB0123456789😀.txt"
282    /// ```
283    pub fn push(&mut self, buffer: &[u16; 13]) {
284        // find the first null, if any
285        let null_idx = buffer
286            .iter()
287            .position(|&b| b == 0x0000)
288            .unwrap_or(buffer.len());
289        // take all the wide chars, up to the null (or go to the end)
290        let buffer = &buffer[0..null_idx];
291
292        // This next part will convert the 16-bit values into chars, noting that
293        // chars outside the Basic Multilingual Plane will require two 16-bit
294        // values to encode (see UTF-16 Surrogate Pairs).
295        //
296        // We cache the decoded chars into this array so we can iterate them
297        // backwards. It's 60 bytes, but it'll have to do.
298        let mut char_vec: heapless::Vec<char, 13> = heapless::Vec::new();
299        // Now do the decode, including the unpaired surrogate (if any) from
300        // last time (maybe it has a pair now!)
301        let mut is_first = true;
302        for ch in char::decode_utf16(
303            buffer
304                .iter()
305                .cloned()
306                .chain(self.unpaired_surrogate.take().iter().cloned()),
307        ) {
308            match ch {
309                Ok(ch) => {
310                    char_vec.push(ch).expect("Vec was full!?");
311                }
312                Err(e) => {
313                    // OK, so we found half a surrogate pair and nothing to go
314                    // with it. Was this the first codepoint in the chunk?
315                    if is_first {
316                        // it was - the other half is probably in the next chunk
317                        // so save this for next time
318                        trace!("LFN saved {:?}", e.unpaired_surrogate());
319                        self.unpaired_surrogate = Some(e.unpaired_surrogate());
320                    } else {
321                        // it wasn't - can't deal with it these mid-sequence, so
322                        // replace it
323                        trace!("LFN replaced {:?}", e.unpaired_surrogate());
324                        char_vec.push('\u{fffd}').expect("Vec was full?!");
325                    }
326                }
327            }
328            is_first = false;
329        }
330
331        for ch in char_vec.iter().rev() {
332            trace!("LFN push {:?}", ch);
333            // a buffer of length 4 is enough to encode any char
334            let mut encoded_ch = [0u8; 4];
335            let encoded_ch = ch.encode_utf8(&mut encoded_ch);
336            if self.free() < encoded_ch.len() {
337                // the LFN buffer they gave us was not long enough. Note for
338                // later, so we don't show them garbage.
339                self.overflow = true;
340                return;
341            }
342            // Store the encoded char in the buffer, working backwards. We
343            // already checked there was enough space.
344            for b in encoded_ch.bytes().rev() {
345                self.free -= 1;
346                self.inner[self.free()] = b;
347            }
348        }
349    }
350
351    /// View this LFN buffer as a string-slice
352    ///
353    /// If the buffer overflowed while parsing the LFN, or if this buffer is
354    /// empty, you get an empty string.
355    pub fn as_str(&self) -> &str {
356        if self.overflow {
357            ""
358        } else {
359            // we always only put UTF-8 encoded data in here
360            unsafe { core::str::from_utf8_unchecked(&self.inner[self.free()..]) }
361        }
362    }
363}
364
365// ****************************************************************************
366//
367// Unit Tests
368//
369// ****************************************************************************
370
371#[cfg(test)]
372mod test {
373    use super::*;
374
375    #[test]
376    fn filename_no_extension() {
377        let sfn = ShortFileName {
378            contents: *b"HELLO      ",
379        };
380        assert_eq!(format!("{}", &sfn), "HELLO");
381        assert_eq!(sfn, ShortFileName::create_from_str("HELLO").unwrap());
382        assert_eq!(sfn, ShortFileName::create_from_str("hello").unwrap());
383        assert_eq!(sfn, ShortFileName::create_from_str("HeLlO").unwrap());
384        assert_eq!(sfn, ShortFileName::create_from_str("HELLO.").unwrap());
385    }
386
387    #[test]
388    fn filename_extension() {
389        let sfn = ShortFileName {
390            contents: *b"HELLO   TXT",
391        };
392        assert_eq!(format!("{}", &sfn), "HELLO.TXT");
393        assert_eq!(sfn, ShortFileName::create_from_str("HELLO.TXT").unwrap());
394    }
395
396    #[test]
397    fn filename_get_extension() {
398        let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap();
399        assert_eq!(sfn.extension(), "TXT".as_bytes());
400        sfn = ShortFileName::create_from_str("hello").unwrap();
401        assert_eq!(sfn.extension(), "".as_bytes());
402        sfn = ShortFileName::create_from_str("hello.a").unwrap();
403        assert_eq!(sfn.extension(), "A".as_bytes());
404    }
405
406    #[test]
407    fn filename_get_base_name() {
408        let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap();
409        assert_eq!(sfn.base_name(), "HELLO".as_bytes());
410        sfn = ShortFileName::create_from_str("12345678").unwrap();
411        assert_eq!(sfn.base_name(), "12345678".as_bytes());
412        sfn = ShortFileName::create_from_str("1").unwrap();
413        assert_eq!(sfn.base_name(), "1".as_bytes());
414    }
415
416    #[test]
417    fn filename_fulllength() {
418        let sfn = ShortFileName {
419            contents: *b"12345678TXT",
420        };
421        assert_eq!(format!("{}", &sfn), "12345678.TXT");
422        assert_eq!(sfn, ShortFileName::create_from_str("12345678.TXT").unwrap());
423    }
424
425    #[test]
426    fn filename_short_extension() {
427        let sfn = ShortFileName {
428            contents: *b"12345678C  ",
429        };
430        assert_eq!(format!("{}", &sfn), "12345678.C");
431        assert_eq!(sfn, ShortFileName::create_from_str("12345678.C").unwrap());
432    }
433
434    #[test]
435    fn filename_short() {
436        let sfn = ShortFileName {
437            contents: *b"1       C  ",
438        };
439        assert_eq!(format!("{}", &sfn), "1.C");
440        assert_eq!(sfn, ShortFileName::create_from_str("1.C").unwrap());
441    }
442
443    #[test]
444    fn filename_ordering() {
445        assert!(
446            ShortFileName::create_from_str("1.C").unwrap()
447                < ShortFileName::create_from_str("2.C").unwrap()
448        );
449        assert!(
450            ShortFileName::create_from_str("1.C").unwrap()
451                < ShortFileName::create_from_str("1.D").unwrap()
452        );
453        assert!(
454            ShortFileName::create_from_str("12.C").unwrap()
455                < ShortFileName::create_from_str("3.C").unwrap()
456        );
457        assert!(
458            ShortFileName::create_from_str("1.D").unwrap()
459                < ShortFileName::create_from_str("12.C").unwrap()
460        );
461        assert_eq!(
462            ShortFileName::create_from_str("1.D")
463                .unwrap()
464                .cmp(&ShortFileName::create_from_str("1.D").unwrap()),
465            core::cmp::Ordering::Equal
466        );
467        assert!(
468            ShortFileName::create_from_str("1").unwrap()
469                < ShortFileName::create_from_str("1.C").unwrap()
470        );
471        assert!(
472            ShortFileName::create_from_str("1.C").unwrap()
473                < ShortFileName::create_from_str("2").unwrap()
474        );
475    }
476
477    #[test]
478    fn filename_empty() {
479        assert_eq!(
480            ShortFileName::create_from_str("").unwrap(),
481            ShortFileName::this_dir()
482        );
483    }
484
485    #[test]
486    fn filename_bad() {
487        assert!(ShortFileName::create_from_str(" ").is_err());
488        assert!(ShortFileName::create_from_str("123456789").is_err());
489        assert!(ShortFileName::create_from_str("12345678.ABCD").is_err());
490    }
491
492    #[test]
493    fn checksum() {
494        assert_eq!(
495            0xB3,
496            ShortFileName::create_from_str("UNARCH~1.DAT")
497                .unwrap()
498                .csum()
499        );
500    }
501
502    #[test]
503    fn one_piece() {
504        let mut storage = [0u8; 64];
505        let mut buf: LfnBuffer = LfnBuffer::new(&mut storage);
506        buf.push(&[
507            0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
508            0xFFFF, 0xFFFF,
509        ]);
510        assert_eq!(buf.as_str(), "0123∂");
511    }
512
513    #[test]
514    fn two_piece() {
515        let mut storage = [0u8; 64];
516        let mut buf: LfnBuffer = LfnBuffer::new(&mut storage);
517        buf.push(&[
518            0x0030, 0x0031, 0x0032, 0x0033, 0x2202, 0x0000, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
519            0xFFFF, 0xFFFF,
520        ]);
521        buf.push(&[
522            0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004a, 0x004b,
523            0x004c, 0x004d,
524        ]);
525        assert_eq!(buf.as_str(), "ABCDEFGHIJKLM0123∂");
526    }
527
528    #[test]
529    fn two_piece_split_surrogate() {
530        let mut storage = [0u8; 64];
531        let mut buf: LfnBuffer = LfnBuffer::new(&mut storage);
532
533        buf.push(&[
534            0xde00, 0x002e, 0x0074, 0x0078, 0x0074, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
535            0xffff, 0xffff,
536        ]);
537        buf.push(&[
538            0xd83d, 0xde00, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038,
539            0x0039, 0xd83d,
540        ]);
541        assert_eq!(buf.as_str(), "😀0123456789😀.txt");
542    }
543}
544
545// ****************************************************************************
546//
547// End Of File
548//
549// ****************************************************************************