embedded_sdmmc/filesystem/
filename.rs1#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
5#[derive(Debug, Clone)]
6pub enum FilenameError {
7 InvalidCharacter,
9 FilenameEmpty,
11 NameTooLong,
13 MisplacedPeriod,
15 Utf8Error,
17}
18
19pub trait ToShortFileName {
21 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#[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 pub const fn parent_dir() -> Self {
57 Self {
58 contents: *b".. ",
59 }
60 }
61
62 pub const fn this_dir() -> Self {
64 Self {
65 contents: *b". ",
66 }
67 }
68
69 pub fn base_name(&self) -> &[u8] {
71 Self::bytes_before_space(&self.contents[..Self::FILENAME_BASE_MAX_LEN])
72 }
73
74 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 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 if name == ".." {
91 return Ok(ShortFileName::parent_dir());
92 }
93
94 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 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 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 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 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 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#[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