Skip to main content

fatfs/
dir_entry.rs

1#[cfg(all(not(feature = "std"), feature = "alloc"))]
2use alloc::string::String;
3use bitflags::bitflags;
4use core::char;
5use core::convert::TryInto;
6use core::fmt;
7#[cfg(not(feature = "unicode"))]
8use core::iter;
9
10#[cfg(feature = "lfn")]
11use crate::dir::LfnBuffer;
12use crate::dir::{Dir, DirRawStream};
13use crate::error::{Error, IoError};
14use crate::file::File;
15use crate::fs::{FatType, FileSystem, OemCpConverter, ReadWriteSeek};
16use crate::io::{self, Read, ReadLeExt, Write, WriteLeExt};
17use crate::time::{Date, DateTime};
18
19bitflags! {
20    /// A FAT file attributes.
21    #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
22    pub struct FileAttributes: u8 {
23        const READ_ONLY  = 0x01;
24        const HIDDEN     = 0x02;
25        const SYSTEM     = 0x04;
26        const VOLUME_ID  = 0x08;
27        const DIRECTORY  = 0x10;
28        const ARCHIVE    = 0x20;
29        const LFN        = Self::READ_ONLY.bits() | Self::HIDDEN.bits()
30                         | Self::SYSTEM.bits() | Self::VOLUME_ID.bits();
31    }
32}
33
34// Size of single directory entry in bytes
35pub(crate) const DIR_ENTRY_SIZE: u32 = 32;
36
37// Directory entry flags available in first byte of the short name
38pub(crate) const DIR_ENTRY_DELETED_FLAG: u8 = 0xE5;
39pub(crate) const DIR_ENTRY_REALLY_E5_FLAG: u8 = 0x05;
40
41// Short file name field size in bytes (besically 8 + 3)
42pub(crate) const SFN_SIZE: usize = 11;
43
44// Byte used for short name padding
45pub(crate) const SFN_PADDING: u8 = b' ';
46
47// Length in characters of a LFN fragment packed in one directory entry
48pub(crate) const LFN_PART_LEN: usize = 13;
49
50// Bit used in order field to mark last LFN entry
51#[cfg(feature = "lfn")]
52pub(crate) const LFN_ENTRY_LAST_FLAG: u8 = 0x40;
53
54// Character to upper case conversion which supports Unicode only if `unicode` feature is enabled
55#[cfg(feature = "unicode")]
56fn char_to_uppercase(c: char) -> char::ToUppercase {
57    c.to_uppercase()
58}
59#[cfg(not(feature = "unicode"))]
60fn char_to_uppercase(c: char) -> iter::Once<char> {
61    iter::once(c.to_ascii_uppercase())
62}
63
64/// Decoded file short name
65#[derive(Clone, Debug, Default)]
66pub(crate) struct ShortName {
67    name: [u8; 12],
68    len: u8,
69}
70
71impl ShortName {
72    pub(crate) fn new(raw_name: &[u8; SFN_SIZE]) -> Self {
73        // get name components length by looking for space character
74        let name_len = raw_name[0..8]
75            .iter()
76            .rposition(|x| *x != SFN_PADDING)
77            .map_or(0, |p| p + 1);
78        let ext_len = raw_name[8..11]
79            .iter()
80            .rposition(|x| *x != SFN_PADDING)
81            .map_or(0, |p| p + 1);
82        let mut name = [SFN_PADDING; 12];
83        name[..name_len].copy_from_slice(&raw_name[..name_len]);
84        let total_len = if ext_len > 0 {
85            name[name_len] = b'.';
86            name[name_len + 1..name_len + 1 + ext_len].copy_from_slice(&raw_name[8..8 + ext_len]);
87            // Return total name length
88            name_len + 1 + ext_len
89        } else {
90            // No extension - return length of name part
91            name_len
92        };
93        // FAT encodes character 0xE5 as 0x05 because 0xE5 marks deleted files
94        if name[0] == DIR_ENTRY_REALLY_E5_FLAG {
95            name[0] = 0xE5;
96        }
97        // Short names in FAT filesystem are encoded in OEM code-page
98        Self {
99            name,
100            len: total_len as u8,
101        }
102    }
103
104    fn as_bytes(&self) -> &[u8] {
105        &self.name[..usize::from(self.len)]
106    }
107
108    #[cfg(feature = "alloc")]
109    fn to_string<OCC: OemCpConverter>(&self, oem_cp_converter: &OCC) -> String {
110        // Strip non-ascii characters from short name
111        self.as_bytes()
112            .iter()
113            .copied()
114            .map(|c| oem_cp_converter.decode(c))
115            .collect()
116    }
117
118    fn eq_ignore_case<OCC: OemCpConverter>(&self, name: &str, oem_cp_converter: &OCC) -> bool {
119        // Convert name to UTF-8 character iterator
120        let byte_iter = self.as_bytes().iter().copied();
121        let char_iter = byte_iter.map(|c| oem_cp_converter.decode(c));
122        // Compare interators ignoring case
123        let uppercase_char_iter = char_iter.flat_map(char_to_uppercase);
124        uppercase_char_iter.eq(name.chars().flat_map(char_to_uppercase))
125    }
126}
127
128#[allow(dead_code)]
129#[derive(Clone, Debug, Default)]
130pub(crate) struct DirFileEntryData {
131    name: [u8; SFN_SIZE],
132    attrs: FileAttributes,
133    reserved_0: u8,
134    create_time_0: u8,
135    create_time_1: u16,
136    create_date: u16,
137    access_date: u16,
138    first_cluster_hi: u16,
139    modify_time: u16,
140    modify_date: u16,
141    first_cluster_lo: u16,
142    size: u32,
143}
144
145impl DirFileEntryData {
146    pub(crate) fn new(name: [u8; SFN_SIZE], attrs: FileAttributes) -> Self {
147        Self {
148            name,
149            attrs,
150            ..Self::default()
151        }
152    }
153
154    pub(crate) fn renamed(&self, new_name: [u8; SFN_SIZE]) -> Self {
155        let mut sfn_entry = self.clone();
156        sfn_entry.name = new_name;
157        sfn_entry
158    }
159
160    pub(crate) fn name(&self) -> &[u8; SFN_SIZE] {
161        &self.name
162    }
163
164    #[cfg(feature = "alloc")]
165    fn lowercase_name(&self) -> ShortName {
166        let mut name_copy: [u8; SFN_SIZE] = self.name;
167        if self.lowercase_basename() {
168            name_copy[..8].make_ascii_lowercase();
169        }
170        if self.lowercase_ext() {
171            name_copy[8..].make_ascii_lowercase();
172        }
173        ShortName::new(&name_copy)
174    }
175
176    pub(crate) fn first_cluster(&self, fat_type: FatType) -> Option<u32> {
177        let first_cluster_hi = if fat_type == FatType::Fat32 {
178            self.first_cluster_hi
179        } else {
180            0
181        };
182        let n = (u32::from(first_cluster_hi) << 16) | u32::from(self.first_cluster_lo);
183        if n == 0 {
184            None
185        } else {
186            Some(n)
187        }
188    }
189
190    pub(crate) fn set_first_cluster(&mut self, cluster: Option<u32>, fat_type: FatType) {
191        let n = cluster.unwrap_or(0);
192        if fat_type == FatType::Fat32 {
193            self.first_cluster_hi = (n >> 16) as u16;
194        }
195        self.first_cluster_lo = (n & 0xFFFF) as u16;
196    }
197
198    pub(crate) fn size(&self) -> Option<u32> {
199        if self.is_file() {
200            Some(self.size)
201        } else {
202            None
203        }
204    }
205
206    fn set_size(&mut self, size: u32) {
207        self.size = size;
208    }
209
210    pub(crate) fn is_dir(&self) -> bool {
211        self.attrs.contains(FileAttributes::DIRECTORY)
212    }
213
214    fn is_file(&self) -> bool {
215        !self.is_dir()
216    }
217
218    fn lowercase_basename(&self) -> bool {
219        self.reserved_0 & (1 << 3) != 0
220    }
221
222    fn lowercase_ext(&self) -> bool {
223        self.reserved_0 & (1 << 4) != 0
224    }
225
226    pub(crate) fn created(&self) -> DateTime {
227        DateTime::decode(self.create_date, self.create_time_1, self.create_time_0)
228    }
229
230    pub(crate) fn accessed(&self) -> Date {
231        Date::decode(self.access_date)
232    }
233
234    pub(crate) fn modified(&self) -> DateTime {
235        DateTime::decode(self.modify_date, self.modify_time, 0)
236    }
237
238    pub(crate) fn set_created(&mut self, date_time: DateTime) {
239        self.create_date = date_time.date.encode();
240        let encoded_time = date_time.time.encode();
241        self.create_time_1 = encoded_time.0;
242        self.create_time_0 = encoded_time.1;
243    }
244
245    pub(crate) fn set_accessed(&mut self, date: Date) {
246        self.access_date = date.encode();
247    }
248
249    pub(crate) fn set_modified(&mut self, date_time: DateTime) {
250        self.modify_date = date_time.date.encode();
251        self.modify_time = date_time.time.encode().0;
252    }
253
254    pub(crate) fn serialize<W: Write>(&self, wrt: &mut W) -> Result<(), W::Error> {
255        wrt.write_all(&self.name)?;
256        wrt.write_u8(self.attrs.bits())?;
257        wrt.write_u8(self.reserved_0)?;
258        wrt.write_u8(self.create_time_0)?;
259        wrt.write_u16_le(self.create_time_1)?;
260        wrt.write_u16_le(self.create_date)?;
261        wrt.write_u16_le(self.access_date)?;
262        wrt.write_u16_le(self.first_cluster_hi)?;
263        wrt.write_u16_le(self.modify_time)?;
264        wrt.write_u16_le(self.modify_date)?;
265        wrt.write_u16_le(self.first_cluster_lo)?;
266        wrt.write_u32_le(self.size)?;
267        Ok(())
268    }
269
270    pub(crate) fn is_deleted(&self) -> bool {
271        self.name[0] == DIR_ENTRY_DELETED_FLAG
272    }
273
274    pub(crate) fn set_deleted(&mut self) {
275        self.name[0] = DIR_ENTRY_DELETED_FLAG;
276    }
277
278    pub(crate) fn is_end(&self) -> bool {
279        self.name[0] == 0
280    }
281
282    pub(crate) fn is_volume(&self) -> bool {
283        self.attrs.contains(FileAttributes::VOLUME_ID)
284    }
285}
286
287#[allow(dead_code)]
288#[derive(Clone, Debug, Default)]
289pub(crate) struct DirLfnEntryData {
290    order: u8,
291    name_0: [u16; 5],
292    attrs: FileAttributes,
293    entry_type: u8,
294    checksum: u8,
295    name_1: [u16; 6],
296    reserved_0: u16,
297    name_2: [u16; 2],
298}
299
300impl DirLfnEntryData {
301    pub(crate) fn new(order: u8, checksum: u8) -> Self {
302        Self {
303            order,
304            checksum,
305            attrs: FileAttributes::LFN,
306            ..Self::default()
307        }
308    }
309
310    pub(crate) fn copy_name_from_slice(&mut self, lfn_part: &[u16; LFN_PART_LEN]) {
311        self.name_0.copy_from_slice(&lfn_part[0..5]);
312        self.name_1.copy_from_slice(&lfn_part[5..5 + 6]);
313        self.name_2.copy_from_slice(&lfn_part[11..11 + 2]);
314    }
315
316    pub(crate) fn copy_name_to_slice(&self, lfn_part: &mut [u16]) {
317        debug_assert!(lfn_part.len() == LFN_PART_LEN);
318        lfn_part[0..5].copy_from_slice(&self.name_0);
319        lfn_part[5..11].copy_from_slice(&self.name_1);
320        lfn_part[11..13].copy_from_slice(&self.name_2);
321    }
322
323    pub(crate) fn serialize<W: Write>(&self, wrt: &mut W) -> Result<(), W::Error> {
324        wrt.write_u8(self.order)?;
325        for ch in &self.name_0 {
326            wrt.write_u16_le(*ch)?;
327        }
328        wrt.write_u8(self.attrs.bits())?;
329        wrt.write_u8(self.entry_type)?;
330        wrt.write_u8(self.checksum)?;
331        for ch in &self.name_1 {
332            wrt.write_u16_le(*ch)?;
333        }
334        wrt.write_u16_le(self.reserved_0)?;
335        for ch in &self.name_2 {
336            wrt.write_u16_le(*ch)?;
337        }
338        Ok(())
339    }
340
341    pub(crate) fn order(&self) -> u8 {
342        self.order
343    }
344
345    pub(crate) fn checksum(&self) -> u8 {
346        self.checksum
347    }
348
349    pub(crate) fn is_deleted(&self) -> bool {
350        self.order == DIR_ENTRY_DELETED_FLAG
351    }
352
353    pub(crate) fn set_deleted(&mut self) {
354        self.order = DIR_ENTRY_DELETED_FLAG;
355    }
356
357    pub(crate) fn is_end(&self) -> bool {
358        self.order == 0
359    }
360}
361
362#[derive(Clone, Debug)]
363pub(crate) enum DirEntryData {
364    File(DirFileEntryData),
365    Lfn(DirLfnEntryData),
366}
367
368impl DirEntryData {
369    pub(crate) fn serialize<E: IoError, W: Write<Error = Error<E>>>(&self, wrt: &mut W) -> Result<(), Error<E>> {
370        trace!("DirEntryData::serialize");
371        match self {
372            DirEntryData::File(file) => file.serialize(wrt),
373            DirEntryData::Lfn(lfn) => lfn.serialize(wrt),
374        }
375    }
376
377    pub(crate) fn deserialize<E: IoError, R: Read<Error = Error<E>>>(rdr: &mut R) -> Result<Self, Error<E>> {
378        trace!("DirEntryData::deserialize");
379        let mut name = [0; SFN_SIZE];
380        match rdr.read_exact(&mut name) {
381            Err(Error::UnexpectedEof) => {
382                // entries can occupy all clusters of directory so there is no zero entry at the end
383                // handle it here by returning non-existing empty entry
384                return Ok(DirEntryData::File(DirFileEntryData::default()));
385            }
386            Err(err) => {
387                return Err(err);
388            }
389            Ok(()) => {}
390        }
391        let attrs = FileAttributes::from_bits_truncate(rdr.read_u8()?);
392        if attrs & FileAttributes::LFN == FileAttributes::LFN {
393            // read long name entry
394            let mut data = DirLfnEntryData {
395                attrs,
396                ..DirLfnEntryData::default()
397            };
398            // divide the name into order and LFN name_0
399            data.order = name[0];
400            for (dst, src) in data.name_0.iter_mut().zip(name[1..].chunks_exact(2)) {
401                // unwrap cannot panic because src has exactly 2 values
402                *dst = u16::from_le_bytes(src.try_into().unwrap());
403            }
404
405            data.entry_type = rdr.read_u8()?;
406            data.checksum = rdr.read_u8()?;
407            for x in &mut data.name_1 {
408                *x = rdr.read_u16_le()?;
409            }
410            data.reserved_0 = rdr.read_u16_le()?;
411            for x in &mut data.name_2 {
412                *x = rdr.read_u16_le()?;
413            }
414            Ok(DirEntryData::Lfn(data))
415        } else {
416            // read short name entry
417            let data = DirFileEntryData {
418                name,
419                attrs,
420                reserved_0: rdr.read_u8()?,
421                create_time_0: rdr.read_u8()?,
422                create_time_1: rdr.read_u16_le()?,
423                create_date: rdr.read_u16_le()?,
424                access_date: rdr.read_u16_le()?,
425                first_cluster_hi: rdr.read_u16_le()?,
426                modify_time: rdr.read_u16_le()?,
427                modify_date: rdr.read_u16_le()?,
428                first_cluster_lo: rdr.read_u16_le()?,
429                size: rdr.read_u32_le()?,
430            };
431            Ok(DirEntryData::File(data))
432        }
433    }
434
435    pub(crate) fn is_deleted(&self) -> bool {
436        match self {
437            DirEntryData::File(file) => file.is_deleted(),
438            DirEntryData::Lfn(lfn) => lfn.is_deleted(),
439        }
440    }
441
442    pub(crate) fn set_deleted(&mut self) {
443        match self {
444            DirEntryData::File(file) => file.set_deleted(),
445            DirEntryData::Lfn(lfn) => lfn.set_deleted(),
446        }
447    }
448
449    pub(crate) fn is_end(&self) -> bool {
450        match self {
451            DirEntryData::File(file) => file.is_end(),
452            DirEntryData::Lfn(lfn) => lfn.is_end(),
453        }
454    }
455}
456
457#[derive(Clone, Debug)]
458pub(crate) struct DirEntryEditor {
459    data: DirFileEntryData,
460    pos: u64,
461    dirty: bool,
462}
463
464impl DirEntryEditor {
465    fn new(data: DirFileEntryData, pos: u64) -> Self {
466        Self {
467            data,
468            pos,
469            dirty: false,
470        }
471    }
472
473    pub(crate) fn inner(&self) -> &DirFileEntryData {
474        &self.data
475    }
476
477    pub(crate) fn set_first_cluster(&mut self, first_cluster: Option<u32>, fat_type: FatType) {
478        if first_cluster != self.data.first_cluster(fat_type) {
479            self.data.set_first_cluster(first_cluster, fat_type);
480            self.dirty = true;
481        }
482    }
483
484    pub(crate) fn set_size(&mut self, size: u32) {
485        match self.data.size() {
486            Some(n) if size != n => {
487                self.data.set_size(size);
488                self.dirty = true;
489            }
490            _ => {}
491        }
492    }
493
494    pub(crate) fn set_created(&mut self, date_time: DateTime) {
495        if date_time != self.data.created() {
496            self.data.set_created(date_time);
497            self.dirty = true;
498        }
499    }
500
501    pub(crate) fn set_accessed(&mut self, date: Date) {
502        if date != self.data.accessed() {
503            self.data.set_accessed(date);
504            self.dirty = true;
505        }
506    }
507
508    pub(crate) fn set_modified(&mut self, date_time: DateTime) {
509        if date_time != self.data.modified() {
510            self.data.set_modified(date_time);
511            self.dirty = true;
512        }
513    }
514
515    pub(crate) fn flush<IO: ReadWriteSeek, TP, OCC>(&mut self, fs: &FileSystem<IO, TP, OCC>) -> Result<(), IO::Error> {
516        if self.dirty {
517            self.write(fs)?;
518            self.dirty = false;
519        }
520        Ok(())
521    }
522
523    fn write<IO: ReadWriteSeek, TP, OCC>(&self, fs: &FileSystem<IO, TP, OCC>) -> Result<(), IO::Error> {
524        let mut disk = fs.disk.borrow_mut();
525        disk.seek(io::SeekFrom::Start(self.pos))?;
526        self.data.serialize(&mut *disk)
527    }
528}
529
530/// A FAT directory entry.
531///
532/// `DirEntry` is returned by `DirIter` when reading a directory.
533#[derive(Clone)]
534pub struct DirEntry<'a, IO: ReadWriteSeek, TP, OCC> {
535    pub(crate) data: DirFileEntryData,
536    pub(crate) short_name: ShortName,
537    #[cfg(feature = "lfn")]
538    pub(crate) lfn_utf16: LfnBuffer,
539    pub(crate) entry_pos: u64,
540    pub(crate) offset_range: (u64, u64),
541    pub(crate) fs: &'a FileSystem<IO, TP, OCC>,
542}
543
544#[allow(clippy::len_without_is_empty)]
545impl<'a, IO: ReadWriteSeek, TP, OCC: OemCpConverter> DirEntry<'a, IO, TP, OCC> {
546    /// Returns short file name.
547    ///
548    /// Non-ASCII characters are replaced by the replacement character (U+FFFD).
549    #[cfg(feature = "alloc")]
550    #[must_use]
551    pub fn short_file_name(&self) -> String {
552        self.short_name.to_string(&self.fs.options.oem_cp_converter)
553    }
554
555    /// Returns short file name as byte array slice.
556    ///
557    /// Characters are encoded in the OEM codepage.
558    #[must_use]
559    pub fn short_file_name_as_bytes(&self) -> &[u8] {
560        self.short_name.as_bytes()
561    }
562
563    /// Returns long file name as u16 array slice.
564    ///
565    /// Characters are encoded in the UCS-2 encoding.
566    #[cfg(feature = "lfn")]
567    #[must_use]
568    pub fn long_file_name_as_ucs2_units(&self) -> Option<&[u16]> {
569        if self.lfn_utf16.len() > 0 {
570            Some(self.lfn_utf16.as_ucs2_units())
571        } else {
572            None
573        }
574    }
575
576    /// Returns long file name or if it doesn't exist fallbacks to short file name.
577    #[cfg(feature = "alloc")]
578    #[must_use]
579    pub fn file_name(&self) -> String {
580        #[cfg(feature = "lfn")]
581        {
582            let lfn_opt = self.long_file_name_as_ucs2_units();
583            if let Some(lfn) = lfn_opt {
584                return String::from_utf16_lossy(lfn);
585            }
586        }
587
588        self.data.lowercase_name().to_string(&self.fs.options.oem_cp_converter)
589    }
590
591    /// Returns file attributes.
592    #[must_use]
593    pub fn attributes(&self) -> FileAttributes {
594        self.data.attrs
595    }
596
597    /// Checks if entry belongs to directory.
598    #[must_use]
599    pub fn is_dir(&self) -> bool {
600        self.data.is_dir()
601    }
602
603    /// Checks if entry belongs to regular file.
604    #[must_use]
605    pub fn is_file(&self) -> bool {
606        self.data.is_file()
607    }
608
609    pub(crate) fn first_cluster(&self) -> Option<u32> {
610        self.data.first_cluster(self.fs.fat_type())
611    }
612
613    fn editor(&self) -> DirEntryEditor {
614        DirEntryEditor::new(self.data.clone(), self.entry_pos)
615    }
616
617    pub(crate) fn is_same_entry(&self, other: &DirEntry<IO, TP, OCC>) -> bool {
618        self.entry_pos == other.entry_pos
619    }
620
621    /// Returns `File` struct for this entry.
622    ///
623    /// # Panics
624    ///
625    /// Will panic if this is not a file.
626    #[must_use]
627    pub fn to_file(&self) -> File<'a, IO, TP, OCC> {
628        assert!(!self.is_dir(), "Not a file entry");
629        File::new(self.first_cluster(), Some(self.editor()), self.fs)
630    }
631
632    /// Returns `Dir` struct for this entry.
633    ///
634    /// # Panics
635    ///
636    /// Will panic if this is not a directory.
637    #[must_use]
638    pub fn to_dir(&self) -> Dir<'a, IO, TP, OCC> {
639        assert!(self.is_dir(), "Not a directory entry");
640        match self.first_cluster() {
641            Some(n) => {
642                let file = File::new(Some(n), Some(self.editor()), self.fs);
643                Dir::new(DirRawStream::File(file), self.fs)
644            }
645            None => self.fs.root_dir(),
646        }
647    }
648
649    /// Returns file size or 0 for directory.
650    #[must_use]
651    pub fn len(&self) -> u64 {
652        u64::from(self.data.size)
653    }
654
655    /// Returns file creation date and time.
656    ///
657    /// Resolution of the time field is 1/100s.
658    #[must_use]
659    pub fn created(&self) -> DateTime {
660        self.data.created()
661    }
662
663    /// Returns file last access date.
664    #[must_use]
665    pub fn accessed(&self) -> Date {
666        self.data.accessed()
667    }
668
669    /// Returns file last modification date and time.
670    ///
671    /// Resolution of the time field is 2s.
672    #[must_use]
673    pub fn modified(&self) -> DateTime {
674        self.data.modified()
675    }
676
677    pub(crate) fn raw_short_name(&self) -> &[u8; SFN_SIZE] {
678        &self.data.name
679    }
680
681    #[cfg(feature = "lfn")]
682    fn eq_name_lfn(&self, name: &str) -> bool {
683        if let Some(lfn) = self.long_file_name_as_ucs2_units() {
684            let self_decode_iter = char::decode_utf16(lfn.iter().copied());
685            let mut other_uppercase_iter = name.chars().flat_map(char_to_uppercase);
686            for decode_result in self_decode_iter {
687                if let Ok(self_char) = decode_result {
688                    for self_uppercase_char in char_to_uppercase(self_char) {
689                        // compare each character in uppercase
690                        if Some(self_uppercase_char) != other_uppercase_iter.next() {
691                            return false;
692                        }
693                    }
694                } else {
695                    // decoding failed
696                    return false;
697                }
698            }
699            // both iterators should be at the end here
700            other_uppercase_iter.next().is_none()
701        } else {
702            // entry has no long name
703            false
704        }
705    }
706
707    pub fn eq_name(&self, name: &str) -> bool {
708        #[cfg(feature = "lfn")]
709        {
710            if self.eq_name_lfn(name) {
711                return true;
712            }
713        }
714
715        self.short_name.eq_ignore_case(name, &self.fs.options.oem_cp_converter)
716    }
717}
718
719impl<IO: ReadWriteSeek, TP, OCC> fmt::Debug for DirEntry<'_, IO, TP, OCC> {
720    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
721        self.data.fmt(f)
722    }
723}
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728    use crate::fs::LossyOemCpConverter;
729
730    #[test]
731    fn short_name_with_ext() {
732        let oem_cp_conv = LossyOemCpConverter::new();
733        assert_eq!(ShortName::new(b"FOO     BAR").to_string(&oem_cp_conv), "FOO.BAR");
734        assert_eq!(ShortName::new(b"LOOK AT M E").to_string(&oem_cp_conv), "LOOK AT.M E");
735        assert_eq!(
736            ShortName::new(b"\x99OOK AT M \x99").to_string(&oem_cp_conv),
737            "\u{FFFD}OOK AT.M \u{FFFD}"
738        );
739        assert!(ShortName::new(b"\x99OOK AT M \x99").eq_ignore_case("\u{FFFD}OOK AT.M \u{FFFD}", &oem_cp_conv));
740    }
741
742    #[test]
743    fn short_name_without_ext() {
744        let oem_cp_conv = LossyOemCpConverter::new();
745        assert_eq!(ShortName::new(b"FOO        ").to_string(&oem_cp_conv), "FOO");
746        assert_eq!(ShortName::new(b"LOOK AT    ").to_string(&oem_cp_conv), "LOOK AT");
747    }
748
749    #[test]
750    fn short_name_eq_ignore_case() {
751        let oem_cp_conv = LossyOemCpConverter::new();
752        let raw_short_name: &[u8; SFN_SIZE] = b"\x99OOK AT M \x99";
753        assert!(ShortName::new(raw_short_name).eq_ignore_case("\u{FFFD}OOK AT.M \u{FFFD}", &oem_cp_conv));
754        assert!(ShortName::new(raw_short_name).eq_ignore_case("\u{FFFD}ook AT.m \u{FFFD}", &oem_cp_conv));
755    }
756
757    #[test]
758    fn short_name_05_changed_to_e5() {
759        let raw_short_name = [0x05; SFN_SIZE];
760        assert_eq!(
761            ShortName::new(&raw_short_name).as_bytes(),
762            [0xE5, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, b'.', 0x05, 0x05, 0x05]
763        );
764    }
765
766    #[test]
767    fn lowercase_short_name() {
768        let oem_cp_conv = LossyOemCpConverter::new();
769        let raw_short_name: &[u8; SFN_SIZE] = b"FOO     RS ";
770        let mut raw_entry = DirFileEntryData {
771            name: *raw_short_name,
772            reserved_0: (1 << 3) | (1 << 4),
773            ..DirFileEntryData::default()
774        };
775        assert_eq!(raw_entry.lowercase_name().to_string(&oem_cp_conv), "foo.rs");
776        raw_entry.reserved_0 = 1 << 3;
777        assert_eq!(raw_entry.lowercase_name().to_string(&oem_cp_conv), "foo.RS");
778        raw_entry.reserved_0 = 1 << 4;
779        assert_eq!(raw_entry.lowercase_name().to_string(&oem_cp_conv), "FOO.rs");
780        raw_entry.reserved_0 = 0;
781        assert_eq!(raw_entry.lowercase_name().to_string(&oem_cp_conv), "FOO.RS");
782    }
783}