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