embedded_sdmmc/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 (without extension) of the file.
70    pub fn base_name(&self) -> &[u8] {
71        Self::bytes_before_space(&self.contents[..Self::FILENAME_BASE_MAX_LEN])
72    }
73
74    /// Get extension of the file (without base 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
89        // Special case `..`, which means "parent directory".
90        if name == ".." {
91            return Ok(ShortFileName::parent_dir());
92        }
93
94        // Special case `.` (or blank), which means "this directory".
95        if name.is_empty() || name == "." {
96            return Ok(ShortFileName::this_dir());
97        }
98
99        let mut idx = 0;
100        let mut seen_dot = false;
101        for ch in name.bytes() {
102            match ch {
103                // Microsoft say these are the invalid characters
104                0x00..=0x1F
105                | 0x20
106                | 0x22
107                | 0x2A
108                | 0x2B
109                | 0x2C
110                | 0x2F
111                | 0x3A
112                | 0x3B
113                | 0x3C
114                | 0x3D
115                | 0x3E
116                | 0x3F
117                | 0x5B
118                | 0x5C
119                | 0x5D
120                | 0x7C => {
121                    return Err(FilenameError::InvalidCharacter);
122                }
123                // Denotes the start of the file extension
124                b'.' => {
125                    if (1..=Self::FILENAME_BASE_MAX_LEN).contains(&idx) {
126                        idx = Self::FILENAME_BASE_MAX_LEN;
127                        seen_dot = true;
128                    } else {
129                        return Err(FilenameError::MisplacedPeriod);
130                    }
131                }
132                _ => {
133                    let ch = ch.to_ascii_uppercase();
134                    if seen_dot {
135                        if (Self::FILENAME_BASE_MAX_LEN..Self::FILENAME_MAX_LEN).contains(&idx) {
136                            sfn.contents[idx] = ch;
137                        } else {
138                            return Err(FilenameError::NameTooLong);
139                        }
140                    } else if idx < Self::FILENAME_BASE_MAX_LEN {
141                        sfn.contents[idx] = ch;
142                    } else {
143                        return Err(FilenameError::NameTooLong);
144                    }
145                    idx += 1;
146                }
147            }
148        }
149        if idx == 0 {
150            return Err(FilenameError::FilenameEmpty);
151        }
152        Ok(sfn)
153    }
154
155    /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry.
156    /// Use this for volume labels with mixed case.
157    pub fn create_from_str_mixed_case(name: &str) -> Result<ShortFileName, FilenameError> {
158        let mut sfn = ShortFileName {
159            contents: [b' '; Self::FILENAME_MAX_LEN],
160        };
161        let mut idx = 0;
162        let mut seen_dot = false;
163        for ch in name.bytes() {
164            match ch {
165                // Microsoft say these are the invalid characters
166                0x00..=0x1F
167                | 0x20
168                | 0x22
169                | 0x2A
170                | 0x2B
171                | 0x2C
172                | 0x2F
173                | 0x3A
174                | 0x3B
175                | 0x3C
176                | 0x3D
177                | 0x3E
178                | 0x3F
179                | 0x5B
180                | 0x5C
181                | 0x5D
182                | 0x7C => {
183                    return Err(FilenameError::InvalidCharacter);
184                }
185                // Denotes the start of the file extension
186                b'.' => {
187                    if (1..=Self::FILENAME_BASE_MAX_LEN).contains(&idx) {
188                        idx = Self::FILENAME_BASE_MAX_LEN;
189                        seen_dot = true;
190                    } else {
191                        return Err(FilenameError::MisplacedPeriod);
192                    }
193                }
194                _ => {
195                    if seen_dot {
196                        if (Self::FILENAME_BASE_MAX_LEN..Self::FILENAME_MAX_LEN).contains(&idx) {
197                            sfn.contents[idx] = ch;
198                        } else {
199                            return Err(FilenameError::NameTooLong);
200                        }
201                    } else if idx < Self::FILENAME_BASE_MAX_LEN {
202                        sfn.contents[idx] = ch;
203                    } else {
204                        return Err(FilenameError::NameTooLong);
205                    }
206                    idx += 1;
207                }
208            }
209        }
210        if idx == 0 {
211            return Err(FilenameError::FilenameEmpty);
212        }
213        Ok(sfn)
214    }
215}
216
217impl core::fmt::Display for ShortFileName {
218    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
219        let mut printed = 0;
220        for (i, &c) in self.contents.iter().enumerate() {
221            if c != b' ' {
222                if i == Self::FILENAME_BASE_MAX_LEN {
223                    write!(f, ".")?;
224                    printed += 1;
225                }
226                write!(f, "{}", c as char)?;
227                printed += 1;
228            }
229        }
230        if let Some(mut width) = f.width() {
231            if width > printed {
232                width -= printed;
233                for _ in 0..width {
234                    write!(f, "{}", f.fill())?;
235                }
236            }
237        }
238        Ok(())
239    }
240}
241
242impl core::fmt::Debug for ShortFileName {
243    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
244        write!(f, "ShortFileName(\"{}\")", self)
245    }
246}
247
248// ****************************************************************************
249//
250// Unit Tests
251//
252// ****************************************************************************
253
254#[cfg(test)]
255mod test {
256    use super::*;
257
258    #[test]
259    fn filename_no_extension() {
260        let sfn = ShortFileName {
261            contents: *b"HELLO      ",
262        };
263        assert_eq!(format!("{}", &sfn), "HELLO");
264        assert_eq!(sfn, ShortFileName::create_from_str("HELLO").unwrap());
265        assert_eq!(sfn, ShortFileName::create_from_str("hello").unwrap());
266        assert_eq!(sfn, ShortFileName::create_from_str("HeLlO").unwrap());
267        assert_eq!(sfn, ShortFileName::create_from_str("HELLO.").unwrap());
268    }
269
270    #[test]
271    fn filename_extension() {
272        let sfn = ShortFileName {
273            contents: *b"HELLO   TXT",
274        };
275        assert_eq!(format!("{}", &sfn), "HELLO.TXT");
276        assert_eq!(sfn, ShortFileName::create_from_str("HELLO.TXT").unwrap());
277    }
278
279    #[test]
280    fn filename_get_extension() {
281        let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap();
282        assert_eq!(sfn.extension(), "TXT".as_bytes());
283        sfn = ShortFileName::create_from_str("hello").unwrap();
284        assert_eq!(sfn.extension(), "".as_bytes());
285        sfn = ShortFileName::create_from_str("hello.a").unwrap();
286        assert_eq!(sfn.extension(), "A".as_bytes());
287    }
288
289    #[test]
290    fn filename_get_base_name() {
291        let mut sfn = ShortFileName::create_from_str("hello.txt").unwrap();
292        assert_eq!(sfn.base_name(), "HELLO".as_bytes());
293        sfn = ShortFileName::create_from_str("12345678").unwrap();
294        assert_eq!(sfn.base_name(), "12345678".as_bytes());
295        sfn = ShortFileName::create_from_str("1").unwrap();
296        assert_eq!(sfn.base_name(), "1".as_bytes());
297    }
298
299    #[test]
300    fn filename_fulllength() {
301        let sfn = ShortFileName {
302            contents: *b"12345678TXT",
303        };
304        assert_eq!(format!("{}", &sfn), "12345678.TXT");
305        assert_eq!(sfn, ShortFileName::create_from_str("12345678.TXT").unwrap());
306    }
307
308    #[test]
309    fn filename_short_extension() {
310        let sfn = ShortFileName {
311            contents: *b"12345678C  ",
312        };
313        assert_eq!(format!("{}", &sfn), "12345678.C");
314        assert_eq!(sfn, ShortFileName::create_from_str("12345678.C").unwrap());
315    }
316
317    #[test]
318    fn filename_short() {
319        let sfn = ShortFileName {
320            contents: *b"1       C  ",
321        };
322        assert_eq!(format!("{}", &sfn), "1.C");
323        assert_eq!(sfn, ShortFileName::create_from_str("1.C").unwrap());
324    }
325
326    #[test]
327    fn filename_empty() {
328        assert_eq!(
329            ShortFileName::create_from_str("").unwrap(),
330            ShortFileName::this_dir()
331        );
332    }
333
334    #[test]
335    fn filename_bad() {
336        assert!(ShortFileName::create_from_str(" ").is_err());
337        assert!(ShortFileName::create_from_str("123456789").is_err());
338        assert!(ShortFileName::create_from_str("12345678.ABCD").is_err());
339    }
340}
341
342// ****************************************************************************
343//
344// End Of File
345//
346// ****************************************************************************