embedded_fat/filesystem/
filename.rs

1//! Filename related types
2
3/// Various filename related errors that can occur.
4#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
5#[derive(Debug, Clone)]
6pub enum FilenameError {
7    /// Tried to create a file with an invalid character.
8    InvalidCharacter,
9    /// Tried to create a file with no file name.
10    FilenameEmpty,
11    /// Given name was too long (we are limited to 8.3).
12    NameTooLong,
13    /// Can't start a file with a period, or after 8 characters.
14    MisplacedPeriod,
15    /// Can't extract utf8 from file name
16    Utf8Error,
17}
18
19/// Describes things we can convert to short 8.3 filenames
20pub trait ToShortFileName {
21    /// Try and convert this value into a [`ShortFileName`].
22    fn to_short_filename(self) -> Result<ShortFileName, FilenameError>;
23}
24
25impl ToShortFileName for ShortFileName {
26    fn to_short_filename(self) -> Result<ShortFileName, FilenameError> {
27        Ok(self)
28    }
29}
30
31impl ToShortFileName for &ShortFileName {
32    fn to_short_filename(self) -> Result<ShortFileName, FilenameError> {
33        Ok(self.clone())
34    }
35}
36
37impl ToShortFileName for &str {
38    fn to_short_filename(self) -> Result<ShortFileName, FilenameError> {
39        ShortFileName::create_from_str(self)
40    }
41}
42
43/// An MS-DOS 8.3 filename. 7-bit ASCII only. All lower-case is converted to
44/// upper-case by default.
45#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
46#[derive(PartialEq, Eq, Clone)]
47pub struct ShortFileName {
48    pub(crate) contents: [u8; 11],
49}
50
51impl ShortFileName {
52    const FILENAME_BASE_MAX_LEN: usize = 8;
53    const FILENAME_MAX_LEN: usize = 11;
54
55    /// Get a short file name containing "..", which means "parent directory".
56    pub const fn parent_dir() -> Self {
57        Self {
58            contents: *b"..         ",
59        }
60    }
61
62    /// Get a short file name containing "..", which means "this directory".
63    pub const fn this_dir() -> Self {
64        Self {
65            contents: *b".          ",
66        }
67    }
68
69    /// Get base name (name without extension) of file name
70    pub fn base_name(&self) -> &[u8] {
71        Self::bytes_before_space(&self.contents[..Self::FILENAME_BASE_MAX_LEN])
72    }
73
74    /// Get base name (name without extension) of file name
75    pub fn extension(&self) -> &[u8] {
76        Self::bytes_before_space(&self.contents[Self::FILENAME_BASE_MAX_LEN..])
77    }
78
79    fn bytes_before_space(bytes: &[u8]) -> &[u8] {
80        bytes.split(|b| *b == b' ').next().unwrap_or(&bytes[0..0])
81    }
82
83    /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry.
84    pub fn create_from_str(name: &str) -> Result<ShortFileName, FilenameError> {
85        let mut sfn = ShortFileName {
86            contents: [b' '; Self::FILENAME_MAX_LEN],
87        };
88        let mut idx = 0;
89        let mut seen_dot = false;
90        for ch in name.bytes() {
91            match ch {
92                // Microsoft say these are the invalid characters
93                0x00..=0x1F
94                | 0x20
95                | 0x22
96                | 0x2A
97                | 0x2B
98                | 0x2C
99                | 0x2F
100                | 0x3A
101                | 0x3B
102                | 0x3C
103                | 0x3D
104                | 0x3E
105                | 0x3F
106                | 0x5B
107                | 0x5C
108                | 0x5D
109                | 0x7C => {
110                    return Err(FilenameError::InvalidCharacter);
111                }
112                // Denotes the start of the file extension
113                b'.' => {
114                    if (1..=Self::FILENAME_BASE_MAX_LEN).contains(&idx) {
115                        idx = Self::FILENAME_BASE_MAX_LEN;
116                        seen_dot = true;
117                    } else {
118                        return Err(FilenameError::MisplacedPeriod);
119                    }
120                }
121                _ => {
122                    let ch = ch.to_ascii_uppercase();
123                    if seen_dot {
124                        if (Self::FILENAME_BASE_MAX_LEN..Self::FILENAME_MAX_LEN).contains(&idx) {
125                            sfn.contents[idx] = ch;
126                        } else {
127                            return Err(FilenameError::NameTooLong);
128                        }
129                    } else if idx < Self::FILENAME_BASE_MAX_LEN {
130                        sfn.contents[idx] = ch;
131                    } else {
132                        return Err(FilenameError::NameTooLong);
133                    }
134                    idx += 1;
135                }
136            }
137        }
138        if idx == 0 {
139            return Err(FilenameError::FilenameEmpty);
140        }
141        Ok(sfn)
142    }
143
144    /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry.
145    /// Use this for volume labels with mixed case.
146    pub fn create_from_str_mixed_case(name: &str) -> Result<ShortFileName, FilenameError> {
147        let mut sfn = ShortFileName {
148            contents: [b' '; Self::FILENAME_MAX_LEN],
149        };
150        let mut idx = 0;
151        let mut seen_dot = false;
152        for ch in name.bytes() {
153            match ch {
154                // Microsoft say these are the invalid characters
155                0x00..=0x1F
156                | 0x20
157                | 0x22
158                | 0x2A
159                | 0x2B
160                | 0x2C
161                | 0x2F
162                | 0x3A
163                | 0x3B
164                | 0x3C
165                | 0x3D
166                | 0x3E
167                | 0x3F
168                | 0x5B
169                | 0x5C
170                | 0x5D
171                | 0x7C => {
172                    return Err(FilenameError::InvalidCharacter);
173                }
174                // Denotes the start of the file extension
175                b'.' => {
176                    if (1..=Self::FILENAME_BASE_MAX_LEN).contains(&idx) {
177                        idx = Self::FILENAME_BASE_MAX_LEN;
178                        seen_dot = true;
179                    } else {
180                        return Err(FilenameError::MisplacedPeriod);
181                    }
182                }
183                _ => {
184                    if seen_dot {
185                        if (Self::FILENAME_BASE_MAX_LEN..Self::FILENAME_MAX_LEN).contains(&idx) {
186                            sfn.contents[idx] = ch;
187                        } else {
188                            return Err(FilenameError::NameTooLong);
189                        }
190                    } else if idx < Self::FILENAME_BASE_MAX_LEN {
191                        sfn.contents[idx] = ch;
192                    } else {
193                        return Err(FilenameError::NameTooLong);
194                    }
195                    idx += 1;
196                }
197            }
198        }
199        if idx == 0 {
200            return Err(FilenameError::FilenameEmpty);
201        }
202        Ok(sfn)
203    }
204
205    #[allow(missing_docs)]
206    pub fn lfn_checksum(&self) -> u8 {
207        let mut sum = 0u8;
208        for b in &self.contents {
209            sum = sum.rotate_right(1).wrapping_add(*b);
210        }
211        sum
212    }
213}
214
215impl core::fmt::Display for ShortFileName {
216    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
217        let mut printed = 0;
218        for (i, &c) in self.contents.iter().enumerate() {
219            if c != b' ' {
220                if i == Self::FILENAME_BASE_MAX_LEN {
221                    write!(f, ".")?;
222                    printed += 1;
223                }
224                write!(f, "{}", c as char)?;
225                printed += 1;
226            }
227        }
228        if let Some(mut width) = f.width() {
229            if width > printed {
230                width -= printed;
231                for _ in 0..width {
232                    write!(f, "{}", f.fill())?;
233                }
234            }
235        }
236        Ok(())
237    }
238}
239
240impl core::fmt::Debug for ShortFileName {
241    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
242        write!(f, "ShortFileName(\"{}\")", self)
243    }
244}
245
246// ****************************************************************************
247//
248// Unit Tests
249//
250// ****************************************************************************
251
252#[cfg(test)]
253mod test {
254    use super::*;
255
256    #[test]
257    fn filename_no_extension() {
258        let sfn = ShortFileName {
259            contents: *b"HELLO      ",
260        };
261        assert_eq!(format!("{}", &sfn), "HELLO");
262        assert_eq!(sfn, ShortFileName::create_from_str("HELLO").unwrap());
263        assert_eq!(sfn, ShortFileName::create_from_str("hello").unwrap());
264        assert_eq!(sfn, ShortFileName::create_from_str("HeLlO").unwrap());
265        assert_eq!(sfn, ShortFileName::create_from_str("HELLO.").unwrap());
266    }
267
268    #[test]
269    fn filename_extension() {
270        let sfn = ShortFileName {
271            contents: *b"HELLO   TXT",
272        };
273        assert_eq!(format!("{}", &sfn), "HELLO.TXT");
274        assert_eq!(sfn, ShortFileName::create_from_str("HELLO.TXT").unwrap());
275    }
276
277    #[test]
278    fn filename_get_extension() {
279        let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap();
280        assert_eq!(sfn.extension(), "TXT".as_bytes());
281        sfn = ShortFileName::create_from_str("hello").unwrap();
282        assert_eq!(sfn.extension(), "".as_bytes());
283        sfn = ShortFileName::create_from_str("hello.a").unwrap();
284        assert_eq!(sfn.extension(), "A".as_bytes());
285    }
286
287    #[test]
288    fn filename_get_base_name() {
289        let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap();
290        assert_eq!(sfn.base_name(), "HELLO".as_bytes());
291        sfn = ShortFileName::create_from_str("12345678").unwrap();
292        assert_eq!(sfn.base_name(), "12345678".as_bytes());
293        sfn = ShortFileName::create_from_str("1").unwrap();
294        assert_eq!(sfn.base_name(), "1".as_bytes());
295    }
296
297    #[test]
298    fn filename_fulllength() {
299        let sfn = ShortFileName {
300            contents: *b"12345678TXT",
301        };
302        assert_eq!(format!("{}", &sfn), "12345678.TXT");
303        assert_eq!(sfn, ShortFileName::create_from_str("12345678.TXT").unwrap());
304    }
305
306    #[test]
307    fn filename_short_extension() {
308        let sfn = ShortFileName {
309            contents: *b"12345678C  ",
310        };
311        assert_eq!(format!("{}", &sfn), "12345678.C");
312        assert_eq!(sfn, ShortFileName::create_from_str("12345678.C").unwrap());
313    }
314
315    #[test]
316    fn filename_short() {
317        let sfn = ShortFileName {
318            contents: *b"1       C  ",
319        };
320        assert_eq!(format!("{}", &sfn), "1.C");
321        assert_eq!(sfn, ShortFileName::create_from_str("1.C").unwrap());
322    }
323
324    #[test]
325    fn filename_bad() {
326        assert!(ShortFileName::create_from_str("").is_err());
327        assert!(ShortFileName::create_from_str(" ").is_err());
328        assert!(ShortFileName::create_from_str("123456789").is_err());
329        assert!(ShortFileName::create_from_str("12345678.ABCD").is_err());
330    }
331}