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