embedded_sdmmc/
filesystem.rs

1//! embedded-sdmmc-rs - Generic File System structures
2//!
3//! Implements generic file system components. These should be applicable to
4//! most (if not all) supported filesystems.
5
6// ****************************************************************************
7//
8// Imports
9//
10// ****************************************************************************
11
12// None
13
14// ****************************************************************************
15//
16// Public Types
17//
18// ****************************************************************************
19
20use core::convert::TryFrom;
21
22use crate::blockdevice::BlockIdx;
23use crate::fat::{FatType, OnDiskDirEntry};
24
25/// Maximum file size supported by this library
26pub const MAX_FILE_SIZE: u32 = core::u32::MAX;
27
28/// Things that impl this can tell you the current time.
29pub trait TimeSource {
30    /// Returns the current time
31    fn get_timestamp(&self) -> Timestamp;
32}
33
34/// Represents a cluster on disk.
35#[derive(Debug, Copy, Clone, PartialEq, Eq)]
36pub struct Cluster(pub(crate) u32);
37
38/// Represents a directory entry, which tells you about
39/// other files and directories.
40#[derive(Debug, PartialEq, Eq, Clone)]
41pub struct DirEntry {
42    /// The name of the file
43    pub name: ShortFileName,
44    /// When the file was last modified
45    pub mtime: Timestamp,
46    /// When the file was first created
47    pub ctime: Timestamp,
48    /// The file attributes (Read Only, Archive, etc)
49    pub attributes: Attributes,
50    /// The starting cluster of the file. The FAT tells us the following Clusters.
51    pub cluster: Cluster,
52    /// The size of the file in bytes.
53    pub size: u32,
54    /// The disk block of this entry
55    pub entry_block: BlockIdx,
56    /// The offset on its block (in bytes)
57    pub entry_offset: u32,
58}
59
60/// An MS-DOS 8.3 filename. 7-bit ASCII only. All lower-case is converted to
61/// upper-case by default.
62#[derive(PartialEq, Eq, Clone)]
63pub struct ShortFileName {
64    pub(crate) contents: [u8; 11],
65}
66
67/// Represents an instant in time, in the local time zone. TODO: Consider
68/// replacing this with POSIX time as a `u32`, which would save two bytes at
69/// the expense of some maths.
70#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)]
71pub struct Timestamp {
72    /// Add 1970 to this file to get the calendar year
73    pub year_since_1970: u8,
74    /// Add one to this value to get the calendar month
75    pub zero_indexed_month: u8,
76    /// Add one to this value to get the calendar day
77    pub zero_indexed_day: u8,
78    /// The number of hours past midnight
79    pub hours: u8,
80    /// The number of minutes past the hour
81    pub minutes: u8,
82    /// The number of seconds past the minute
83    pub seconds: u8,
84}
85
86/// Indicates whether a directory entry is read-only, a directory, a volume
87/// label, etc.
88#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)]
89pub struct Attributes(pub(crate) u8);
90
91/// Represents an open file on disk.
92#[derive(Debug)]
93pub struct File {
94    /// The starting point of the file.
95    pub(crate) starting_cluster: Cluster,
96    /// The current cluster, and how many bytes that short-cuts us
97    pub(crate) current_cluster: (u32, Cluster),
98    /// How far through the file we've read (in bytes).
99    pub(crate) current_offset: u32,
100    /// The length of the file, in bytes.
101    pub(crate) length: u32,
102    /// What mode the file was opened in
103    pub(crate) mode: Mode,
104    /// DirEntry of this file
105    pub(crate) entry: DirEntry,
106}
107
108/// Represents an open directory on disk.
109#[derive(Debug)]
110pub struct Directory {
111    /// The starting point of the directory listing.
112    pub(crate) cluster: Cluster,
113    /// Dir Entry of this directory, None for the root directory
114    pub(crate) entry: Option<DirEntry>,
115}
116
117/// The different ways we can open a file.
118#[derive(Debug, PartialEq, Eq, Copy, Clone)]
119pub enum Mode {
120    /// Open a file for reading, if it exists.
121    ReadOnly,
122    /// Open a file for appending (writing to the end of the existing file), if it exists.
123    ReadWriteAppend,
124    /// Open a file and remove all contents, before writing to the start of the existing file, if it exists.
125    ReadWriteTruncate,
126    /// Create a new empty file. Fail if it exists.
127    ReadWriteCreate,
128    /// Create a new empty file, or truncate an existing file.
129    ReadWriteCreateOrTruncate,
130    /// Create a new empty file, or append to an existing file.
131    ReadWriteCreateOrAppend,
132}
133
134/// Various filename related errors that can occur.
135#[derive(Debug, Clone)]
136pub enum FilenameError {
137    /// Tried to create a file with an invalid character.
138    InvalidCharacter,
139    /// Tried to create a file with no file name.
140    FilenameEmpty,
141    /// Given name was too long (we are limited to 8.3).
142    NameTooLong,
143    /// Can't start a file with a period, or after 8 characters.
144    MisplacedPeriod,
145}
146
147// ****************************************************************************
148//
149// Public Data
150//
151// ****************************************************************************
152
153// None
154
155// ****************************************************************************
156//
157// Private Types
158//
159// ****************************************************************************
160
161// None
162
163// ****************************************************************************
164//
165// Private Data
166//
167// ****************************************************************************
168
169// None
170
171// ****************************************************************************
172//
173// Public Functions / Impl for Public Types
174//
175// ****************************************************************************
176
177impl Cluster {
178    /// Magic value indicating an invalid cluster value.
179    pub const INVALID: Cluster = Cluster(0xFFFF_FFF6);
180    /// Magic value indicating a bad cluster.
181    pub const BAD: Cluster = Cluster(0xFFFF_FFF7);
182    /// Magic value indicating a empty cluster.
183    pub const EMPTY: Cluster = Cluster(0x0000_0000);
184    /// Magic value indicating the cluster holding the root directory (which
185    /// doesn't have a number in FAT16 as there's a reserved region).
186    pub const ROOT_DIR: Cluster = Cluster(0xFFFF_FFFC);
187    /// Magic value indicating that the cluster is allocated and is the final cluster for the file
188    pub const END_OF_FILE: Cluster = Cluster(0xFFFF_FFFF);
189}
190
191impl core::ops::Add<u32> for Cluster {
192    type Output = Cluster;
193    fn add(self, rhs: u32) -> Cluster {
194        Cluster(self.0 + rhs)
195    }
196}
197
198impl core::ops::AddAssign<u32> for Cluster {
199    fn add_assign(&mut self, rhs: u32) {
200        self.0 += rhs;
201    }
202}
203
204impl core::ops::Add<Cluster> for Cluster {
205    type Output = Cluster;
206    fn add(self, rhs: Cluster) -> Cluster {
207        Cluster(self.0 + rhs.0)
208    }
209}
210
211impl core::ops::AddAssign<Cluster> for Cluster {
212    fn add_assign(&mut self, rhs: Cluster) {
213        self.0 += rhs.0;
214    }
215}
216
217impl DirEntry {
218    pub(crate) fn serialize(&self, fat_type: FatType) -> [u8; OnDiskDirEntry::LEN] {
219        let mut data = [0u8; OnDiskDirEntry::LEN];
220        data[0..11].copy_from_slice(&self.name.contents);
221        data[11] = self.attributes.0;
222        // 12: Reserved. Must be set to zero
223        // 13: CrtTimeTenth, not supported, set to zero
224        data[14..18].copy_from_slice(&self.ctime.serialize_to_fat()[..]);
225        // 0 + 18: LastAccDate, not supported, set to zero
226        let cluster_number = self.cluster.0;
227        let cluster_hi = if fat_type == FatType::Fat16 {
228            [0u8; 2]
229        } else {
230            // Safe due to the AND operation
231            u16::try_from((cluster_number >> 16) & 0x0000_FFFF)
232                .unwrap()
233                .to_le_bytes()
234        };
235        data[20..22].copy_from_slice(&cluster_hi[..]);
236        data[22..26].copy_from_slice(&self.mtime.serialize_to_fat()[..]);
237        // Safe due to the AND operation
238        let cluster_lo = u16::try_from(cluster_number & 0x0000_FFFF)
239            .unwrap()
240            .to_le_bytes();
241        data[26..28].copy_from_slice(&cluster_lo[..]);
242        data[28..32].copy_from_slice(&self.size.to_le_bytes()[..]);
243        data
244    }
245
246    pub(crate) fn new(
247        name: ShortFileName,
248        attributes: Attributes,
249        cluster: Cluster,
250        ctime: Timestamp,
251        entry_block: BlockIdx,
252        entry_offset: u32,
253    ) -> Self {
254        Self {
255            name,
256            mtime: ctime,
257            ctime,
258            attributes,
259            cluster,
260            size: 0,
261            entry_block,
262            entry_offset,
263        }
264    }
265}
266
267impl ShortFileName {
268    const FILENAME_BASE_MAX_LEN: usize = 8;
269    const FILENAME_EXT_MAX_LEN: usize = 3;
270    const FILENAME_MAX_LEN: usize = 11;
271
272    /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry.
273    pub fn create_from_str(name: &str) -> Result<ShortFileName, FilenameError> {
274        let mut sfn = ShortFileName {
275            contents: [b' '; Self::FILENAME_MAX_LEN],
276        };
277        let mut idx = 0;
278        let mut seen_dot = false;
279        for ch in name.bytes() {
280            match ch {
281                // Microsoft say these are the invalid characters
282                0x00..=0x1F
283                | 0x20
284                | 0x22
285                | 0x2A
286                | 0x2B
287                | 0x2C
288                | 0x2F
289                | 0x3A
290                | 0x3B
291                | 0x3C
292                | 0x3D
293                | 0x3E
294                | 0x3F
295                | 0x5B
296                | 0x5C
297                | 0x5D
298                | 0x7C => {
299                    return Err(FilenameError::InvalidCharacter);
300                }
301                // Denotes the start of the file extension
302                b'.' => {
303                    if idx >= 1 && idx <= Self::FILENAME_BASE_MAX_LEN {
304                        idx = Self::FILENAME_BASE_MAX_LEN;
305                        seen_dot = true;
306                    } else {
307                        return Err(FilenameError::MisplacedPeriod);
308                    }
309                }
310                _ => {
311                    let ch = if ch >= b'a' && ch <= b'z' {
312                        // Uppercase characters only
313                        ch - 32
314                    } else {
315                        ch
316                    };
317                    if seen_dot {
318                        if idx >= Self::FILENAME_BASE_MAX_LEN && idx < Self::FILENAME_MAX_LEN {
319                            sfn.contents[idx] = ch;
320                        } else {
321                            return Err(FilenameError::NameTooLong);
322                        }
323                    } else if idx < Self::FILENAME_BASE_MAX_LEN {
324                        sfn.contents[idx] = ch;
325                    } else {
326                        return Err(FilenameError::NameTooLong);
327                    }
328                    idx += 1;
329                }
330            }
331        }
332        if idx == 0 {
333            return Err(FilenameError::FilenameEmpty);
334        }
335        Ok(sfn)
336    }
337
338    /// Create a new MS-DOS 8.3 space-padded file name as stored in the directory entry.
339    /// Use this for volume labels with mixed case.
340    pub fn create_from_str_mixed_case(name: &str) -> Result<ShortFileName, FilenameError> {
341        let mut sfn = ShortFileName {
342            contents: [b' '; Self::FILENAME_MAX_LEN],
343        };
344        let mut idx = 0;
345        let mut seen_dot = false;
346        for ch in name.bytes() {
347            match ch {
348                // Microsoft say these are the invalid characters
349                0x00..=0x1F
350                | 0x20
351                | 0x22
352                | 0x2A
353                | 0x2B
354                | 0x2C
355                | 0x2F
356                | 0x3A
357                | 0x3B
358                | 0x3C
359                | 0x3D
360                | 0x3E
361                | 0x3F
362                | 0x5B
363                | 0x5C
364                | 0x5D
365                | 0x7C => {
366                    return Err(FilenameError::InvalidCharacter);
367                }
368                // Denotes the start of the file extension
369                b'.' => {
370                    if idx >= 1 && idx <= Self::FILENAME_BASE_MAX_LEN {
371                        idx = Self::FILENAME_BASE_MAX_LEN;
372                        seen_dot = true;
373                    } else {
374                        return Err(FilenameError::MisplacedPeriod);
375                    }
376                }
377                _ => {
378                    if seen_dot {
379                        if idx >= Self::FILENAME_BASE_MAX_LEN && idx < Self::FILENAME_MAX_LEN {
380                            sfn.contents[idx] = ch;
381                        } else {
382                            return Err(FilenameError::NameTooLong);
383                        }
384                    } else if idx < Self::FILENAME_BASE_MAX_LEN {
385                        sfn.contents[idx] = ch;
386                    } else {
387                        return Err(FilenameError::NameTooLong);
388                    }
389                    idx += 1;
390                }
391            }
392        }
393        if idx == 0 {
394            return Err(FilenameError::FilenameEmpty);
395        }
396        Ok(sfn)
397    }
398}
399
400impl core::fmt::Display for ShortFileName {
401    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
402        let mut printed = 0;
403        for (i, &c) in self.contents.iter().enumerate() {
404            if c != b' ' {
405                if i == Self::FILENAME_BASE_MAX_LEN {
406                    write!(f, ".")?;
407                    printed += 1;
408                }
409                write!(f, "{}", c as char)?;
410                printed += 1;
411            }
412        }
413        if let Some(mut width) = f.width() {
414            if width > printed {
415                width -= printed;
416                for _ in 0..width {
417                    write!(f, "{}", f.fill())?;
418                }
419            }
420        }
421        Ok(())
422    }
423}
424
425impl core::fmt::Debug for ShortFileName {
426    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
427        write!(f, "ShortFileName(\"{}\")", self)
428    }
429}
430
431impl Timestamp {
432    const MONTH_LOOKUP: [u32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
433
434    /// Create a `Timestamp` from the 16-bit FAT date and time fields.
435    pub fn from_fat(date: u16, time: u16) -> Timestamp {
436        let year = (1980 + (date >> 9)) as u16;
437        let month = ((date >> 5) & 0x000F) as u8;
438        let day = (date & 0x001F) as u8;
439        let hours = ((time >> 11) & 0x001F) as u8;
440        let minutes = ((time >> 5) & 0x0003F) as u8;
441        let seconds = ((time << 1) & 0x0003F) as u8;
442        // Volume labels have a zero for month/day, so tolerate that...
443        Timestamp {
444            year_since_1970: (year - 1970) as u8,
445            zero_indexed_month: if month == 0 { 0 } else { month - 1 },
446            zero_indexed_day: if day == 0 { 0 } else { day - 1 },
447            hours,
448            minutes,
449            seconds,
450        }
451    }
452
453    // TODO add tests for the method
454    /// Serialize a `Timestamp` to FAT format
455    pub fn serialize_to_fat(self) -> [u8; 4] {
456        let mut data = [0u8; 4];
457
458        let hours = (u16::from(self.hours) << 11) & 0xF800;
459        let minutes = (u16::from(self.minutes) << 5) & 0x07E0;
460        let seconds = (u16::from(self.seconds / 2)) & 0x001F;
461        data[..2].copy_from_slice(&(hours | minutes | seconds).to_le_bytes()[..]);
462
463        let year = if self.year_since_1970 < 10 {
464            0
465        } else {
466            (u16::from(self.year_since_1970 - 10) << 9) & 0xFE00
467        };
468        let month = (u16::from(self.zero_indexed_month + 1) << 5) & 0x01E0;
469        let day = u16::from(self.zero_indexed_day + 1) & 0x001F;
470        data[2..].copy_from_slice(&(year | month | day).to_le_bytes()[..]);
471        data
472    }
473
474    /// Create a `Timestamp` from year/month/day/hour/minute/second.
475    ///
476    /// Values should be given as you'd write then (i.e. 1980, 01, 01, 13, 30,
477    /// 05) is 1980-Jan-01, 1:30:05pm.
478    pub fn from_calendar(
479        year: u16,
480        month: u8,
481        day: u8,
482        hours: u8,
483        minutes: u8,
484        seconds: u8,
485    ) -> Result<Timestamp, &'static str> {
486        Ok(Timestamp {
487            year_since_1970: if year >= 1970 && year <= (1970 + 255) {
488                (year - 1970) as u8
489            } else {
490                return Err("Bad year");
491            },
492            zero_indexed_month: if month >= 1 && month <= 12 {
493                month - 1
494            } else {
495                return Err("Bad month");
496            },
497            zero_indexed_day: if day >= 1 && day <= 31 {
498                day - 1
499            } else {
500                return Err("Bad day");
501            },
502            hours: if hours <= 23 {
503                hours
504            } else {
505                return Err("Bad hours");
506            },
507            minutes: if minutes <= 59 {
508                minutes
509            } else {
510                return Err("Bad minutes");
511            },
512            seconds: if seconds <= 59 {
513                seconds
514            } else {
515                return Err("Bad seconds");
516            },
517        })
518    }
519}
520
521impl core::fmt::Debug for Timestamp {
522    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
523        write!(f, "Timestamp({})", self)
524    }
525}
526
527impl core::fmt::Display for Timestamp {
528    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
529        write!(
530            f,
531            "{}-{:02}-{:02} {:02}:{:02}:{:02}",
532            u16::from(self.year_since_1970) + 1970,
533            self.zero_indexed_month + 1,
534            self.zero_indexed_day + 1,
535            self.hours,
536            self.minutes,
537            self.seconds
538        )
539    }
540}
541
542impl Attributes {
543    /// Indicates this file cannot be written.
544    pub const READ_ONLY: u8 = 0x01;
545    /// Indicates the file is hidden.
546    pub const HIDDEN: u8 = 0x02;
547    /// Indicates this is a system file.
548    pub const SYSTEM: u8 = 0x04;
549    /// Indicates this is a volume label.
550    pub const VOLUME: u8 = 0x08;
551    /// Indicates this is a directory.
552    pub const DIRECTORY: u8 = 0x10;
553    /// Indicates this file needs archiving (i.e. has been modified since last
554    /// archived).
555    pub const ARCHIVE: u8 = 0x20;
556    /// This set of flags indicates the file is actually a long file name
557    /// fragment.
558    pub const LFN: u8 = Self::READ_ONLY | Self::HIDDEN | Self::SYSTEM | Self::VOLUME;
559
560    /// Create a `Attributes` value from the `u8` stored in a FAT16/FAT32
561    /// Directory Entry.
562    pub(crate) fn create_from_fat(value: u8) -> Attributes {
563        Attributes(value)
564    }
565
566    pub(crate) fn set_archive(&mut self, flag: bool) {
567        let archive = if flag { 0x20 } else { 0x00 };
568        self.0 |= archive;
569    }
570
571    /// Does this file has the read-only attribute set?
572    pub fn is_read_only(self) -> bool {
573        (self.0 & Self::READ_ONLY) == Self::READ_ONLY
574    }
575
576    /// Does this file has the hidden attribute set?
577    pub fn is_hidden(self) -> bool {
578        (self.0 & Self::HIDDEN) == Self::HIDDEN
579    }
580
581    /// Does this file has the system attribute set?
582    pub fn is_system(self) -> bool {
583        (self.0 & Self::SYSTEM) == Self::SYSTEM
584    }
585
586    /// Does this file has the volume attribute set?
587    pub fn is_volume(self) -> bool {
588        (self.0 & Self::VOLUME) == Self::VOLUME
589    }
590
591    /// Does this entry point at a directory?
592    pub fn is_directory(self) -> bool {
593        (self.0 & Self::DIRECTORY) == Self::DIRECTORY
594    }
595
596    /// Does this need archiving?
597    pub fn is_archive(self) -> bool {
598        (self.0 & Self::ARCHIVE) == Self::ARCHIVE
599    }
600
601    /// Is this a long file name fragment?
602    pub fn is_lfn(self) -> bool {
603        (self.0 & Self::LFN) == Self::LFN
604    }
605}
606
607impl core::fmt::Debug for Attributes {
608    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
609        if self.is_lfn() {
610            write!(f, "LFN")?;
611        } else {
612            if self.is_directory() {
613                write!(f, "D")?;
614            } else {
615                write!(f, "F")?;
616            }
617            if self.is_read_only() {
618                write!(f, "R")?;
619            }
620            if self.is_hidden() {
621                write!(f, "H")?;
622            }
623            if self.is_system() {
624                write!(f, "S")?;
625            }
626            if self.is_volume() {
627                write!(f, "V")?;
628            }
629            if self.is_archive() {
630                write!(f, "A")?;
631            }
632        }
633        Ok(())
634    }
635}
636
637impl File {
638    /// Create a new file handle.
639    pub(crate) fn new(cluster: Cluster, length: u32, mode: Mode, entry: DirEntry) -> File {
640        File {
641            starting_cluster: cluster,
642            current_cluster: (0, cluster),
643            mode,
644            length,
645            current_offset: 0,
646            entry,
647        }
648    }
649
650    /// Are we at the end of the file?
651    pub fn eof(&self) -> bool {
652        self.current_offset == self.length
653    }
654
655    /// How long is the file?
656    pub fn length(&self) -> u32 {
657        self.length
658    }
659
660    /// Seek to a new position in the file, relative to the start of the file.
661    pub fn seek_from_start(&mut self, offset: u32) -> Result<(), ()> {
662        if offset <= self.length {
663            self.current_offset = offset;
664            if offset < self.current_cluster.0 {
665                // Back to start
666                self.current_cluster = (0, self.starting_cluster);
667            }
668            Ok(())
669        } else {
670            Err(())
671        }
672    }
673
674    /// Seek to a new position in the file, relative to the end of the file.
675    pub fn seek_from_end(&mut self, offset: u32) -> Result<(), ()> {
676        if offset <= self.length {
677            self.current_offset = self.length - offset;
678            if offset < self.current_cluster.0 {
679                // Back to start
680                self.current_cluster = (0, self.starting_cluster);
681            }
682            Ok(())
683        } else {
684            Err(())
685        }
686    }
687
688    /// Seek to a new position in the file, relative to the current position.
689    pub fn seek_from_current(&mut self, offset: i32) -> Result<(), ()> {
690        let new_offset = i64::from(self.current_offset) + i64::from(offset);
691        if new_offset >= 0 && new_offset <= i64::from(self.length) {
692            self.current_offset = new_offset as u32;
693            Ok(())
694        } else {
695            Err(())
696        }
697    }
698
699    /// Amount of file left to read.
700    pub fn left(&self) -> u32 {
701        self.length - self.current_offset
702    }
703
704    pub(crate) fn update_length(&mut self, new: u32) {
705        self.length = new;
706        self.entry.size = new;
707    }
708}
709
710impl Directory {}
711
712impl FilenameError {}
713
714// ****************************************************************************
715//
716// Private Functions / Impl for Priate Types
717//
718// ****************************************************************************
719
720// None
721
722// ****************************************************************************
723//
724// Unit Tests
725//
726// ****************************************************************************
727
728#[cfg(test)]
729mod test {
730    use super::*;
731
732    #[test]
733    fn filename_no_extension() {
734        let sfn = ShortFileName {
735            contents: *b"HELLO      ",
736        };
737        assert_eq!(format!("{}", &sfn), "HELLO");
738        assert_eq!(sfn, ShortFileName::create_from_str("HELLO").unwrap());
739        assert_eq!(sfn, ShortFileName::create_from_str("hello").unwrap());
740        assert_eq!(sfn, ShortFileName::create_from_str("HeLlO").unwrap());
741        assert_eq!(sfn, ShortFileName::create_from_str("HELLO.").unwrap());
742    }
743
744    #[test]
745    fn filename_extension() {
746        let sfn = ShortFileName {
747            contents: *b"HELLO   TXT",
748        };
749        assert_eq!(format!("{}", &sfn), "HELLO.TXT");
750        assert_eq!(sfn, ShortFileName::create_from_str("HELLO.TXT").unwrap());
751    }
752
753    #[test]
754    fn filename_fulllength() {
755        let sfn = ShortFileName {
756            contents: *b"12345678TXT",
757        };
758        assert_eq!(format!("{}", &sfn), "12345678.TXT");
759        assert_eq!(sfn, ShortFileName::create_from_str("12345678.TXT").unwrap());
760    }
761
762    #[test]
763    fn filename_short_extension() {
764        let sfn = ShortFileName {
765            contents: *b"12345678C  ",
766        };
767        assert_eq!(format!("{}", &sfn), "12345678.C");
768        assert_eq!(sfn, ShortFileName::create_from_str("12345678.C").unwrap());
769    }
770
771    #[test]
772    fn filename_short() {
773        let sfn = ShortFileName {
774            contents: *b"1       C  ",
775        };
776        assert_eq!(format!("{}", &sfn), "1.C");
777        assert_eq!(sfn, ShortFileName::create_from_str("1.C").unwrap());
778    }
779
780    #[test]
781    fn filename_bad() {
782        assert!(ShortFileName::create_from_str("").is_err());
783        assert!(ShortFileName::create_from_str(" ").is_err());
784        assert!(ShortFileName::create_from_str("123456789").is_err());
785        assert!(ShortFileName::create_from_str("12345678.ABCD").is_err());
786    }
787}
788
789// ****************************************************************************
790//
791// End Of File
792//
793// ****************************************************************************