Skip to main content

hadris_fat/
dir.rs

1io_transform! {
2
3use core::mem::size_of;
4
5use hadris_common::types::endian::Endian;
6
7use crate::error::{Error, Result};
8use crate::file::ShortFileName;
9#[cfg(feature = "lfn")]
10use crate::file::{LfnBuilder, LongFileName};
11use crate::raw::{DirEntryAttrFlags, NtCaseFlags, RawDirectoryEntry};
12use crate::time::FatDateTime;
13use super::fs::FatVolume;
14#[cfg(not(feature = "alloc"))]
15use super::io::ReadExt;
16use super::io::{Cluster, ClusterLike, Read, Seek, SeekFrom};
17use super::read::FileReader;
18
19/// Formats a stored 8.3 short name as its human-facing filename: the NT case
20/// flags are applied, trailing space padding is trimmed from the base and
21/// extension, and the `.` separator is dropped when there is no extension.
22///
23/// [`ShortFileName::as_str`] returns the padded on-disk field (`README  .TXT`);
24/// this returns the logical name (`readme.txt`).
25#[cfg(feature = "alloc")]
26fn short_name_display(short: &ShortFileName, flags: NtCaseFlags) -> alloc::string::String {
27    // Short names are OEM-encoded on disk, so high bytes are not necessarily
28    // valid UTF-8 — decode lossily instead of panicking (as `as_str` would).
29    let raw = alloc::string::String::from_utf8_lossy(short.as_padded_bytes());
30    if raw == "." || raw == ".." {
31        return raw.into_owned();
32    }
33
34    let cased = short.with_nt_case(flags);
35    let raw = alloc::string::String::from_utf8_lossy(cased.as_padded_bytes());
36    let (base, ext) = match raw.find('.') {
37        Some(dot) => (raw[..dot].trim_end(), raw[dot + 1..].trim_end()),
38        None => (raw.trim_end(), ""),
39    };
40    let mut name = alloc::string::String::with_capacity(base.len() + 1 + ext.len());
41    name.push_str(base);
42    if !ext.is_empty() {
43        name.push('.');
44        name.push_str(ext);
45    }
46    name
47}
48
49/// Directory-slot coordinates of a subdirectory's own entry in its parent,
50/// captured so write operations can confirm the directory still exists before
51/// writing entries into its clusters (a stale `FatDir` whose directory was
52/// deleted and whose cluster was reused would otherwise corrupt another file).
53/// `None` for the root directory, which can never be deleted.
54#[cfg(feature = "write")]
55#[derive(Clone, Copy)]
56pub(crate) struct DirSlot {
57    pub(crate) parent_clus: Cluster<usize>,
58    pub(crate) offset_within_cluster: usize,
59    pub(crate) short_name: ShortFileName,
60    /// Creation timestamp captured at lookup, used alongside the short name to
61    /// tell a live handle from one whose slot was reused by a same-named file.
62    pub(crate) created: FatDateTime,
63}
64
65#[cfg(feature = "write")]
66impl DirSlot {
67    pub(crate) fn from_entry(entry: &FileEntry) -> Self {
68        Self {
69            parent_clus: entry.parent_clus,
70            offset_within_cluster: entry.offset_within_cluster,
71            short_name: entry.short_name,
72            created: entry.created,
73        }
74    }
75}
76
77/// A directory within a mounted FAT filesystem.
78pub struct FatDir<'a, DATA: Read + Seek> {
79    pub(crate) data: &'a FatVolume<DATA>,
80    /// Cluster for subdirectories, or 0 (sentinel) for FAT12/16 fixed root
81    pub(crate) cluster: Cluster,
82    /// For FAT12/16 root: (start_byte, size_bytes), None for cluster-based dirs
83    pub(crate) fixed_root: Option<(usize, usize)>,
84    /// Slot of this directory's own entry in its parent, for stale-handle
85    /// revalidation on write. `None` for the root.
86    #[cfg(feature = "write")]
87    pub(crate) dir_entry: Option<DirSlot>,
88}
89
90impl<'a, DATA: Read + Seek> FatDir<'a, DATA> {
91    /// Creates an iterator over this directory's entries.
92    #[cfg(feature = "lfn")]
93    pub fn entries(&self) -> FatDirIter<'a, DATA> {
94        FatDirIter {
95            data: self.data,
96            cluster: self.cluster,
97            #[cfg(feature = "write")]
98            dir_start_cluster: self.cluster,
99            offset: 0,
100            fixed_root_remaining: self.fixed_root.map(|(_, size)| size),
101            fixed_root_start: self.fixed_root.map(|(start, _)| start),
102            cluster_steps: 0,
103            lfn_builder: LfnBuilder::new(),
104            #[cfg(feature = "alloc")]
105            cluster_buffer: None,
106            #[cfg(feature = "alloc")]
107            buffer_valid: false,
108            #[cfg(feature = "alloc")]
109            buffer_base: 0,
110        }
111    }
112
113    /// Creates an iterator over this directory's entries.
114    #[cfg(not(feature = "lfn"))]
115    pub fn entries(&self) -> FatDirIter<'a, DATA> {
116        FatDirIter {
117            data: self.data,
118            cluster: self.cluster,
119            #[cfg(feature = "write")]
120            dir_start_cluster: self.cluster,
121            offset: 0,
122            fixed_root_remaining: self.fixed_root.map(|(_, size)| size),
123            fixed_root_start: self.fixed_root.map(|(start, _)| start),
124            cluster_steps: 0,
125            #[cfg(feature = "alloc")]
126            cluster_buffer: None,
127            #[cfg(feature = "alloc")]
128            buffer_valid: false,
129            #[cfg(feature = "alloc")]
130            buffer_base: 0,
131        }
132    }
133
134    /// Open a subdirectory from a file entry.
135    ///
136    /// The entry must be a directory.
137    pub fn open_entry(&self, entry: &FileEntry) -> Result<FatDir<'a, DATA>> {
138        if !entry.is_directory() {
139            return Err(Error::NotADirectory);
140        }
141        Ok(FatDir {
142            data: self.data,
143            cluster: entry.cluster(),
144            fixed_root: None, // Subdirectories are never fixed root
145            #[cfg(feature = "write")]
146            dir_entry: Some(DirSlot::from_entry(entry)),
147        })
148    }
149
150    /// Find an entry by name.
151    ///
152    /// When the `lfn` feature is enabled, this performs a case-sensitive match
153    /// against long file names first, then falls back to case-insensitive short
154    /// name matching.
155    ///
156    /// Without the `lfn` feature, only case-insensitive short name matching is used.
157    pub async fn find(&self, name: &str) -> Result<Option<FileEntry>> {
158        let mut iter = self.entries();
159        loop {
160            match iter.next_entry().await {
161                Some(result) => {
162                    let DirectoryEntry::Entry(file_entry) = result?;
163
164                    // Check LFN match (case-sensitive)
165                    #[cfg(feature = "lfn")]
166                    if let Some(lfn) = file_entry.long_name()
167                        && lfn.eq_str(name)
168                    {
169                        return Ok(Some(file_entry));
170                    }
171                    // Check short name match (case-insensitive, handles 8.3 padding)
172                    if file_entry.short_name().matches(name) {
173                        return Ok(Some(file_entry));
174                    }
175                }
176                None => return Ok(None),
177            }
178        }
179    }
180
181    /// Open a subdirectory by name.
182    ///
183    /// Returns an error if the entry is not found or is not a directory.
184    pub async fn open_dir(&self, name: &str) -> Result<FatDir<'a, DATA>> {
185        let entry = self.find(name).await?.ok_or(Error::EntryNotFound)?;
186
187        if !entry.is_directory() {
188            return Err(Error::NotADirectory);
189        }
190
191        // Subdirectories always use cluster chains, never fixed root
192        Ok(FatDir {
193            data: self.data,
194            cluster: entry.cluster(),
195            fixed_root: None,
196            #[cfg(feature = "write")]
197            dir_entry: Some(DirSlot::from_entry(&entry)),
198        })
199    }
200
201    /// Open a file for reading by name.
202    ///
203    /// Returns an error if the entry is not found or is a directory.
204    pub async fn open_file(&self, name: &str) -> Result<FileReader<'a, DATA>> {
205        let entry = self.find(name).await?.ok_or(Error::EntryNotFound)?;
206        FileReader::new(self.data, &entry)
207    }
208}
209
210/// Stateful iterator over the entries in a FAT directory.
211pub struct FatDirIter<'a, DATA: Read + Seek> {
212    data: &'a FatVolume<DATA>,
213    /// Current cluster (or 0 for fixed root directory)
214    cluster: Cluster,
215    /// First cluster of the directory chain (or 0 for a fixed root).
216    #[cfg(feature = "write")]
217    dir_start_cluster: Cluster,
218    /// Offset within current cluster (or within fixed root dir)
219    offset: usize,
220    /// For fixed root directory: remaining bytes to read (None for cluster-based)
221    fixed_root_remaining: Option<usize>,
222    /// For fixed root directory: start byte offset
223    fixed_root_start: Option<usize>,
224    /// Cluster transitions taken so far. A cluster-based directory chain
225    /// longer than `max_cluster` clusters has to revisit one — that's a
226    /// loop and we abort with `Error::ClusterLoop`.
227    cluster_steps: u32,
228    #[cfg(feature = "lfn")]
229    lfn_builder: LfnBuilder,
230    /// Buffered cluster data (reduces seeks by reading entire cluster at once)
231    #[cfg(feature = "alloc")]
232    cluster_buffer: Option<alloc::vec::Vec<u8>>,
233    /// Whether the buffer is valid for the current cluster
234    #[cfg(feature = "alloc")]
235    buffer_valid: bool,
236    /// Directory-region-relative offset the buffer starts at. Always 0 for
237    /// cluster-based directories (the buffer holds one whole cluster); for
238    /// fixed root directories it tracks which 4 KiB window of the root region
239    /// is buffered so the iterator can slide the window forward instead of
240    /// stopping at the first 4096 bytes.
241    #[cfg(feature = "alloc")]
242    buffer_base: usize,
243}
244
245impl<DATA: Read + Seek> FatDirIter<'_, DATA> {
246    /// Read the next directory entry.
247    pub async fn next_entry(&mut self) -> Option<Result<DirectoryEntry>> {
248        let mut data = self.data.data.lock();
249        let entry_size = size_of::<RawDirectoryEntry>();
250        let cluster_size = data.cluster_size;
251
252        loop {
253            // Check bounds and handle cluster transitions
254            if let Some(ref mut remaining) = self.fixed_root_remaining {
255                // Fixed root directory (FAT12/16)
256                if *remaining < entry_size {
257                    return None; // End of fixed root directory
258                }
259            } else {
260                // Cluster-based directory (FAT32 or subdirectory).
261                // A directory entry whose first cluster is 0 has no data
262                // allocated (1 is reserved); treat it as an empty directory
263                // instead of underflowing the cluster-to-offset math.
264                if self.cluster.0 < 2 {
265                    return None;
266                }
267                // Check if we need to move to the next cluster
268                if self.offset >= cluster_size {
269                    self.cluster_steps = self.cluster_steps.saturating_add(1);
270                    if self.cluster_steps > self.data.fat.max_cluster() {
271                        return Some(Err(Error::ClusterLoop {
272                            cluster: self.cluster.0 as u32,
273                        }));
274                    }
275                    // Drop data lock so the routed helper can acquire cache
276                    // first (canonical order) without deadlocking. Re-lock
277                    // after.
278                    drop(data);
279                    let next = match self.data.next_cluster_routed(self.cluster.0).await {
280                        Ok(n) => n,
281                        Err(e) => return Some(Err(e)),
282                    };
283                    data = self.data.data.lock();
284                    match next {
285                        Some(cluster) => {
286                            self.cluster.0 = cluster as usize;
287                            self.offset = 0;
288                            #[cfg(feature = "alloc")]
289                            {
290                                self.buffer_valid = false;
291                            }
292                        }
293                        None => return None, // End of directory
294                    }
295                }
296            }
297
298            // Read the entry - use buffering when alloc is available
299            #[cfg(feature = "alloc")]
300            let raw_entry = {
301                // Refill when the buffer is stale or, for a fixed root, when
302                // the read offset has walked past the buffered window — the
303                // window slides forward in 4 KiB chunks instead of ending the
304                // directory at the first 4096 bytes.
305                let needs_refill = !self.buffer_valid
306                    || self.cluster_buffer.is_none()
307                    || (self.fixed_root_remaining.is_some()
308                        && (self.offset < self.buffer_base
309                            || self.offset + entry_size
310                                > self.buffer_base + self.cluster_buffer.as_ref().unwrap().len()));
311                if needs_refill {
312                    let buffer_size = if let Some(remaining) = self.fixed_root_remaining {
313                        // For fixed root, buffer the remaining bytes (up to a reasonable size)
314                        remaining.min(4096)
315                    } else {
316                        cluster_size
317                    };
318
319                    self.buffer_base = if self.fixed_root_remaining.is_some() {
320                        self.offset
321                    } else {
322                        0
323                    };
324                    let seek_pos = if self.fixed_root_remaining.is_some() {
325                        let start = self.fixed_root_start.unwrap();
326                        (start + self.buffer_base) as u64
327                    } else {
328                        self.cluster
329                            .to_bytes(self.data.info.data_start, cluster_size)
330                            as u64
331                    };
332
333                    if let Err(e) = data.seek(SeekFrom::Start(seek_pos)).await {
334                        return Some(Err(Error::Io(e.erase())));
335                    }
336
337                    let mut buffer = alloc::vec![0u8; buffer_size];
338                    if let Err(e) = data.read_exact(&mut buffer).await {
339                        return Some(Err(Error::Io(e.erase())));
340                    }
341
342                    self.cluster_buffer = Some(buffer);
343                    self.buffer_valid = true;
344                }
345
346                // Read entry from buffer
347                let buffer = self.cluster_buffer.as_ref().unwrap();
348                let offset = self.offset - self.buffer_base;
349
350                if offset + entry_size > buffer.len() {
351                    // Buffer exhausted, need to handle this case
352                    // For fixed root: fewer than entry_size bytes remain
353                    // For cluster-based: handled by cluster transition above
354                    if self.fixed_root_remaining.is_some() {
355                        return None;
356                    }
357                    continue;
358                }
359
360                let entry_bytes: [u8; 32] = buffer[offset..offset + entry_size].try_into().unwrap();
361
362                // Safety: RawDirectoryEntry is a union of properly aligned types
363                // and entry_bytes has the correct size
364                unsafe { core::mem::transmute::<[u8; 32], RawDirectoryEntry>(entry_bytes) }
365            };
366
367            #[cfg(not(feature = "alloc"))]
368            let raw_entry = {
369                // Calculate seek position
370                let seek_pos = if self.fixed_root_remaining.is_some() {
371                    let start = self.fixed_root_start.unwrap();
372                    (start + self.offset) as u64
373                } else {
374                    self.cluster
375                        .to_bytes(self.data.info.data_start, cluster_size)
376                        as u64
377                        + self.offset as u64
378                };
379
380                if let Err(e) = data.seek(SeekFrom::Start(seek_pos)).await {
381                    return Some(Err(Error::Io(e.erase())));
382                }
383
384                // Read the directory entry
385                match data.read_struct::<RawDirectoryEntry>().await {
386                    Ok(e) => e,
387                    Err(e) => return Some(Err(Error::Io(e))),
388                }
389            };
390
391            let entry_bytes = unsafe { raw_entry.bytes };
392
393            // Check for end of directory
394            if entry_bytes[0] == 0 {
395                #[cfg(feature = "lfn")]
396                self.lfn_builder.reset();
397                return None;
398            }
399
400            // Check for deleted entry
401            if entry_bytes[0] == 0xE5 {
402                self.offset += entry_size;
403                if let Some(ref mut remaining) = self.fixed_root_remaining {
404                    *remaining = remaining.saturating_sub(entry_size);
405                }
406                #[cfg(feature = "lfn")]
407                self.lfn_builder.reset(); // Deleted entry breaks LFN sequence
408                continue;
409            }
410
411            self.offset += entry_size;
412            if let Some(ref mut remaining) = self.fixed_root_remaining {
413                *remaining = remaining.saturating_sub(entry_size);
414            }
415
416            // Check if this is an LFN entry (attributes == LONG_NAME)
417            #[cfg(feature = "lfn")]
418            {
419                let entry_attr = unsafe { raw_entry.file }.attributes;
420                if entry_attr == DirEntryAttrFlags::LONG_NAME.bits() {
421                    // This is an LFN entry
422                    let lfn = unsafe { raw_entry.lfn };
423                    let seq = lfn.sequence_number;
424
425                    // Check if this is the start of a new LFN sequence (has 0x40 bit set)
426                    if seq & LfnBuilder::LAST_ENTRY_MASK != 0 {
427                        self.lfn_builder.start(seq, lfn.checksum);
428                    }
429
430                    if self.lfn_builder.building {
431                        self.lfn_builder.add_entry(
432                            seq,
433                            lfn.checksum,
434                            &lfn.name1,
435                            &lfn.name2,
436                            &lfn.name3,
437                        );
438                    }
439                    continue;
440                }
441            }
442
443            // This is a regular file/directory entry
444            let file_entry = unsafe { raw_entry.file };
445
446            let attr = DirEntryAttrFlags::from_bits_retain(file_entry.attributes);
447            // Skip any entry carrying VOLUME_ID: both plain label entries and
448            // corrupt combinations (e.g. VOLUME_ID|DIRECTORY), which the FAT
449            // spec does not define as listable entries. LFN components
450            // (exactly LONG_NAME) were already handled above.
451            if attr.contains(DirEntryAttrFlags::VOLUME_ID) {
452                #[cfg(feature = "lfn")]
453                self.lfn_builder.reset();
454                continue;
455            }
456
457            // Convert 0x05 back to 0xE5 for kanji compatibility
458            let mut name_bytes = file_entry.name;
459            if name_bytes[0] == 0x05 {
460                name_bytes[0] = 0xE5;
461            }
462
463            let short_name = match ShortFileName::new(name_bytes) {
464                Ok(n) => n,
465                Err(_) => return Some(Err(Error::InvalidShortFilename)),
466            };
467
468            // Try to get the LFN if we've been building one
469            #[cfg(feature = "lfn")]
470            let long_name = self.lfn_builder.finish(&short_name);
471
472            // For FAT12/16 with fixed root dir, parent_clus is 0 (sentinel)
473            // For cluster-based dirs, parent_clus is the actual cluster
474            let created = FatDateTime::from_raw(
475                u16::from_le_bytes(file_entry.creation_date),
476                u16::from_le_bytes(file_entry.creation_time),
477                file_entry.creation_time_tenth,
478            );
479            let modified = FatDateTime::from_raw(
480                u16::from_le_bytes(file_entry.last_write_date),
481                u16::from_le_bytes(file_entry.last_write_time),
482                0,
483            );
484            let last_access_date = u16::from_le_bytes(file_entry.last_access_date);
485
486            return Some(Ok(DirectoryEntry::Entry(FileEntry {
487                short_name,
488                nt_case: NtCaseFlags::from_bits_truncate(file_entry.reserved),
489                #[cfg(feature = "lfn")]
490                long_name,
491                attr,
492                size: file_entry.size.get() as usize,
493                #[cfg(feature = "write")]
494                parent_dir_clus: self.dir_start_cluster,
495                #[cfg(feature = "write")]
496                parent_clus: self.cluster,
497                #[cfg(feature = "write")]
498                offset_within_cluster: self.offset - entry_size,
499                cluster: Cluster::from_parts(
500                    file_entry.first_cluster_high.get(),
501                    file_entry.first_cluster_low.get(),
502                ),
503                created,
504                last_access_date,
505                modified,
506            })));
507        }
508    }
509}
510
511#[derive(Debug, Clone)]
512/// A parsed FAT directory record.
513pub enum DirectoryEntry {
514    /// A file or directory entry
515    Entry(FileEntry),
516}
517
518impl DirectoryEntry {
519    /// Get the display name of the entry.
520    /// Returns the long filename if available, otherwise the short name.
521    ///
522    /// Requires the `alloc` feature. See [`FileEntry::name`].
523    #[cfg(feature = "alloc")]
524    pub fn name(&self) -> alloc::borrow::Cow<'_, str> {
525        match self {
526            Self::Entry(ent) => ent.name(),
527        }
528    }
529
530    /// Get the file entry if this is an Entry variant
531    pub fn as_entry(&self) -> Option<&FileEntry> {
532        match self {
533            Self::Entry(ent) => Some(ent),
534        }
535    }
536}
537
538#[derive(Debug, Clone)]
539/// A parsed value together with non-fatal filesystem diagnostics.
540pub struct ParseInfo<T> {
541    /// Parsed value.
542    pub data: T,
543    /// Warnings encountered while parsing.
544    pub warnings: FileSystemWarnings,
545    /// Errors accumulated while parsing.
546    pub errors: FileSystemErrors,
547}
548
549bitflags::bitflags! {
550    /// Non-fatal warnings discovered while parsing a filesystem object.
551    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
552    pub struct FileSystemWarnings: u64 {
553
554    }
555
556    /// Errors accumulated while parsing a filesystem object.
557    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
558    pub struct FileSystemErrors: u64 {
559
560    }
561}
562
563#[derive(Debug, Clone)]
564/// Metadata for a file or subdirectory entry.
565pub struct FileEntry {
566    pub(crate) short_name: ShortFileName,
567    /// Windows NT 8.3 name case flags (`DIR_NTRes`); applied to the display
568    /// name when no long name is present.
569    pub(crate) nt_case: NtCaseFlags,
570    #[cfg(feature = "lfn")]
571    pub(crate) long_name: Option<LongFileName>,
572    pub(crate) attr: DirEntryAttrFlags,
573    pub(crate) size: usize,
574    /// First cluster of the containing directory (0 for a fixed root).
575    #[cfg(feature = "write")]
576    pub(crate) parent_dir_clus: Cluster<usize>,
577    /// Cluster containing this short directory entry.
578    #[cfg(feature = "write")]
579    pub(crate) parent_clus: Cluster<usize>,
580    /// Offset of this entry within the parent cluster (used for write operations)
581    #[cfg(feature = "write")]
582    pub(crate) offset_within_cluster: usize,
583    pub(crate) cluster: Cluster<usize>,
584    /// Creation time (date + time + 10ms-units).
585    pub(crate) created: FatDateTime,
586    /// Last-access date (FAT stores no time component for access).
587    pub(crate) last_access_date: u16,
588    /// Last-modified time (date + time; `time_tenth` is 0).
589    pub(crate) modified: FatDateTime,
590}
591
592impl FileEntry {
593    /// Get the file's display name.
594    ///
595    /// Returns the long filename if available, otherwise the short name. The
596    /// `Cow` is borrowed for short names and owned (allocated) for long names,
597    /// since long names are stored internally as UTF-16 and require decoding
598    /// before they can be exposed as a `&str`.
599    ///
600    /// Requires the `alloc` feature. Without `alloc`, use [`Self::short_name`]
601    /// and [`Self::long_name`] (with [`LongFileName::chars`] /
602    /// [`LongFileName::eq_str`]) directly.
603    #[cfg(feature = "alloc")]
604    pub fn name(&self) -> alloc::borrow::Cow<'_, str> {
605        #[cfg(feature = "lfn")]
606        {
607            use alloc::string::ToString;
608            if let Some(ref lfn) = self.long_name {
609                return alloc::borrow::Cow::Owned(lfn.to_string());
610            }
611        }
612        alloc::borrow::Cow::Owned(short_name_display(&self.short_name, self.nt_case))
613    }
614
615    /// Get the short (8.3) filename, in its canonical on-disk (uppercase) form.
616    ///
617    /// Use [`Self::nt_case`] with [`ShortFileName::with_nt_case`] to recover the
618    /// original lowercase presentation without allocating.
619    pub fn short_name(&self) -> &ShortFileName {
620        &self.short_name
621    }
622
623    /// Windows NT 8.3 name case flags (`DIR_NTRes`) for this entry.
624    ///
625    /// Records whether the short name's base and/or extension were originally
626    /// lowercase. Meaningful only when the entry has no long file name.
627    pub fn nt_case(&self) -> NtCaseFlags {
628        self.nt_case
629    }
630
631    /// Get the long filename, if available
632    #[cfg(feature = "lfn")]
633    pub fn long_name(&self) -> Option<&LongFileName> {
634        self.long_name.as_ref()
635    }
636
637    /// Get the file attributes
638    pub fn attributes(&self) -> DirEntryAttrFlags {
639        self.attr
640    }
641
642    /// Check if this entry is a directory
643    pub fn is_directory(&self) -> bool {
644        self.attr.contains(DirEntryAttrFlags::DIRECTORY)
645    }
646
647    /// Check if this entry is a regular file
648    pub fn is_file(&self) -> bool {
649        !self.is_directory()
650    }
651
652    /// Returns the file length in bytes (zero for directories).
653    pub fn len(&self) -> u64 {
654        self.size as u64
655    }
656
657    /// Returns whether this entry has a zero byte length.
658    pub fn is_empty(&self) -> bool {
659        self.len() == 0
660    }
661
662    /// Creation timestamp (date + time + 10-ms units).
663    pub fn created(&self) -> FatDateTime {
664        self.created
665    }
666
667    /// Last-access date. FAT does not store an access time, only a date.
668    /// Returns the raw FAT-encoded date `(year-1980)<<9 | month<<5 | day`.
669    pub fn accessed_date(&self) -> u16 {
670        self.last_access_date
671    }
672
673    /// Last-modified timestamp (date + time; sub-2-second precision is not
674    /// preserved by FAT for the modified field).
675    pub fn modified(&self) -> FatDateTime {
676        self.modified
677    }
678
679    /// Get the first cluster of the file data
680    pub fn cluster(&self) -> Cluster<usize> {
681        self.cluster
682    }
683}
684
685} // end io_transform!
686
687sync_only! {
688
689impl<DATA: Read + Seek> Iterator for FatDirIter<'_, DATA> {
690    type Item = Result<DirectoryEntry>;
691
692    fn next(&mut self) -> Option<Self::Item> {
693        let mut data = self.data.data.lock();
694        let entry_size = size_of::<RawDirectoryEntry>();
695        let cluster_size = data.cluster_size;
696
697        loop {
698            // Check bounds and handle cluster transitions
699            if let Some(ref mut remaining) = self.fixed_root_remaining {
700                // Fixed root directory (FAT12/16)
701                if *remaining < entry_size {
702                    return None; // End of fixed root directory
703                }
704            } else {
705                // Cluster-based directory (FAT32 or subdirectory).
706                // A directory entry whose first cluster is 0 has no data
707                // allocated (1 is reserved); treat it as an empty directory
708                // instead of underflowing the cluster-to-offset math.
709                if self.cluster.0 < 2 {
710                    return None;
711                }
712                // Check if we need to move to the next cluster
713                if self.offset >= cluster_size {
714                    self.cluster_steps = self.cluster_steps.saturating_add(1);
715                    if self.cluster_steps > self.data.fat.max_cluster() {
716                        return Some(Err(Error::ClusterLoop {
717                            cluster: self.cluster.0 as u32,
718                        }));
719                    }
720                    // Drop data lock so next_cluster_routed can acquire
721                    // cache+data in canonical order; re-lock afterwards.
722                    drop(data);
723                    let next = match self.data.next_cluster_routed(self.cluster.0) {
724                        Ok(n) => n,
725                        Err(e) => return Some(Err(e)),
726                    };
727                    data = self.data.data.lock();
728                    match next {
729                        Some(cluster) => {
730                            self.cluster.0 = cluster as usize;
731                            self.offset = 0;
732                            #[cfg(feature = "alloc")]
733                            {
734                                self.buffer_valid = false;
735                            }
736                        }
737                        None => return None, // End of directory
738                    }
739                }
740            }
741
742            // Read the entry - use buffering when alloc is available
743            #[cfg(feature = "alloc")]
744            let raw_entry = {
745                // Refill when the buffer is stale or, for a fixed root, when
746                // the read offset has walked past the buffered window — the
747                // window slides forward in 4 KiB chunks instead of ending the
748                // directory at the first 4096 bytes.
749                let needs_refill = !self.buffer_valid
750                    || self.cluster_buffer.is_none()
751                    || (self.fixed_root_remaining.is_some()
752                        && (self.offset < self.buffer_base
753                            || self.offset + entry_size
754                                > self.buffer_base + self.cluster_buffer.as_ref().unwrap().len()));
755                if needs_refill {
756                    let buffer_size = if let Some(remaining) = self.fixed_root_remaining {
757                        // For fixed root, buffer the remaining bytes (up to a reasonable size)
758                        remaining.min(4096)
759                    } else {
760                        cluster_size
761                    };
762
763                    self.buffer_base = if self.fixed_root_remaining.is_some() {
764                        self.offset
765                    } else {
766                        0
767                    };
768                    let seek_pos = if self.fixed_root_remaining.is_some() {
769                        let start = self.fixed_root_start.unwrap();
770                        (start + self.buffer_base) as u64
771                    } else {
772                        self.cluster
773                            .to_bytes(self.data.info.data_start, cluster_size)
774                            as u64
775                    };
776
777                    if let Err(e) = data.seek(SeekFrom::Start(seek_pos)) {
778                        return Some(Err(Error::Io(e.erase())));
779                    }
780
781                    let mut buffer = alloc::vec![0u8; buffer_size];
782                    if let Err(e) = data.read_exact(&mut buffer) {
783                        return Some(Err(Error::Io(e.erase())));
784                    }
785
786                    self.cluster_buffer = Some(buffer);
787                    self.buffer_valid = true;
788                }
789
790                // Read entry from buffer
791                let buffer = self.cluster_buffer.as_ref().unwrap();
792                let offset = self.offset - self.buffer_base;
793
794                if offset + entry_size > buffer.len() {
795                    // Buffer exhausted, need to handle this case
796                    // For fixed root: fewer than entry_size bytes remain
797                    // For cluster-based: handled by cluster transition above
798                    if self.fixed_root_remaining.is_some() {
799                        return None;
800                    }
801                    continue;
802                }
803
804                let entry_bytes: [u8; 32] = buffer[offset..offset + entry_size].try_into().unwrap();
805
806                // Safety: RawDirectoryEntry is a union of properly aligned types
807                // and entry_bytes has the correct size
808                unsafe { core::mem::transmute::<[u8; 32], RawDirectoryEntry>(entry_bytes) }
809            };
810
811            #[cfg(not(feature = "alloc"))]
812            let raw_entry = {
813                // Calculate seek position
814                let seek_pos = if self.fixed_root_remaining.is_some() {
815                    let start = self.fixed_root_start.unwrap();
816                    (start + self.offset) as u64
817                } else {
818                    self.cluster
819                        .to_bytes(self.data.info.data_start, cluster_size)
820                        as u64
821                        + self.offset as u64
822                };
823
824                if let Err(e) = data.seek(SeekFrom::Start(seek_pos)) {
825                    return Some(Err(Error::Io(e.erase())));
826                }
827
828                // Read the directory entry
829                match data.read_struct::<RawDirectoryEntry>() {
830                    Ok(e) => e,
831                    Err(e) => return Some(Err(Error::Io(e))),
832                }
833            };
834
835            let entry_bytes = unsafe { raw_entry.bytes };
836
837            // Check for end of directory
838            if entry_bytes[0] == 0 {
839                #[cfg(feature = "lfn")]
840                self.lfn_builder.reset();
841                return None;
842            }
843
844            // Check for deleted entry
845            if entry_bytes[0] == 0xE5 {
846                self.offset += entry_size;
847                if let Some(ref mut remaining) = self.fixed_root_remaining {
848                    *remaining = remaining.saturating_sub(entry_size);
849                }
850                #[cfg(feature = "lfn")]
851                self.lfn_builder.reset(); // Deleted entry breaks LFN sequence
852                continue;
853            }
854
855            self.offset += entry_size;
856            if let Some(ref mut remaining) = self.fixed_root_remaining {
857                *remaining = remaining.saturating_sub(entry_size);
858            }
859
860            // Check if this is an LFN entry (attributes == LONG_NAME)
861            #[cfg(feature = "lfn")]
862            {
863                let entry_attr = unsafe { raw_entry.file }.attributes;
864                if entry_attr == DirEntryAttrFlags::LONG_NAME.bits() {
865                    // This is an LFN entry
866                    let lfn = unsafe { raw_entry.lfn };
867                    let seq = lfn.sequence_number;
868
869                    // Check if this is the start of a new LFN sequence (has 0x40 bit set)
870                    if seq & LfnBuilder::LAST_ENTRY_MASK != 0 {
871                        self.lfn_builder.start(seq, lfn.checksum);
872                    }
873
874                    if self.lfn_builder.building {
875                        self.lfn_builder.add_entry(
876                            seq,
877                            lfn.checksum,
878                            &lfn.name1,
879                            &lfn.name2,
880                            &lfn.name3,
881                        );
882                    }
883                    continue;
884                }
885            }
886
887            // This is a regular file/directory entry
888            let file_entry = unsafe { raw_entry.file };
889
890            let attr = DirEntryAttrFlags::from_bits_retain(file_entry.attributes);
891            // Skip any entry carrying VOLUME_ID: both plain label entries and
892            // corrupt combinations (e.g. VOLUME_ID|DIRECTORY), which the FAT
893            // spec does not define as listable entries. LFN components
894            // (exactly LONG_NAME) were already handled above.
895            if attr.contains(DirEntryAttrFlags::VOLUME_ID) {
896                #[cfg(feature = "lfn")]
897                self.lfn_builder.reset();
898                continue;
899            }
900
901            // Convert 0x05 back to 0xE5 for kanji compatibility
902            let mut name_bytes = file_entry.name;
903            if name_bytes[0] == 0x05 {
904                name_bytes[0] = 0xE5;
905            }
906
907            let short_name = match ShortFileName::new(name_bytes) {
908                Ok(n) => n,
909                Err(_) => return Some(Err(Error::InvalidShortFilename)),
910            };
911
912            // Try to get the LFN if we've been building one
913            #[cfg(feature = "lfn")]
914            let long_name = self.lfn_builder.finish(&short_name);
915
916            // For FAT12/16 with fixed root dir, parent_clus is 0 (sentinel)
917            // For cluster-based dirs, parent_clus is the actual cluster
918            let created = FatDateTime::from_raw(
919                u16::from_le_bytes(file_entry.creation_date),
920                u16::from_le_bytes(file_entry.creation_time),
921                file_entry.creation_time_tenth,
922            );
923            let modified = FatDateTime::from_raw(
924                u16::from_le_bytes(file_entry.last_write_date),
925                u16::from_le_bytes(file_entry.last_write_time),
926                0,
927            );
928            let last_access_date = u16::from_le_bytes(file_entry.last_access_date);
929
930            return Some(Ok(DirectoryEntry::Entry(FileEntry {
931                short_name,
932                nt_case: NtCaseFlags::from_bits_truncate(file_entry.reserved),
933                #[cfg(feature = "lfn")]
934                long_name,
935                attr,
936                size: file_entry.size.get() as usize,
937                #[cfg(feature = "write")]
938                parent_dir_clus: self.dir_start_cluster,
939                #[cfg(feature = "write")]
940                parent_clus: self.cluster,
941                #[cfg(feature = "write")]
942                offset_within_cluster: self.offset - entry_size,
943                cluster: Cluster::from_parts(
944                    file_entry.first_cluster_high.get(),
945                    file_entry.first_cluster_low.get(),
946                ),
947                created,
948                last_access_date,
949                modified,
950            })));
951        }
952    }
953}
954
955} // end sync_only!