Skip to main content

hadris_fat/
write.rs

1//! Write operations for FAT filesystems.
2
3io_transform! {
4
5#[cfg(feature = "write")]
6use core::ops::DerefMut;
7
8#[cfg(feature = "write")]
9use crate::{
10    raw::{DirEntryAttrFlags, RawDirectoryEntry, RawFileEntry},
11    error::{Error, Result},
12    file::ShortFileName,
13};
14#[cfg(feature = "write")]
15use super::{
16    fat_table::Fat, dir::{FatDir, FileEntry}, fs::FatVolume,
17    io::{Cluster, ClusterLike, Read, ReadExt, Seek, SeekFrom, Write},
18};
19
20#[cfg(feature = "write")]
21use hadris_common::types::endian::{Endian, LittleEndian};
22
23/// A writer for file content in a FAT filesystem.
24#[cfg(feature = "write")]
25pub struct FileWriter<'a, DATA: Read + Write + Seek> {
26    fs: &'a FatVolume<DATA>,
27    /// First cluster of the file (None if empty file)
28    first_cluster: Option<Cluster<usize>>,
29    /// Current cluster being written to
30    current_cluster: Option<Cluster<usize>>,
31    /// Offset within the current cluster
32    offset_in_cluster: usize,
33    /// Total bytes written so far
34    total_written: usize,
35    /// Parent directory cluster (0 for fixed root directory)
36    entry_parent: Cluster<usize>,
37    /// Offset of the directory entry within the parent
38    entry_offset: usize,
39    /// Fixed root directory info (for FAT12/16)
40    fixed_root: Option<(usize, usize)>,
41    /// Override for the modified timestamp written by `finish()`. When `None`,
42    /// the configured `TimeProvider` supplies "now".
43    pending_modified: Option<crate::time::FatDateTime>,
44    /// Override for the access date written by `finish()`. FAT stores no
45    /// access time, only a date — hence `u16` rather than `FatDateTime`.
46    pending_accessed: Option<u16>,
47    /// Override for the creation timestamp written by `finish()`.
48    pending_created: Option<crate::time::FatDateTime>,
49    /// Set to `true` only inside `finish()` once the on-disk entry has been
50    /// updated. Drives the `dirty-file-panic` Drop check; when the feature
51    /// is off, drop is a no-op regardless of this field.
52    finished: bool,
53}
54
55#[cfg(feature = "write")]
56impl<'a, DATA: Read + Write + Seek> Drop for FileWriter<'a, DATA> {
57    fn drop(&mut self) {
58        // Without `dirty-file-panic`, drop is a no-op: callers that forget
59        // `finish()` silently lose the size/timestamp commit (the data
60        // bytes themselves are already on disk because `write()` flushes
61        // immediately). With the feature, we panic loudly so the bug is
62        // caught in dev rather than in production.
63        #[cfg(feature = "dirty-file-panic")]
64        if !self.finished {
65            panic!(
66                "FileWriter dropped without calling finish() — \
67                 directory entry size/timestamps are NOT committed. \
68                 Disable the `dirty-file-panic` feature if this is intended."
69            );
70        }
71    }
72}
73
74#[cfg(feature = "write")]
75impl<'a, DATA: Read + Write + Seek> FileWriter<'a, DATA> {
76    /// Create a new FileWriter for a file entry.
77    ///
78    /// The entry must be a file (not a directory).
79    pub fn new(fs: &'a FatVolume<DATA>, entry: &FileEntry) -> Result<Self> {
80        if entry.is_directory() {
81            return Err(Error::NotAFile);
82        }
83
84        let first_cluster = if entry.cluster().0 >= 2 {
85            Some(entry.cluster())
86        } else {
87            None
88        };
89
90        // Get fixed root info if the parent is the root directory (cluster 0)
91        // and this is a FAT12/16 filesystem
92        let fixed_root = if entry.parent_clus.0 == 0 {
93            fs.fixed_root_dir_info()
94        } else {
95            None
96        };
97
98        Ok(Self {
99            fs,
100            first_cluster,
101            current_cluster: first_cluster,
102            offset_in_cluster: 0,
103            total_written: 0,
104            entry_parent: entry.parent_clus,
105            entry_offset: entry.offset_within_cluster,
106            fixed_root,
107            pending_modified: None,
108            pending_accessed: None,
109            pending_created: None,
110            finished: false,
111        })
112    }
113
114    /// Create a FileWriter positioned at the end of the file for appending.
115    ///
116    /// Walks the FAT chain to find the last cluster and positions the
117    /// writer at the file's current end. Subsequent writes append data
118    /// and `finish()` updates the size to include both existing and new data.
119    pub async fn new_append(fs: &'a FatVolume<DATA>, entry: &FileEntry) -> Result<Self> {
120        if entry.is_directory() {
121            return Err(Error::NotAFile);
122        }
123
124        let fixed_root = if entry.parent_clus.0 == 0 {
125            fs.fixed_root_dir_info()
126        } else {
127            None
128        };
129
130        let file_size = entry.len() as usize;
131        let first_cluster = if entry.cluster().0 >= 2 {
132            Some(entry.cluster())
133        } else {
134            None
135        };
136
137        if file_size == 0 || first_cluster.is_none() {
138            // Empty file — same as a regular new writer
139            return Ok(Self {
140                fs,
141                first_cluster,
142                current_cluster: first_cluster,
143                offset_in_cluster: 0,
144                total_written: 0,
145                entry_parent: entry.parent_clus,
146                entry_offset: entry.offset_within_cluster,
147                fixed_root,
148                pending_modified: None,
149                pending_accessed: None,
150                pending_created: None,
151                finished: false,
152            });
153        }
154
155        let cluster_size = {
156            let data = fs.data.lock();
157            data.cluster_size
158        };
159
160        // Walk the FAT chain to find the last cluster. Bounded by
161        // `max_cluster` so a corrupt looping chain cannot hang the writer.
162        let max_steps = fs.fat.max_cluster();
163        let last = {
164            let mut data = fs.data.lock();
165            fs.fat
166                .walk_chain(data.deref_mut(), first_cluster.unwrap().0 as u32, max_steps, |_| {})
167                .await?
168        };
169        let current = Cluster(last as usize);
170
171        let offset_in_last = file_size % cluster_size;
172
173        Ok(Self {
174            fs,
175            first_cluster,
176            current_cluster: Some(current),
177            offset_in_cluster: offset_in_last,
178            total_written: file_size,
179            entry_parent: entry.parent_clus,
180            entry_offset: entry.offset_within_cluster,
181            fixed_root,
182            pending_modified: None,
183            pending_accessed: None,
184            pending_created: None,
185            finished: false,
186        })
187    }
188
189    /// Write data to the file.
190    ///
191    /// Allocates new clusters as needed.
192    pub async fn write(&mut self, buf: &[u8]) -> Result<usize> {
193        if buf.is_empty() {
194            return Ok(0);
195        }
196
197        let cluster_size = self.fs.info.cluster_size;
198        let mut written = 0;
199
200        while written < buf.len() {
201            // Check if we need a new cluster
202            if self.current_cluster.is_none() || self.offset_in_cluster >= cluster_size {
203                // Allocate via the routed helper so the FAT cache (when
204                // installed) sees the mutation. The helper acquires both
205                // cache+data locks internally in canonical order.
206                let hint = self.current_cluster.map(|c| c.0 as u32 + 1).unwrap_or(2);
207                let new_cluster = self.fs.allocate_cluster_routed(hint).await?;
208
209                // Update FSInfo tracking (FAT32 only)
210                self.fs.decrement_free_count();
211                self.fs.update_next_free_hint(new_cluster);
212
213                // Link previous cluster to the new one (also routed).
214                if let Some(prev) = self.current_cluster {
215                    self.fs.write_clus_routed(prev.0, new_cluster).await?;
216                }
217
218                // Update first cluster if this is the first allocation
219                if self.first_cluster.is_none() {
220                    self.first_cluster = Some(Cluster(new_cluster as usize));
221                }
222
223                self.current_cluster = Some(Cluster(new_cluster as usize));
224                self.offset_in_cluster = 0;
225            }
226
227            let cluster = self.current_cluster.unwrap();
228            let bytes_left_in_cluster = cluster_size - self.offset_in_cluster;
229            let to_write = (buf.len() - written).min(bytes_left_in_cluster);
230
231            // Lock data only for the payload write.
232            {
233                let mut data = self.fs.data.lock();
234                let seek_pos = cluster.to_bytes(self.fs.info.data_start, cluster_size)
235                    + self.offset_in_cluster;
236                data.seek(SeekFrom::Start(seek_pos as u64)).await?;
237                data.write_all(&buf[written..written + to_write]).await?;
238            }
239
240            self.offset_in_cluster += to_write;
241            self.total_written += to_write;
242            written += to_write;
243        }
244
245        Ok(written)
246    }
247
248    /// Get the total number of bytes written.
249    pub fn bytes_written(&self) -> usize {
250        self.total_written
251    }
252
253    /// Override the modified timestamp written by [`finish`](Self::finish).
254    ///
255    /// Without this call, `finish()` stamps "now" via the configured
256    /// [`TimeProvider`](crate::time::TimeProvider). Useful for preserving the
257    /// original mtime when copying files between volumes, or for
258    /// reproducible-image builds.
259    pub fn set_modified(&mut self, dt: crate::time::FatDateTime) -> &mut Self {
260        self.pending_modified = Some(dt);
261        self
262    }
263
264    /// Override the access date written by [`finish`](Self::finish).
265    ///
266    /// FAT does not store an access *time* — only a date. Pass the raw
267    /// FAT-encoded date `(year-1980)<<9 | month<<5 | day`.
268    pub fn set_accessed(&mut self, date: u16) -> &mut Self {
269        self.pending_accessed = Some(date);
270        self
271    }
272
273    /// Override the creation timestamp written by [`finish`](Self::finish).
274    ///
275    /// Most filesystems write creation time only at file-create. This setter
276    /// lets writers retroactively patch it, useful when re-imaging or
277    /// migrating data with timestamps from another source.
278    pub fn set_created(&mut self, dt: crate::time::FatDateTime) -> &mut Self {
279        self.pending_created = Some(dt);
280        self
281    }
282
283    /// Finish writing and update the directory entry with the new size.
284    ///
285    /// This must be called after writing to persist the file size. On FAT32 it
286    /// also flushes the FSInfo sector so its `free_count` matches the FAT —
287    /// without this, `fsck.fat` rejects the image after writes.
288    ///
289    /// With the `dirty-file-panic` feature enabled, dropping the writer
290    /// without calling `finish` panics — the most common cause of "the file
291    /// I just wrote shows up as zero bytes" bugs.
292    pub async fn finish(mut self) -> Result<()> {
293        {
294            let mut data = self.fs.data.lock();
295            let cluster_size = data.cluster_size;
296
297            // Calculate entry position - handle fixed root directory
298            let entry_pos = if self.entry_parent.0 == 0 {
299                // Fixed root directory (FAT12/16)
300                let (root_start, _) = self.fixed_root.expect("Fixed root info required");
301                root_start + self.entry_offset
302            } else {
303                // Cluster-based directory
304                self.entry_parent
305                    .to_bytes(self.fs.info.data_start, cluster_size)
306                    + self.entry_offset
307            };
308
309            // Read the current directory entry
310            data.seek(SeekFrom::Start(entry_pos as u64)).await?;
311
312            let mut raw_entry = data.read_struct::<RawDirectoryEntry>().await?;
313            let file_entry = unsafe { &mut raw_entry.file };
314
315            // Update size
316            file_entry.size =
317                hadris_common::types::number::U32::<LittleEndian>::new(self.total_written as u32);
318
319            // Update first cluster - for FAT12/16, only use low 16 bits
320            if let Some(cluster) = self.first_cluster {
321                let (high, low) = match &self.fs.fat {
322                    Fat::Fat12(_) | Fat::Fat16(_) => (0u16, cluster.0 as u16),
323                    Fat::Fat32(_) => ((cluster.0 >> 16) as u16, cluster.0 as u16),
324                };
325                file_entry.first_cluster_high =
326                    hadris_common::types::number::U16::<LittleEndian>::new(high);
327                file_entry.first_cluster_low =
328                    hadris_common::types::number::U16::<LittleEndian>::new(low);
329            } else {
330                file_entry.first_cluster_high =
331                    hadris_common::types::number::U16::<LittleEndian>::new(0);
332                file_entry.first_cluster_low =
333                    hadris_common::types::number::U16::<LittleEndian>::new(0);
334            }
335
336            // Update timestamps. Overrides win over the configured clock so
337            // callers can preserve original times when copying or rebuilding.
338            let modified = self.pending_modified.unwrap_or_else(|| self.fs.time_provider().now());
339            file_entry.last_write_date = modified.date.to_le_bytes();
340            file_entry.last_write_time = modified.time.to_le_bytes();
341            file_entry.last_access_date = self
342                .pending_accessed
343                .unwrap_or(modified.date)
344                .to_le_bytes();
345            if let Some(created) = self.pending_created {
346                file_entry.creation_date = created.date.to_le_bytes();
347                file_entry.creation_time = created.time.to_le_bytes();
348                file_entry.creation_time_tenth = created.time_tenth;
349            }
350
351            // Write back the entry
352            data.seek(SeekFrom::Start(entry_pos as u64)).await?;
353            data.write_all(bytemuck::bytes_of(&raw_entry)).await?;
354            data.flush().await?;
355        }
356
357        // Flush FSInfo so on-disk free_count matches in-memory state
358        // (no-op for FAT12/16). The lock above must be released first because
359        // write_fsinfo also acquires it.
360        self.fs.write_fsinfo().await?;
361
362        // Mark as cleanly finished so the Drop guard (under
363        // `dirty-file-panic`) accepts the consume.
364        self.finished = true;
365
366        Ok(())
367    }
368}
369
370/// Extension trait for FatVolume to write files.
371#[cfg(feature = "write")]
372pub trait FatVolumeWriteExt<DATA: Read + Write + Seek> {
373    /// Create a writer for a file entry.
374    fn write_file<'a>(&'a self, entry: &FileEntry) -> Result<FileWriter<'a, DATA>>;
375
376    /// Truncate a file to the specified size.
377    ///
378    /// If `new_size` is greater than or equal to the current file size, this method
379    /// does nothing. Otherwise, it frees any clusters that are no longer needed
380    /// and updates the directory entry with the new size.
381    ///
382    /// # Errors
383    ///
384    /// Returns [`Error::NotAFile`] if the entry is a directory.
385    async fn truncate(&self, entry: &FileEntry, new_size: usize) -> Result<()>;
386
387    /// Patch the timestamps on an existing entry without rewriting its data.
388    ///
389    /// Each parameter is `Option`: `None` keeps the on-disk value untouched.
390    /// `accessed_date` is the raw FAT-encoded date (FAT does not store an
391    /// access *time*).
392    ///
393    /// Useful when copying files between volumes or rebuilding a reproducible
394    /// image — every other write path stamps "now" via the configured
395    /// [`TimeProvider`](crate::time::TimeProvider), which is the wrong
396    /// behaviour for those workflows.
397    async fn set_times(
398        &self,
399        entry: &FileEntry,
400        modified: Option<crate::time::FatDateTime>,
401        accessed_date: Option<u16>,
402        created: Option<crate::time::FatDateTime>,
403    ) -> Result<()>;
404}
405
406#[cfg(feature = "write")]
407impl<DATA: Read + Write + Seek> FatVolumeWriteExt<DATA> for FatVolume<DATA> {
408    fn write_file<'a>(&'a self, entry: &FileEntry) -> Result<FileWriter<'a, DATA>> {
409        FileWriter::new(self, entry)
410    }
411
412    async fn truncate(&self, entry: &FileEntry, new_size: usize) -> Result<()> {
413        if !entry.is_file() {
414            return Err(Error::NotAFile);
415        }
416
417        let current_size = entry.len() as usize;
418        if new_size >= current_size {
419            return Ok(()); // Nothing to do
420        }
421
422        let first_cluster = entry.cluster();
423        let cluster_size = self.info.cluster_size;
424
425        // Get fixed root info if the parent is in the fixed root directory
426        let fixed_root = if entry.parent_clus.0 == 0 {
427            self.fixed_root_dir_info()
428        } else {
429            None
430        };
431
432        if new_size == 0 {
433            // Free entire chain — routed through cache when installed.
434            let freed_count = if first_cluster.0 >= 2 {
435                self.free_chain_routed(first_cluster.0 as u32).await?
436            } else {
437                0
438            };
439            // Update FSInfo tracking (FAT32 only)
440            self.increment_free_count(freed_count);
441            // Update directory entry: size=0, first_cluster=0
442            self.update_entry_size_and_cluster(entry, 0, Cluster(0), fixed_root).await?;
443        } else {
444            // Calculate which cluster to keep
445            let clusters_needed = new_size.div_ceil(cluster_size);
446
447            // Walk chain to find the last cluster to keep. The hop count is
448            // bounded both by the file size and by `max_cluster` so a
449            // looping chain on corrupt media surfaces as ClusterLoop.
450            let max_cluster = self.fat.max_cluster();
451            let hops = ((clusters_needed.saturating_sub(1)) as u32).min(max_cluster);
452            let current = {
453                let mut steps_remaining = hops;
454                let mut cur = first_cluster.0 as u32;
455                while steps_remaining > 0 {
456                    match self.next_cluster_routed(cur as usize).await? {
457                        Some(next) => cur = next,
458                        None => break,
459                    }
460                    steps_remaining -= 1;
461                }
462                Cluster(cur as usize)
463            };
464
465            // Truncate after this cluster — routed.
466            let freed_count = self.truncate_chain_routed(current.0 as u32).await?;
467            // Update FSInfo tracking (FAT32 only)
468            self.increment_free_count(freed_count);
469
470            // Update directory entry with new size (keep first_cluster)
471            self.update_entry_size_and_cluster(entry, new_size, first_cluster, fixed_root).await?;
472        }
473
474        // Flush FSInfo so on-disk free_count matches in-memory state (FAT32).
475        self.write_fsinfo().await?;
476
477        Ok(())
478    }
479
480    async fn set_times(
481        &self,
482        entry: &FileEntry,
483        modified: Option<crate::time::FatDateTime>,
484        accessed_date: Option<u16>,
485        created: Option<crate::time::FatDateTime>,
486    ) -> Result<()> {
487        if modified.is_none() && accessed_date.is_none() && created.is_none() {
488            return Ok(());
489        }
490
491        let mut data = self.data.lock();
492        let cluster_size = data.cluster_size;
493
494        let entry_pos = if entry.parent_clus.0 == 0 {
495            let (root_start, _) = self
496                .fixed_root_dir_info()
497                .expect("Fixed root info required for cluster 0");
498            root_start + entry.offset_within_cluster
499        } else {
500            entry
501                .parent_clus
502                .to_bytes(self.info.data_start, cluster_size)
503                + entry.offset_within_cluster
504        };
505
506        data.seek(SeekFrom::Start(entry_pos as u64)).await?;
507        let mut raw_entry = data.read_struct::<RawDirectoryEntry>().await?;
508        let file_entry = unsafe { &mut raw_entry.file };
509
510        if let Some(m) = modified {
511            file_entry.last_write_date = m.date.to_le_bytes();
512            file_entry.last_write_time = m.time.to_le_bytes();
513        }
514        if let Some(date) = accessed_date {
515            file_entry.last_access_date = date.to_le_bytes();
516        }
517        if let Some(c) = created {
518            file_entry.creation_date = c.date.to_le_bytes();
519            file_entry.creation_time = c.time.to_le_bytes();
520            file_entry.creation_time_tenth = c.time_tenth;
521        }
522
523        data.seek(SeekFrom::Start(entry_pos as u64)).await?;
524        data.write_all(bytemuck::bytes_of(&raw_entry)).await?;
525        data.flush().await?;
526
527        Ok(())
528    }
529}
530
531/// Convert 0xE5 to 0x05 in the first byte of a short name for kanji compatibility.
532///
533/// The FAT spec uses 0xE5 as a deleted-entry marker, so actual filenames starting
534/// with byte 0xE5 (valid kanji lead byte) must be stored as 0x05. The read path
535/// converts 0x05 back to 0xE5.
536#[cfg(feature = "write")]
537fn kanji_short_name_fixup(name: &mut [u8; 11]) {
538    if name[0] == 0xE5 {
539        name[0] = 0x05;
540    }
541}
542
543/// Maximum number of LFN entries the spec allows: 20 entries × 13 UTF-16 code
544/// units per entry = 260 char "ceiling", though the spec caps the encoded
545/// name itself at 255 code units.
546#[cfg(feature = "write")]
547pub(crate) const MAX_LFN_ENTRIES: usize = 20;
548
549/// Decide whether `name` can be stored as a single short (8.3) directory entry
550/// using the Windows NT `DIR_NTRes` case flags, and if so which flags to set.
551///
552/// Returns `Some(bits)` when the name fits 8.3 with at most a per-part *uniform*
553/// case difference — `bits` carries `LOWER_BASE` (0x08) and/or `LOWER_EXT`
554/// (0x10) so a lowercase name round-trips without a long-file-name entry. An
555/// already-uppercase 8.3 name returns `Some(0)`. Returns `None` when the name
556/// needs LFN entries to round-trip: too long, spaces, multiple dots, mixed case
557/// within the base or extension, or characters not representable in the 8.3
558/// character set.
559///
560/// This replaces the older "does this need an LFN?" predicate — a `None` result
561/// is exactly the set of names that previously required LFN entries.
562#[cfg(feature = "write")]
563fn short_name_case_bits(name: &str) -> Option<u8> {
564    const LOWER_BASE: u8 = 0x08;
565    const LOWER_EXT: u8 = 0x10;
566
567    let (base, ext) = match name.rfind('.') {
568        Some(pos) if pos > 0 => (&name[..pos], &name[pos + 1..]),
569        _ => (name, ""),
570    };
571    if base.is_empty() || base.chars().count() > 8 || ext.chars().count() > 3 {
572        return None;
573    }
574    if name.matches('.').count() > 1 {
575        return None;
576    }
577
578    // Returns `Some(true)` for an all-lowercase part, `Some(false)` for an
579    // all-uppercase (or caseless) part, and `None` when the part is not 8.3
580    // representable (invalid character or mixed case).
581    fn part_is_lower(part: &str) -> Option<bool> {
582        let mut seen_lower = false;
583        let mut seen_upper = false;
584        for c in part.chars() {
585            if !c.is_ascii() {
586                return None;
587            }
588            let upper = (c as u8).to_ascii_uppercase();
589            let representable = upper.is_ascii_uppercase()
590                || upper.is_ascii_digit()
591                || ShortFileName::ALLOWED_SYMBOLS.contains(&upper);
592            if !representable {
593                return None;
594            }
595            if c.is_ascii_lowercase() {
596                seen_lower = true;
597            } else if c.is_ascii_uppercase() {
598                seen_upper = true;
599            }
600        }
601        if seen_lower && seen_upper {
602            return None;
603        }
604        Some(seen_lower)
605    }
606
607    let mut bits = 0;
608    if part_is_lower(base)? {
609        bits |= LOWER_BASE;
610    }
611    if part_is_lower(ext)? {
612        bits |= LOWER_EXT;
613    }
614    Some(bits)
615}
616
617/// Maximum number of LFN entries we'll walk backward when cleaning up
618/// orphaned long-name slots on delete/rename. The FAT spec caps at 20
619/// entries per name; bounding the scan defends against corrupt directory
620/// contents that would otherwise spoof an unbounded LFN run.
621#[cfg(feature = "write")]
622const LFN_CLEANUP_SCAN_LIMIT: usize = 20;
623
624#[cfg(feature = "write")]
625#[derive(Clone, Copy, Debug, PartialEq, Eq)]
626struct DirectoryEntryPosition {
627    cluster: Cluster<usize>,
628    offset: usize,
629}
630
631#[cfg(feature = "write")]
632const MAX_DIRECTORY_ENTRY_RUN: usize = MAX_LFN_ENTRIES + 1;
633
634#[cfg(feature = "write")]
635#[derive(Clone, Copy, Debug)]
636struct DirectoryEntryRun {
637    positions: [DirectoryEntryPosition; MAX_DIRECTORY_ENTRY_RUN],
638    len: usize,
639}
640
641#[cfg(feature = "write")]
642impl DirectoryEntryRun {
643    fn new() -> Self {
644        Self {
645            positions: [DirectoryEntryPosition {
646                cluster: Cluster(0),
647                offset: 0,
648            }; MAX_DIRECTORY_ENTRY_RUN],
649            len: 0,
650        }
651    }
652
653    fn push(&mut self, position: DirectoryEntryPosition) {
654        debug_assert!(self.len < self.positions.len());
655        self.positions[self.len] = position;
656        self.len += 1;
657    }
658
659    fn clear(&mut self) {
660        self.len = 0;
661    }
662
663    fn get(&self, index: usize) -> DirectoryEntryPosition {
664        debug_assert!(index < self.len);
665        self.positions[index]
666    }
667
668    fn last(&self) -> DirectoryEntryPosition {
669        self.get(self.len - 1)
670    }
671}
672
673/// Encode `name` (UTF-8) into UTF-16LE LFN entries, written into `out` in
674/// disk order. Returns the number of LFN entries produced (excluding the
675/// short entry).
676///
677/// Disk layout placed into `out`:
678///   `out[0]`               = sequence N | LAST_ENTRY_MASK (highest seq)
679///   `out[1..n]`            = sequences N-1, N-2, ..., 1
680///   (caller writes the short entry into `out[n]`)
681///
682/// Returns `None` if the name exceeds 255 UTF-16 code units (FAT spec cap).
683#[cfg(all(feature = "write", feature = "lfn"))]
684fn build_lfn_entries(
685    name: &str,
686    short_checksum: u8,
687    out: &mut [RawDirectoryEntry],
688) -> Option<usize> {
689    use crate::raw::RawLfnEntry;
690
691    // Worst-case staging buffer: 20 LFN entries × 13 UTF-16 units = 260.
692    // Sized larger than the spec cap (255) so we always have room for the
693    // 0x0000 terminator + 0xFFFF filler when a 255-unit name doesn't
694    // perfectly fill the last entry. A 255-unit buffer (the previous size)
695    // would index out of bounds at exactly the spec cap.
696    const STAGING_CAP: usize = MAX_LFN_ENTRIES * crate::file::LongFileName::CHARS_PER_ENTRY;
697    let mut u16_buf = [0u16; STAGING_CAP];
698    let mut u16_len = 0usize;
699    for ch in name.chars() {
700        let mut tmp = [0u16; 2];
701        for &c in ch.encode_utf16(&mut tmp).iter() {
702            // Cap the *encoded* length at 255 (FAT spec) — anything longer
703            // surfaces as `None` so the caller can return `InvalidFilename`.
704            if u16_len >= crate::file::LFN_MAX_UTF16_UNITS {
705                return None;
706            }
707            u16_buf[u16_len] = c;
708            u16_len += 1;
709        }
710    }
711
712    let chars_per_entry = crate::file::LongFileName::CHARS_PER_ENTRY;
713    let num_lfn = u16_len.div_ceil(chars_per_entry);
714    if num_lfn == 0 || num_lfn > MAX_LFN_ENTRIES || out.len() < num_lfn {
715        return None;
716    }
717
718    // Pad the unused tail of the last entry: 0x0000 terminator immediately
719    // after the last real char, then 0xFFFF for any remaining slots — that's
720    // what the spec expects from a writer. Skip when the name perfectly
721    // fills the last entry (terminator omitted in that case per spec).
722    let total_capacity = num_lfn * chars_per_entry;
723    if u16_len < total_capacity {
724        u16_buf[u16_len] = 0x0000;
725        for slot in &mut u16_buf[u16_len + 1..total_capacity] {
726            *slot = 0xFFFF;
727        }
728    }
729
730    // LFN entries on disk are stored in reverse: the first entry encountered
731    // by a reader has the highest sequence number (with `LAST_ENTRY_MASK`)
732    // and contains the *last* segment of the name. Walk from highest seq
733    // down to 1, slotting them into out[0..num_lfn].
734    for (entry_idx, out_entry) in out.iter_mut().enumerate().take(num_lfn) {
735        let seq_num = (num_lfn - entry_idx) as u8;
736        let seq_byte = if entry_idx == 0 {
737            seq_num | crate::file::LfnBuilder::LAST_ENTRY_MASK
738        } else {
739            seq_num
740        };
741
742        let chunk_start = (seq_num as usize - 1) * chars_per_entry;
743        let chunk = &u16_buf[chunk_start..chunk_start + chars_per_entry];
744
745        let mut name1 = [0u8; 10];
746        let mut name2 = [0u8; 12];
747        let mut name3 = [0u8; 4];
748        for i in 0..5 {
749            let bytes = chunk[i].to_le_bytes();
750            name1[i * 2] = bytes[0];
751            name1[i * 2 + 1] = bytes[1];
752        }
753        for i in 0..6 {
754            let bytes = chunk[5 + i].to_le_bytes();
755            name2[i * 2] = bytes[0];
756            name2[i * 2 + 1] = bytes[1];
757        }
758        for i in 0..2 {
759            let bytes = chunk[11 + i].to_le_bytes();
760            name3[i * 2] = bytes[0];
761            name3[i * 2 + 1] = bytes[1];
762        }
763
764        let lfn = RawLfnEntry {
765            sequence_number: seq_byte,
766            name1,
767            attributes: DirEntryAttrFlags::LONG_NAME.bits(),
768            ty: 0,
769            checksum: short_checksum,
770            name2,
771            first_cluster_low: [0, 0],
772            name3,
773        };
774        *out_entry = RawDirectoryEntry { lfn };
775    }
776
777    Some(num_lfn)
778}
779
780/// Directory write operations
781#[cfg(feature = "write")]
782impl<DATA: Read + Write + Seek> FatVolume<DATA> {
783    async fn mark_entry_span_deleted(&self, entry: &FileEntry) -> Result<()> {
784        let entry_size = core::mem::size_of::<RawDirectoryEntry>();
785        let cluster_size = self.info.cluster_size;
786        let target = DirectoryEntryPosition {
787            cluster: entry.parent_clus,
788            offset: entry.offset_within_cluster,
789        };
790        let mut pending = DirectoryEntryRun::new();
791
792        if entry.parent_dir_clus.0 == 0 {
793            let (root_start, root_size) = self
794                .fixed_root_dir_info()
795                .expect("Fixed root info required for cluster 0");
796            let max_entries = root_size / entry_size;
797            let mut data = self.data.lock();
798            for i in 0..max_entries {
799                let position = DirectoryEntryPosition {
800                    cluster: Cluster(0),
801                    offset: i * entry_size,
802                };
803                if position == target {
804                    for index in 0..pending.len {
805                        data.seek(SeekFrom::Start(
806                            (root_start + pending.get(index).offset) as u64,
807                        ))
808                        .await?;
809                        data.write_all(&[0xE5]).await?;
810                    }
811                    data.seek(SeekFrom::Start((root_start + position.offset) as u64))
812                        .await?;
813                    data.write_all(&[0xE5]).await?;
814                    return Ok(());
815                }
816
817                data.seek(SeekFrom::Start((root_start + position.offset) as u64))
818                    .await?;
819                let raw = data.read_struct::<RawDirectoryEntry>().await?;
820                let bytes = unsafe { raw.bytes };
821                if bytes[0] == 0x00 {
822                    break;
823                }
824                if bytes[0] != 0xE5
825                    && unsafe { raw.file }.attributes == DirEntryAttrFlags::LONG_NAME.bits()
826                {
827                    if pending.len == LFN_CLEANUP_SCAN_LIMIT {
828                        for index in 1..pending.len {
829                            pending.positions[index - 1] = pending.positions[index];
830                        }
831                        pending.len -= 1;
832                    }
833                    pending.push(position);
834                } else {
835                    pending.clear();
836                }
837            }
838            return Err(Error::EntryNotFound);
839        }
840
841        let mut current = entry.parent_dir_clus;
842        let mut steps = 0u32;
843        loop {
844            steps = steps.saturating_add(1);
845            if steps > self.fat.max_cluster() {
846                return Err(Error::ClusterLoop {
847                    cluster: current.0 as u32,
848                });
849            }
850
851            {
852                let mut data = self.data.lock();
853                for offset in (0..cluster_size).step_by(entry_size) {
854                    let position = DirectoryEntryPosition {
855                        cluster: current,
856                        offset,
857                    };
858                    let seek_pos =
859                        current.to_bytes(self.info.data_start, cluster_size) + offset;
860                    if position == target {
861                        for index in 0..pending.len {
862                            let previous = pending.get(index);
863                            let previous_pos = previous
864                                .cluster
865                                .to_bytes(self.info.data_start, cluster_size)
866                                + previous.offset;
867                            data.seek(SeekFrom::Start(previous_pos as u64)).await?;
868                            data.write_all(&[0xE5]).await?;
869                        }
870                        data.seek(SeekFrom::Start(seek_pos as u64)).await?;
871                        data.write_all(&[0xE5]).await?;
872                        return Ok(());
873                    }
874
875                    data.seek(SeekFrom::Start(seek_pos as u64)).await?;
876                    let raw = data.read_struct::<RawDirectoryEntry>().await?;
877                    let bytes = unsafe { raw.bytes };
878                    if bytes[0] == 0x00 {
879                        return Err(Error::EntryNotFound);
880                    }
881                    if bytes[0] != 0xE5
882                        && unsafe { raw.file }.attributes == DirEntryAttrFlags::LONG_NAME.bits()
883                    {
884                        if pending.len == LFN_CLEANUP_SCAN_LIMIT {
885                            for index in 1..pending.len {
886                                pending.positions[index - 1] = pending.positions[index];
887                            }
888                            pending.len -= 1;
889                        }
890                        pending.push(position);
891                    } else {
892                        pending.clear();
893                    }
894                }
895            }
896
897            match self.next_cluster_routed(current.0).await? {
898                Some(next) => current = Cluster(next as usize),
899                None => return Err(Error::EntryNotFound),
900            }
901        }
902    }
903
904    /// Find `count` consecutive free entry slots in a directory, allocating
905    /// new directory clusters if needed.
906    ///
907    /// The returned positions are in logical directory order and may cross
908    /// cluster boundaries.
909    async fn find_free_entry_run_in_dir(
910        &self,
911        dir: &FatDir<'_, DATA>,
912        count: usize,
913    ) -> Result<DirectoryEntryRun> {
914        debug_assert!((1..=MAX_DIRECTORY_ENTRY_RUN).contains(&count));
915        if let Some((root_start, root_size)) = dir.fixed_root {
916            self.find_free_entry_run_in_fixed_root(root_start, root_size, count)
917                .await
918        } else {
919            self.find_free_entry_run_in_cluster_chain(dir.cluster, count)
920                .await
921        }
922    }
923
924    /// Find `count` consecutive free entries in a fixed root directory.
925    ///
926    /// Returns DirectoryFull if no such run exists.
927    async fn find_free_entry_run_in_fixed_root(
928        &self,
929        root_start: usize,
930        root_size: usize,
931        count: usize,
932    ) -> Result<DirectoryEntryRun> {
933        let mut data = self.data.lock();
934        let entry_size = core::mem::size_of::<RawDirectoryEntry>();
935        let max_entries = root_size / entry_size;
936        let mut run = DirectoryEntryRun::new();
937        let mut end_seen = false;
938
939        for i in 0..max_entries {
940            let offset = i * entry_size;
941            let free = if end_seen {
942                true
943            } else {
944                data.seek(SeekFrom::Start((root_start + offset) as u64))
945                    .await?;
946                let raw_entry = data.read_struct::<RawDirectoryEntry>().await?;
947                let first_byte = unsafe { raw_entry.bytes[0] };
948                if first_byte == 0x00 {
949                    end_seen = true;
950                    true
951                } else {
952                    first_byte == 0xE5
953                }
954            };
955
956            if free {
957                run.push(DirectoryEntryPosition {
958                    cluster: Cluster(0),
959                    offset,
960                });
961                if run.len == count {
962                    return Ok(run);
963                }
964            } else {
965                run.clear();
966            }
967        }
968
969        Err(Error::DirectoryFull)
970    }
971
972    /// Find `count` consecutive free entries starting at the given cluster
973    /// chain. Allocates a new cluster (extending the chain) when the existing
974    /// space is exhausted. Free runs continue across cluster boundaries.
975    async fn find_free_entry_run_in_cluster_chain(
976        &self,
977        dir_cluster: Cluster<usize>,
978        count: usize,
979    ) -> Result<DirectoryEntryRun> {
980        let cluster_size = self.info.cluster_size;
981        let entry_size = core::mem::size_of::<RawDirectoryEntry>();
982        let entries_per_cluster = cluster_size / entry_size;
983        let mut current_cluster = dir_cluster;
984        let mut run = DirectoryEntryRun::new();
985        let mut end_seen = false;
986        // Bound the chain walk so a corrupt directory chain cannot loop
987        // forever. Anything past `max_cluster` clusters has to revisit one.
988        let chain_limit = self.fat.max_cluster();
989        let mut steps: u32 = 0;
990
991        loop {
992            steps = steps.saturating_add(1);
993            if steps > chain_limit {
994                return Err(Error::ClusterLoop {
995                    cluster: current_cluster.0 as u32,
996                });
997            }
998            {
999                let mut data = self.data.lock();
1000                for i in 0..entries_per_cluster {
1001                    let offset = i * entry_size;
1002                    let free = if end_seen {
1003                        true
1004                    } else {
1005                        let seek_pos =
1006                            current_cluster.to_bytes(self.info.data_start, cluster_size) + offset;
1007                        data.seek(SeekFrom::Start(seek_pos as u64)).await?;
1008                        let raw_entry = data.read_struct::<RawDirectoryEntry>().await?;
1009                        let first_byte = unsafe { raw_entry.bytes[0] };
1010                        if first_byte == 0x00 {
1011                            end_seen = true;
1012                            true
1013                        } else {
1014                            first_byte == 0xE5
1015                        }
1016                    };
1017
1018                    if free {
1019                        run.push(DirectoryEntryPosition {
1020                            cluster: current_cluster,
1021                            offset,
1022                        });
1023                        if run.len == count {
1024                            return Ok(run);
1025                        }
1026                    } else {
1027                        run.clear();
1028                    }
1029                }
1030            }
1031
1032            // Try to get next cluster (cache-routed).
1033            let next = self.next_cluster_routed(current_cluster.0).await?;
1034            match next {
1035                Some(cluster) => {
1036                    current_cluster = Cluster(cluster as usize);
1037                }
1038                None => {
1039                    // No more clusters: allocate a fresh one and link it in.
1040                    let hint = current_cluster.0 as u32 + 1;
1041                    let new_cluster = self.allocate_cluster_routed(hint).await?;
1042                    let new_cluster_pos = Cluster(new_cluster as usize)
1043                        .to_bytes(self.info.data_start, cluster_size);
1044                    let zero_result = {
1045                        let mut data = self.data.lock();
1046                        data.seek(SeekFrom::Start(new_cluster_pos as u64)).await?;
1047                        let zeros = alloc::vec![0u8; cluster_size];
1048                        data.write_all(&zeros).await
1049                    };
1050                    if let Err(error) = zero_result {
1051                        let _ = self.free_chain_routed(new_cluster).await;
1052                        return Err(error.into());
1053                    }
1054                    if let Err(error) = self
1055                        .write_clus_routed(current_cluster.0, new_cluster)
1056                        .await
1057                    {
1058                        let _ = self.free_chain_routed(new_cluster).await;
1059                        return Err(error);
1060                    }
1061
1062                    self.decrement_free_count();
1063                    self.update_next_free_hint(new_cluster);
1064                    current_cluster = Cluster(new_cluster as usize);
1065                    end_seen = true;
1066                }
1067            }
1068        }
1069    }
1070
1071    /// Write a raw directory entry at the specified location.
1072    ///
1073    /// For fixed root directory entries (cluster == 0), uses the fixed root offset.
1074    async fn write_raw_entry(
1075        &self,
1076        cluster: Cluster<usize>,
1077        offset: usize,
1078        entry: &RawFileEntry,
1079        fixed_root: Option<(usize, usize)>,
1080    ) -> Result<()> {
1081        let mut data = self.data.lock();
1082        let cluster_size = data.cluster_size;
1083
1084        // Calculate seek position
1085        let seek_pos = if cluster.0 == 0 {
1086            // Fixed root directory (FAT12/16)
1087            let (root_start, _) = fixed_root.expect("Fixed root info required for cluster 0");
1088            root_start + offset
1089        } else {
1090            // Cluster-based directory
1091            cluster.to_bytes(self.info.data_start, cluster_size) + offset
1092        };
1093
1094        data.seek(SeekFrom::Start(seek_pos as u64)).await?;
1095        data.write_all(bytemuck::bytes_of(entry)).await?;
1096        Ok(())
1097    }
1098
1099    /// Write a raw `RawDirectoryEntry` (which may carry an LFN payload via
1100    /// the union variant). Identical seek logic to `write_raw_entry`; the
1101    /// split exists only because the short-entry caller already passes a
1102    /// `RawFileEntry`.
1103    #[cfg(feature = "lfn")]
1104    async fn write_raw_directory_entry(
1105        &self,
1106        cluster: Cluster<usize>,
1107        offset: usize,
1108        entry: &RawDirectoryEntry,
1109        fixed_root: Option<(usize, usize)>,
1110    ) -> Result<()> {
1111        let mut data = self.data.lock();
1112        let cluster_size = data.cluster_size;
1113        let seek_pos = if cluster.0 == 0 {
1114            let (root_start, _) = fixed_root.expect("Fixed root info required for cluster 0");
1115            root_start + offset
1116        } else {
1117            cluster.to_bytes(self.info.data_start, cluster_size) + offset
1118        };
1119        data.seek(SeekFrom::Start(seek_pos as u64)).await?;
1120        // Safety: the union has a `bytes` variant guaranteed to be 32 bytes,
1121        // and the caller has already populated the entry through a typed
1122        // write. `bytemuck::bytes_of` is safe because RawDirectoryEntry is
1123        // Pod (NoUninit + AnyBitPattern declared in raw.rs).
1124        data.write_all(bytemuck::bytes_of(entry)).await?;
1125        Ok(())
1126    }
1127
1128    /// Create a new file in the given directory.
1129    ///
1130    /// Returns the FileEntry for the newly created file.
1131    pub async fn create_file(&self, parent: &FatDir<'_, DATA>, name: &str) -> Result<FileEntry> {
1132        // Check if entry already exists
1133        if parent.find(name).await?.is_some() {
1134            return Err(Error::AlreadyExists);
1135        }
1136
1137        // Generate short filename (suffix=0 means no ~N suffix)
1138        let short_name = ShortFileName::from_long_name_with(name, 0, self.oem_converter())
1139            .map_err(|_| Error::InvalidFilename)?;
1140
1141        // A name that fits 8.3 apart from per-part case is stored as a single
1142        // short entry with the NT case byte set; otherwise it needs LFN entries.
1143        // When the lfn feature is off, we never emit LFN.
1144        let case_bits = short_name_case_bits(name);
1145        #[cfg(feature = "lfn")]
1146        let mut lfn_buf: [RawDirectoryEntry; MAX_LFN_ENTRIES] = unsafe { core::mem::zeroed() };
1147        #[cfg(feature = "lfn")]
1148        let (lfn_count, nt_res) = match case_bits {
1149            Some(bits) => (0usize, bits),
1150            None => (
1151                build_lfn_entries(name, short_name.lfn_checksum(), &mut lfn_buf)
1152                    .ok_or(Error::InvalidFilename)?,
1153                0u8,
1154            ),
1155        };
1156        #[cfg(not(feature = "lfn"))]
1157        let (lfn_count, nt_res) = (0usize, case_bits.unwrap_or(0));
1158
1159        // Find a free run sized for the LFN preamble + the short entry.
1160        let total_slots = lfn_count + 1;
1161        let run = self.find_free_entry_run_in_dir(parent, total_slots).await?;
1162
1163        // Write LFN entries first (in disk order), then the short entry.
1164        let now = self.time_provider().now();
1165        let (date, time, time_tenth) = now.to_raw();
1166
1167        let mut raw_name = short_name.to_raw_bytes();
1168        kanji_short_name_fixup(&mut raw_name);
1169
1170        let entry = RawFileEntry {
1171            name: raw_name,
1172            attributes: DirEntryAttrFlags::ARCHIVE.bits(),
1173            reserved: nt_res,
1174            creation_time_tenth: time_tenth,
1175            creation_time: time.to_le_bytes(),
1176            creation_date: date.to_le_bytes(),
1177            last_access_date: date.to_le_bytes(),
1178            first_cluster_high: hadris_common::types::number::U16::<LittleEndian>::new(0),
1179            last_write_time: time.to_le_bytes(),
1180            last_write_date: date.to_le_bytes(),
1181            first_cluster_low: hadris_common::types::number::U16::<LittleEndian>::new(0),
1182            size: hadris_common::types::number::U32::<LittleEndian>::new(0),
1183        };
1184
1185        #[cfg(feature = "lfn")]
1186        for (i, lfn_entry) in lfn_buf.iter().enumerate().take(lfn_count) {
1187            let position = run.get(i);
1188            self.write_raw_directory_entry(
1189                position.cluster,
1190                position.offset,
1191                lfn_entry,
1192                parent.fixed_root,
1193            )
1194                .await?;
1195        }
1196        // Short entry sits at the end of the run.
1197        let short_position = run.last();
1198        self.write_raw_entry(
1199            short_position.cluster,
1200            short_position.offset,
1201            &entry,
1202            parent.fixed_root,
1203        )
1204        .await?;
1205
1206        // Flush FSInfo so on-disk free_count matches in-memory state (FAT32).
1207        // find_free_entry_slot_in_dir may have extended the parent directory.
1208        self.write_fsinfo().await?;
1209
1210        Ok(FileEntry {
1211            short_name,
1212            nt_case: crate::raw::NtCaseFlags::from_bits_truncate(nt_res),
1213            #[cfg(feature = "lfn")]
1214            long_name: if lfn_count > 0 {
1215                crate::file::LongFileName::from_str_utf16(name)
1216            } else {
1217                None
1218            },
1219            attr: DirEntryAttrFlags::ARCHIVE,
1220            size: 0,
1221            parent_dir_clus: parent.cluster,
1222            parent_clus: short_position.cluster,
1223            offset_within_cluster: short_position.offset,
1224            cluster: Cluster(0),
1225            created: now,
1226            last_access_date: now.date,
1227            modified: crate::time::FatDateTime::from_raw(now.date, now.time, 0),
1228        })
1229    }
1230
1231    /// Create a new directory.
1232    ///
1233    /// Returns a FatDir handle for the newly created directory.
1234    pub async fn create_dir<'a>(
1235        &'a self,
1236        parent: &FatDir<'a, DATA>,
1237        name: &str,
1238    ) -> Result<FatDir<'a, DATA>> {
1239        // Check if entry already exists
1240        if parent.find(name).await?.is_some() {
1241            return Err(Error::AlreadyExists);
1242        }
1243
1244        // Generate short filename (suffix=0 means no ~N suffix)
1245        let short_name = ShortFileName::from_long_name_with(name, 0, self.oem_converter())
1246            .map_err(|_| Error::InvalidFilename)?;
1247
1248        // Allocate a cluster for the directory contents (cache-routed).
1249        let new_cluster = self.allocate_cluster_routed(2).await?;
1250
1251        // Update FSInfo tracking (FAT32 only)
1252        self.decrement_free_count();
1253        self.update_next_free_hint(new_cluster);
1254
1255        // A name that fits 8.3 apart from per-part case is stored as a single
1256        // short entry with the NT case byte set; otherwise it needs LFN entries.
1257        // When the lfn feature is off, we never emit LFN.
1258        let case_bits = short_name_case_bits(name);
1259        #[cfg(feature = "lfn")]
1260        let mut lfn_buf: [RawDirectoryEntry; MAX_LFN_ENTRIES] = unsafe { core::mem::zeroed() };
1261        #[cfg(feature = "lfn")]
1262        let (lfn_count, nt_res) = match case_bits {
1263            Some(bits) => (0usize, bits),
1264            None => (
1265                build_lfn_entries(name, short_name.lfn_checksum(), &mut lfn_buf)
1266                    .ok_or(Error::InvalidFilename)?,
1267                0u8,
1268            ),
1269        };
1270        #[cfg(not(feature = "lfn"))]
1271        let (lfn_count, nt_res) = (0usize, case_bits.unwrap_or(0));
1272
1273        // Allocate `lfn_count + 1` consecutive slots.
1274        let total_slots = lfn_count + 1;
1275        let run = self.find_free_entry_run_in_dir(parent, total_slots).await?;
1276
1277        // Create the directory entry in parent
1278        let now = self.time_provider().now();
1279        let (date, time, time_tenth) = now.to_raw();
1280
1281        // For FAT12/16, only use the low 16 bits of the cluster number
1282        let (cluster_high, cluster_low) = match &self.fat {
1283            Fat::Fat12(_) | Fat::Fat16(_) => (0u16, new_cluster as u16),
1284            Fat::Fat32(_) => ((new_cluster >> 16) as u16, new_cluster as u16),
1285        };
1286
1287        let mut raw_name = short_name.to_raw_bytes();
1288        kanji_short_name_fixup(&mut raw_name);
1289
1290        let entry = RawFileEntry {
1291            name: raw_name,
1292            attributes: DirEntryAttrFlags::DIRECTORY.bits(),
1293            reserved: nt_res,
1294            creation_time_tenth: time_tenth,
1295            creation_time: time.to_le_bytes(),
1296            creation_date: date.to_le_bytes(),
1297            last_access_date: date.to_le_bytes(),
1298            first_cluster_high: hadris_common::types::number::U16::<LittleEndian>::new(
1299                cluster_high,
1300            ),
1301            last_write_time: time.to_le_bytes(),
1302            last_write_date: date.to_le_bytes(),
1303            first_cluster_low: hadris_common::types::number::U16::<LittleEndian>::new(cluster_low),
1304            size: hadris_common::types::number::U32::<LittleEndian>::new(0),
1305        };
1306
1307        #[cfg(feature = "lfn")]
1308        for (i, lfn_entry) in lfn_buf.iter().enumerate().take(lfn_count) {
1309            let position = run.get(i);
1310            self.write_raw_directory_entry(
1311                position.cluster,
1312                position.offset,
1313                lfn_entry,
1314                parent.fixed_root,
1315            )
1316                .await?;
1317        }
1318        let short_position = run.last();
1319        let (slot_cluster, slot_offset) = (short_position.cluster, short_position.offset);
1320        self.write_raw_entry(slot_cluster, slot_offset, &entry, parent.fixed_root).await?;
1321
1322        // Initialize the new directory with . and .. entries
1323        {
1324            let mut data = self.data.lock();
1325            let cluster_size = data.cluster_size;
1326            let dir_pos =
1327                Cluster(new_cluster as usize).to_bytes(self.info.data_start, cluster_size);
1328
1329            // Zero out the cluster first
1330            data.seek(SeekFrom::Start(dir_pos as u64)).await?;
1331            let zeros = alloc::vec![0u8; cluster_size];
1332            data.write_all(&zeros).await?;
1333
1334            // Write "." entry (points to self)
1335            let dot_entry = RawFileEntry {
1336                name: *b".          ",
1337                attributes: DirEntryAttrFlags::DIRECTORY.bits(),
1338                reserved: 0,
1339                creation_time_tenth: time_tenth,
1340                creation_time: time.to_le_bytes(),
1341                creation_date: date.to_le_bytes(),
1342                last_access_date: date.to_le_bytes(),
1343                first_cluster_high: hadris_common::types::number::U16::<LittleEndian>::new(
1344                    cluster_high,
1345                ),
1346                last_write_time: time.to_le_bytes(),
1347                last_write_date: date.to_le_bytes(),
1348                first_cluster_low: hadris_common::types::number::U16::<LittleEndian>::new(
1349                    cluster_low,
1350                ),
1351                size: hadris_common::types::number::U32::<LittleEndian>::new(0),
1352            };
1353            data.seek(SeekFrom::Start(dir_pos as u64)).await?;
1354            data.write_all(bytemuck::bytes_of(&dot_entry)).await?;
1355
1356            // Write ".." entry (points to parent).
1357            // FAT12/16 root has cluster 0 already, so it stores 0.
1358            // FAT32 spec: when the parent is the FAT32 root, ".." must store
1359            // cluster 0 even though the root has a real cluster — fsck.fat
1360            // rejects images that use the actual root cluster here.
1361            let parent_cluster = parent.cluster.0 as u32;
1362            let dotdot_cluster = if self.is_fat32_root_cluster(parent_cluster) {
1363                0
1364            } else {
1365                parent_cluster
1366            };
1367            let (parent_high, parent_low) = match &self.fat {
1368                Fat::Fat12(_) | Fat::Fat16(_) => (0u16, dotdot_cluster as u16),
1369                Fat::Fat32(_) => ((dotdot_cluster >> 16) as u16, dotdot_cluster as u16),
1370            };
1371
1372            let dotdot_entry = RawFileEntry {
1373                name: *b"..         ",
1374                attributes: DirEntryAttrFlags::DIRECTORY.bits(),
1375                reserved: 0,
1376                creation_time_tenth: time_tenth,
1377                creation_time: time.to_le_bytes(),
1378                creation_date: date.to_le_bytes(),
1379                last_access_date: date.to_le_bytes(),
1380                first_cluster_high: hadris_common::types::number::U16::<LittleEndian>::new(
1381                    parent_high,
1382                ),
1383                last_write_time: time.to_le_bytes(),
1384                last_write_date: date.to_le_bytes(),
1385                first_cluster_low: hadris_common::types::number::U16::<LittleEndian>::new(
1386                    parent_low,
1387                ),
1388                size: hadris_common::types::number::U32::<LittleEndian>::new(0),
1389            };
1390            let dotdot_pos = dir_pos + core::mem::size_of::<RawDirectoryEntry>();
1391            data.seek(SeekFrom::Start(dotdot_pos as u64)).await?;
1392            data.write_all(bytemuck::bytes_of(&dotdot_entry)).await?;
1393        }
1394
1395        // Flush FSInfo so on-disk free_count matches in-memory state (FAT32).
1396        self.write_fsinfo().await?;
1397
1398        Ok(FatDir {
1399            data: self,
1400            cluster: Cluster(new_cluster as usize),
1401            fixed_root: None, // Newly created directories are never fixed root
1402        })
1403    }
1404
1405    /// Delete a file or empty directory.
1406    pub async fn delete(&self, entry: &FileEntry) -> Result<()> {
1407        // If it's a directory, check if it's empty (only . and ..)
1408        if entry.is_directory() {
1409            let dir = FatDir {
1410                data: self,
1411                cluster: entry.cluster(),
1412                fixed_root: None, // User-created directories are never fixed root
1413            };
1414
1415            let mut count = 0;
1416            let mut iter = dir.entries();
1417            while let Some(item) = iter.next_entry().await {
1418                let item = item?;
1419                let name = item.name();
1420                if name != "." && name != ".." {
1421                    count += 1;
1422                }
1423            }
1424
1425            if count > 0 {
1426                return Err(Error::DirectoryNotEmpty);
1427            }
1428        }
1429
1430        // Free the cluster chain if there is one (cache-routed).
1431        if entry.cluster().0 >= 2 {
1432            let freed_count = self.free_chain_routed(entry.cluster().0 as u32).await?;
1433            // Update FSInfo tracking (FAT32 only)
1434            self.increment_free_count(freed_count);
1435        }
1436
1437        // Mark the directory entry as deleted, plus any LFN slots that
1438        // precede it. Without the LFN cleanup, a delete would leave
1439        // orphaned LFN slots on disk — fsck.fat flags those as "stray
1440        // long-name slots" and they'd confuse a fresh-mount lookup until
1441        // the slots are eventually overwritten.
1442        self.mark_entry_span_deleted(entry).await?;
1443
1444        // Flush FSInfo so on-disk free_count matches in-memory state (FAT32).
1445        self.write_fsinfo().await?;
1446
1447        Ok(())
1448    }
1449
1450    /// Rename or move a file or directory.
1451    ///
1452    /// Creates a new directory entry with `new_name` in `dest_dir`, copying
1453    /// the cluster chain, size, and attributes from the source entry, then
1454    /// marks the old entry as deleted. Data is NOT copied — only the
1455    /// directory entry metadata changes.
1456    ///
1457    /// If moving a directory to a different parent, the `..` entry is updated
1458    /// to point to the new parent.
1459    pub async fn rename(
1460        &self,
1461        entry: &FileEntry,
1462        dest_dir: &FatDir<'_, DATA>,
1463        new_name: &str,
1464    ) -> Result<FileEntry> {
1465        // Check if destination already has this name
1466        if dest_dir.find(new_name).await?.is_some() {
1467            return Err(Error::AlreadyExists);
1468        }
1469
1470        // Generate short filename
1471        let short_name = ShortFileName::from_long_name_with(new_name, 0, self.oem_converter())
1472            .map_err(|_| Error::InvalidFilename)?;
1473
1474        // The new name is stored as a single short entry (with NT case bits)
1475        // when it fits 8.3 apart from case, otherwise via LFN entries. When the
1476        // lfn feature is off, we never emit LFN.
1477        let case_bits = short_name_case_bits(new_name);
1478        #[cfg(feature = "lfn")]
1479        let mut lfn_buf: [RawDirectoryEntry; MAX_LFN_ENTRIES] = unsafe { core::mem::zeroed() };
1480        #[cfg(feature = "lfn")]
1481        let (lfn_count, nt_res) = match case_bits {
1482            Some(bits) => (0usize, bits),
1483            None => (
1484                build_lfn_entries(new_name, short_name.lfn_checksum(), &mut lfn_buf)
1485                    .ok_or(Error::InvalidFilename)?,
1486                0u8,
1487            ),
1488        };
1489        #[cfg(not(feature = "lfn"))]
1490        let (lfn_count, nt_res) = (0usize, case_bits.unwrap_or(0));
1491
1492        // Find a contiguous run sized for LFN entries plus the short entry.
1493        let total_slots = lfn_count + 1;
1494        let run = self
1495            .find_free_entry_run_in_dir(dest_dir, total_slots)
1496            .await?;
1497        #[cfg(feature = "lfn")]
1498        for (i, lfn_entry) in lfn_buf.iter().enumerate().take(lfn_count) {
1499            let position = run.get(i);
1500            self.write_raw_directory_entry(
1501                position.cluster,
1502                position.offset,
1503                lfn_entry,
1504                dest_dir.fixed_root,
1505            )
1506            .await?;
1507        }
1508        let short_position = run.last();
1509        let slot_cluster = short_position.cluster;
1510        let slot_offset = short_position.offset;
1511
1512        // Read the original raw entry to preserve all fields
1513        let original_raw = {
1514            let mut data = self.data.lock();
1515            let cluster_size = data.cluster_size;
1516
1517            let entry_pos = if entry.parent_clus.0 == 0 {
1518                let (root_start, _) = self
1519                    .fixed_root_dir_info()
1520                    .expect("Fixed root info required for cluster 0");
1521                root_start + entry.offset_within_cluster
1522            } else {
1523                entry
1524                    .parent_clus
1525                    .to_bytes(self.info.data_start, cluster_size)
1526                    + entry.offset_within_cluster
1527            };
1528
1529            data.seek(SeekFrom::Start(entry_pos as u64)).await?;
1530            data.read_struct::<RawDirectoryEntry>().await?
1531        };
1532
1533        // Build the new entry with the new name but same cluster/size/attributes
1534        let original_file = unsafe { &original_raw.file };
1535        let mut raw_name = short_name.to_raw_bytes();
1536        kanji_short_name_fixup(&mut raw_name);
1537
1538        let now = self.time_provider().now();
1539        let new_entry = RawFileEntry {
1540            name: raw_name,
1541            // Case bits follow the new name, not the original entry's.
1542            reserved: nt_res,
1543            attributes: original_file.attributes,
1544            creation_time_tenth: original_file.creation_time_tenth,
1545            creation_time: original_file.creation_time,
1546            creation_date: original_file.creation_date,
1547            last_access_date: now.date.to_le_bytes(),
1548            first_cluster_high: original_file.first_cluster_high,
1549            last_write_time: now.time.to_le_bytes(),
1550            last_write_date: now.date.to_le_bytes(),
1551            first_cluster_low: original_file.first_cluster_low,
1552            size: original_file.size,
1553        };
1554
1555        // Write the new entry
1556        self.write_raw_entry(slot_cluster, slot_offset, &new_entry, dest_dir.fixed_root)
1557            .await?;
1558
1559        // If moving a directory to a different parent, update the ".." entry
1560        if entry.is_directory()
1561            && entry.cluster().0 >= 2
1562            && dest_dir.cluster != entry.parent_dir_clus
1563        {
1564            let mut data = self.data.lock();
1565            let cluster_size = data.cluster_size;
1566            let dir_data_start =
1567                entry.cluster().to_bytes(self.info.data_start, cluster_size);
1568            // ".." is the second entry (32 bytes after ".")
1569            let dotdot_pos = dir_data_start + core::mem::size_of::<RawDirectoryEntry>();
1570            data.seek(SeekFrom::Start(dotdot_pos as u64)).await?;
1571            let mut dotdot = data.read_struct::<RawDirectoryEntry>().await?;
1572            let dotdot_file = unsafe { &mut dotdot.file };
1573
1574            // FAT32 spec: when the new parent is the FAT32 root, ".." stores
1575            // cluster 0 even though the root has a real cluster.
1576            let parent_cluster = dest_dir.cluster.0 as u32;
1577            let dotdot_cluster = if self.is_fat32_root_cluster(parent_cluster) {
1578                0
1579            } else {
1580                parent_cluster
1581            };
1582            let (parent_high, parent_low) = match &self.fat {
1583                Fat::Fat12(_) | Fat::Fat16(_) => (0u16, dotdot_cluster as u16),
1584                Fat::Fat32(_) => ((dotdot_cluster >> 16) as u16, dotdot_cluster as u16),
1585            };
1586            dotdot_file.first_cluster_high =
1587                hadris_common::types::number::U16::<LittleEndian>::new(parent_high);
1588            dotdot_file.first_cluster_low =
1589                hadris_common::types::number::U16::<LittleEndian>::new(parent_low);
1590
1591            data.seek(SeekFrom::Start(dotdot_pos as u64)).await?;
1592            data.write_all(bytemuck::bytes_of(&dotdot)).await?;
1593        }
1594
1595        // Mark the old entry deleted, including any preceding LFN slots so
1596        // we don't leave orphaned long-name entries behind.
1597        self.mark_entry_span_deleted(entry).await?;
1598
1599        // Flush FSInfo so on-disk free_count matches in-memory state (FAT32).
1600        // find_free_entry_slot_in_dir on dest_dir may have extended it.
1601        self.write_fsinfo().await?;
1602
1603        Ok(FileEntry {
1604            short_name,
1605            nt_case: crate::raw::NtCaseFlags::from_bits_truncate(nt_res),
1606            #[cfg(feature = "lfn")]
1607            long_name: if lfn_count > 0 {
1608                crate::file::LongFileName::from_str_utf16(new_name)
1609            } else {
1610                None
1611            },
1612            attr: DirEntryAttrFlags::from_bits_retain(original_file.attributes),
1613            size: original_file.size.get() as usize,
1614            parent_dir_clus: dest_dir.cluster,
1615            parent_clus: slot_cluster,
1616            offset_within_cluster: slot_offset,
1617            cluster: entry.cluster(),
1618            // Preserve original creation time; bump access/modified to "now".
1619            created: crate::time::FatDateTime::from_raw(
1620                u16::from_le_bytes(original_file.creation_date),
1621                u16::from_le_bytes(original_file.creation_time),
1622                original_file.creation_time_tenth,
1623            ),
1624            last_access_date: now.date,
1625            modified: crate::time::FatDateTime::from_raw(now.date, now.time, 0),
1626        })
1627    }
1628
1629    /// Update a directory entry's size and first cluster fields.
1630    ///
1631    /// This is used by truncate and other operations that need to modify these fields.
1632    async fn update_entry_size_and_cluster(
1633        &self,
1634        entry: &FileEntry,
1635        new_size: usize,
1636        first_cluster: Cluster<usize>,
1637        fixed_root: Option<(usize, usize)>,
1638    ) -> Result<()> {
1639        use super::fat_table::Fat;
1640
1641        let mut data = self.data.lock();
1642        let cluster_size = data.cluster_size;
1643
1644        // Calculate entry position - handle fixed root directory
1645        let entry_pos = if entry.parent_clus.0 == 0 {
1646            // Fixed root directory (FAT12/16)
1647            let (root_start, _) = fixed_root.expect("Fixed root info required for cluster 0");
1648            root_start + entry.offset_within_cluster
1649        } else {
1650            // Cluster-based directory
1651            entry
1652                .parent_clus
1653                .to_bytes(self.info.data_start, cluster_size)
1654                + entry.offset_within_cluster
1655        };
1656
1657        // Read the current directory entry
1658        data.seek(SeekFrom::Start(entry_pos as u64)).await?;
1659
1660        let mut raw_entry = data.read_struct::<RawDirectoryEntry>().await?;
1661        let file_entry = unsafe { &mut raw_entry.file };
1662
1663        // Update size
1664        file_entry.size = hadris_common::types::number::U32::<LittleEndian>::new(new_size as u32);
1665
1666        // Update first cluster
1667        let (high, low) = if first_cluster.0 >= 2 {
1668            match &self.fat {
1669                Fat::Fat12(_) | Fat::Fat16(_) => (0u16, first_cluster.0 as u16),
1670                Fat::Fat32(_) => ((first_cluster.0 >> 16) as u16, first_cluster.0 as u16),
1671            }
1672        } else {
1673            (0u16, 0u16)
1674        };
1675        file_entry.first_cluster_high =
1676            hadris_common::types::number::U16::<LittleEndian>::new(high);
1677        file_entry.first_cluster_low = hadris_common::types::number::U16::<LittleEndian>::new(low);
1678
1679        // Update modification time
1680        let now = self.time_provider().now();
1681        file_entry.last_write_date = now.date.to_le_bytes();
1682        file_entry.last_write_time = now.time.to_le_bytes();
1683        file_entry.last_access_date = now.date.to_le_bytes();
1684
1685        // Write back the entry
1686        data.seek(SeekFrom::Start(entry_pos as u64)).await?;
1687        data.write_all(bytemuck::bytes_of(&raw_entry)).await?;
1688
1689        Ok(())
1690    }
1691}
1692
1693/// Volume label modification (root directory entry).
1694#[cfg(feature = "write")]
1695impl<DATA: Read + Write + Seek> FatVolume<DATA> {
1696    /// Overwrite the volume label stored in the root-directory entry.
1697    ///
1698    /// Returns [`Error::EntryNotFound`] if no label entry exists today —
1699    /// callers should format the volume with a label, or extend the API
1700    /// later to allocate a new entry. The 11-byte name is written verbatim
1701    /// (FAT spec: space-padded, conventionally uppercase ASCII).
1702    ///
1703    /// This does **not** update the BPB volume label; reformatting is the
1704    /// only way to change that one without rewriting the boot sector.
1705    pub async fn set_root_label(&self, name: &[u8; 11]) -> Result<()> {
1706        let (pos, raw) = self
1707            .find_root_label_entry()
1708            .await?
1709            .ok_or(Error::EntryNotFound)?;
1710        let mut updated = raw;
1711        // Writing to a union field of `Copy` type without `Drop` is safe in
1712        // modern Rust — the existing memory is overwritten verbatim.
1713        updated.file.name = *name;
1714
1715        let mut data = self.data.lock();
1716        data.seek(SeekFrom::Start(pos as u64)).await?;
1717        data.write_all(bytemuck::bytes_of(&updated)).await?;
1718        data.flush().await?;
1719        Ok(())
1720    }
1721}
1722
1723/// File attribute modification
1724#[cfg(feature = "write")]
1725impl<DATA: Read + Write + Seek> FatVolume<DATA> {
1726    /// Set the attributes of a file or directory entry.
1727    ///
1728    /// Only the user-mutable bits (`READ_ONLY`, `HIDDEN`, `SYSTEM`, `ARCHIVE`)
1729    /// may be changed in place. Attempting to flip `DIRECTORY` or `VOLUME_ID`
1730    /// returns [`Error::InvalidAttributeChange`] — those bits identify the
1731    /// kind of entry on disk and changing them would orphan a cluster chain
1732    /// or break the root volume label.
1733    pub async fn set_attributes(
1734        &self,
1735        entry: &FileEntry,
1736        attrs: DirEntryAttrFlags,
1737    ) -> Result<()> {
1738        // Reject flips on the immutable bits before touching disk.
1739        let current = entry.attributes();
1740        let immutable = DirEntryAttrFlags::DIRECTORY | DirEntryAttrFlags::VOLUME_ID;
1741        let changed = (current ^ attrs) & immutable;
1742        if changed.contains(DirEntryAttrFlags::DIRECTORY) {
1743            return Err(Error::InvalidAttributeChange { bit: "DIRECTORY" });
1744        }
1745        if changed.contains(DirEntryAttrFlags::VOLUME_ID) {
1746            return Err(Error::InvalidAttributeChange { bit: "VOLUME_ID" });
1747        }
1748
1749        let mut data = self.data.lock();
1750        let cluster_size = data.cluster_size;
1751
1752        let entry_pos = if entry.parent_clus.0 == 0 {
1753            let (root_start, _) = self
1754                .fixed_root_dir_info()
1755                .expect("Fixed root info required for cluster 0");
1756            root_start + entry.offset_within_cluster
1757        } else {
1758            entry
1759                .parent_clus
1760                .to_bytes(self.info.data_start, cluster_size)
1761                + entry.offset_within_cluster
1762        };
1763
1764        // Read the current directory entry
1765        data.seek(SeekFrom::Start(entry_pos as u64)).await?;
1766        let mut raw_entry = data.read_struct::<RawDirectoryEntry>().await?;
1767        let file_entry = unsafe { &mut raw_entry.file };
1768
1769        // Update attributes
1770        file_entry.attributes = attrs.bits();
1771
1772        // Write back the entry
1773        data.seek(SeekFrom::Start(entry_pos as u64)).await?;
1774        data.write_all(bytemuck::bytes_of(&raw_entry)).await?;
1775
1776        Ok(())
1777    }
1778}
1779
1780/// FSInfo update operations
1781#[cfg(feature = "write")]
1782impl<DATA: Read + Write + Seek> FatVolume<DATA> {
1783    /// Synchronize the FSInfo sector to disk.
1784    ///
1785    /// For FAT32 filesystems, this updates the FSInfo sector with the current
1786    /// free cluster count and next free cluster hint. For FAT12/16 filesystems,
1787    /// this only flushes pending writes.
1788    pub async fn sync(&self) -> Result<()> {
1789        self.write_fsinfo().await?;
1790
1791        let mut data = self.data.lock();
1792        data.flush().await?;
1793        Ok(())
1794    }
1795
1796    /// Write the FSInfo sector to disk (FAT32 only).
1797    ///
1798    /// This updates the free cluster count and next free cluster hint in the
1799    /// FSInfo sector. For FAT12/16 filesystems, this is a no-op.
1800    async fn write_fsinfo(&self) -> Result<()> {
1801        use super::fs::FatFsExt;
1802        use crate::raw::RawFsInfo;
1803
1804        let ext = match &self.ext {
1805            FatFsExt::Fat32(ext) => ext,
1806            _ => return Ok(()), // No FSInfo for FAT12/16
1807        };
1808
1809        let mut data = self.data.lock();
1810
1811        // Seek to FSInfo sector
1812        data.seek_sector(ext.fs_info_sec).await?;
1813
1814        // Read current FSInfo to preserve other fields
1815        let mut fs_info = data.read_struct::<RawFsInfo>().await?;
1816
1817        // Update the mutable fields
1818        fs_info.free_count =
1819            hadris_common::types::number::U32::<LittleEndian>::new(ext.free_count.get());
1820        fs_info.next_free =
1821            hadris_common::types::number::U32::<LittleEndian>::new(ext.next_free.get().0);
1822
1823        // Write back
1824        data.seek_sector(ext.fs_info_sec).await?;
1825        data.write_all(bytemuck::bytes_of(&fs_info)).await?;
1826
1827        Ok(())
1828    }
1829
1830    /// Decrement the free cluster count (called after cluster allocation).
1831    ///
1832    /// This only affects FAT32 filesystems.
1833    pub(crate) fn decrement_free_count(&self) {
1834        use super::fs::FatFsExt;
1835
1836        if let FatFsExt::Fat32(ext) = &self.ext {
1837            let count = ext.free_count.get();
1838            if count > 0 && count != 0xFFFFFFFF {
1839                ext.free_count.set(count - 1);
1840            }
1841        }
1842    }
1843
1844    /// Increment the free cluster count (called after cluster free).
1845    ///
1846    /// This only affects FAT32 filesystems.
1847    pub(crate) fn increment_free_count(&self, amount: u32) {
1848        use super::fs::FatFsExt;
1849
1850        if let FatFsExt::Fat32(ext) = &self.ext {
1851            let count = ext.free_count.get();
1852            if count != 0xFFFFFFFF {
1853                ext.free_count.set(count.saturating_add(amount));
1854            }
1855        }
1856    }
1857
1858    /// Update the next free cluster hint (called after cluster allocation).
1859    ///
1860    /// This only affects FAT32 filesystems.
1861    pub(crate) fn update_next_free_hint(&self, cluster: u32) {
1862        use super::fs::FatFsExt;
1863
1864        if let FatFsExt::Fat32(ext) = &self.ext {
1865            // Set hint to the cluster after the one just allocated
1866            ext.next_free.set(Cluster(cluster.saturating_add(1)));
1867        }
1868    }
1869
1870    /// Get the current free cluster count (FAT32 only).
1871    ///
1872    /// Returns `None` for FAT12/16 filesystems or if the count is unknown (0xFFFFFFFF).
1873    pub fn free_cluster_count(&self) -> Option<u32> {
1874        use super::fs::FatFsExt;
1875
1876        match &self.ext {
1877            FatFsExt::Fat32(ext) => {
1878                let count = ext.free_count.get();
1879                if count != 0xFFFFFFFF {
1880                    Some(count)
1881                } else {
1882                    None
1883                }
1884            }
1885            _ => None,
1886        }
1887    }
1888
1889    /// Get the next free cluster hint (FAT32 only).
1890    ///
1891    /// Returns `None` for FAT12/16 filesystems or if the hint is unknown.
1892    pub fn next_free_cluster_hint(&self) -> Option<u32> {
1893        use super::fs::FatFsExt;
1894
1895        match &self.ext {
1896            FatFsExt::Fat32(ext) => {
1897                let hint = ext.next_free.get().0;
1898                if hint >= 2 && hint != 0xFFFFFFFF {
1899                    Some(hint)
1900                } else {
1901                    None
1902                }
1903            }
1904            _ => None,
1905        }
1906    }
1907}
1908
1909/// Miri-targeted unit tests for `build_lfn_entries`. These exercise the
1910/// `unsafe { lfn: ... }` union writes inside the staging buffer and the
1911/// 0xFFFF padding writes that previously OOB'd at the spec cap (255 UTF-16
1912/// units). Pure functions, no I/O — so miri's no-syscall sandbox runs them
1913/// at full speed.
1914///
1915/// Wired into CI via `.github/workflows/rust.yml` (the `miri` job).
1916#[cfg(all(test, feature = "write", feature = "lfn"))]
1917mod lfn_write_safety_tests {
1918    use super::{build_lfn_entries, MAX_LFN_ENTRIES};
1919    use crate::raw::{DirEntryAttrFlags, RawDirectoryEntry};
1920
1921    fn fresh_out() -> [RawDirectoryEntry; MAX_LFN_ENTRIES] {
1922        // SAFETY: zero-bytes is a valid bit pattern for every union variant
1923        // of RawDirectoryEntry — bytemuck::AnyBitPattern is impl'd on it.
1924        unsafe { core::mem::zeroed() }
1925    }
1926
1927    /// All bytes of every written LFN slot must be readable through the
1928    /// `bytes` union arm without UB. Before this commit, an exactly-255
1929    /// UTF-16 name OOB'd the staging buffer; this test pins that fix.
1930    #[test]
1931    fn build_lfn_entries_at_spec_cap_255_units_does_not_oob() {
1932        let name: alloc::string::String = core::iter::repeat_n('a', 255).collect();
1933        let mut out = fresh_out();
1934        let n = build_lfn_entries(&name, 0, &mut out).expect("must accept 255 chars");
1935        assert_eq!(n, 20);
1936        for entry in out.iter().take(n) {
1937            // Touch every byte through the bytes union arm — miri flags
1938            // any out-of-bounds reads or invalid bit patterns.
1939            let bytes = unsafe { entry.bytes };
1940            assert_eq!(bytes.len(), 32);
1941        }
1942    }
1943
1944    /// 256 UTF-16 units must surface as `None` (caller turns this into
1945    /// `InvalidFilename`) — silently truncating a filename is worse than
1946    /// refusing it.
1947    #[test]
1948    fn build_lfn_entries_overlong_returns_none() {
1949        let name: alloc::string::String = core::iter::repeat_n('a', 256).collect();
1950        let mut out = fresh_out();
1951        assert!(build_lfn_entries(&name, 0, &mut out).is_none());
1952    }
1953
1954    /// Supplementary-plane chars (e.g. U+1F31F 🌟) need surrogate pairs in
1955    /// UTF-16 — 2 code units per char. The staging path writes both halves;
1956    /// miri verifies the writes stay within `u16_buf`.
1957    #[test]
1958    fn build_lfn_entries_supplementary_plane_writes_both_surrogates() {
1959        // 100 emoji × 2 UTF-16 units each = 200 units, fits the spec cap.
1960        let name: alloc::string::String = core::iter::repeat_n('\u{1F31F}', 100).collect();
1961        let mut out = fresh_out();
1962        let n = build_lfn_entries(&name, 0, &mut out).expect("100 emoji fits");
1963        // 200 units / 13 chars per entry = 16 (15.38 rounded up).
1964        assert_eq!(n, 16);
1965        // Sanity: the first slot's name1 starts with 0xD83C 0xDF1F or the
1966        // appropriate surrogate pair. Don't depend on which slot maps where —
1967        // just confirm that some slot contains valid surrogate halves.
1968        let mut saw_high = false;
1969        let mut saw_low = false;
1970        for entry in out.iter().take(n) {
1971            let lfn = unsafe { entry.lfn };
1972            for chunk in lfn.name1.chunks_exact(2) {
1973                let unit = u16::from_le_bytes([chunk[0], chunk[1]]);
1974                if (0xD800..0xDC00).contains(&unit) {
1975                    saw_high = true;
1976                }
1977                if (0xDC00..0xE000).contains(&unit) {
1978                    saw_low = true;
1979                }
1980            }
1981        }
1982        assert!(saw_high && saw_low, "must encode both halves of the surrogate pair");
1983    }
1984
1985    /// Exact fill (length is a multiple of 13): the spec says no
1986    /// terminator/filler is written. Verify the last LFN entry's bytes are
1987    /// all real chars, not 0xFFFF or 0x0000.
1988    #[test]
1989    fn build_lfn_entries_exact_13_unit_multiple_no_padding() {
1990        let name: alloc::string::String = core::iter::repeat_n('a', 13).collect();
1991        let mut out = fresh_out();
1992        let n = build_lfn_entries(&name, 0, &mut out).expect("13 chars fits");
1993        assert_eq!(n, 1);
1994
1995        let lfn = unsafe { out[0].lfn };
1996        // All 13 units must be 'a' (0x0061). Walk name1 (5), name2 (6), name3 (2).
1997        for chunk in lfn
1998            .name1
1999            .chunks_exact(2)
2000            .chain(lfn.name2.chunks_exact(2))
2001            .chain(lfn.name3.chunks_exact(2))
2002        {
2003            let unit = u16::from_le_bytes([chunk[0], chunk[1]]);
2004            assert_eq!(unit, b'a' as u16, "exact-fill must contain only real chars");
2005        }
2006        // Sequence number has the LAST_ENTRY_MASK on the highest seq.
2007        assert_eq!(lfn.sequence_number, 0x41); // seq 1 + 0x40
2008        assert_eq!(lfn.attributes, DirEntryAttrFlags::LONG_NAME.bits());
2009    }
2010
2011    /// The first entry on disk (out[0]) carries the highest sequence number
2012    /// with `LAST_ENTRY_MASK` set. This invariant is what readers rely on
2013    /// to find the start of an LFN run.
2014    #[test]
2015    fn build_lfn_entries_first_slot_has_last_entry_mask() {
2016        let name = "longishname.tx"; // 14 chars, 2 LFN entries
2017        let mut out = fresh_out();
2018        let n = build_lfn_entries(name, 0xAB, &mut out).expect("ok");
2019        assert_eq!(n, 2);
2020        let first = unsafe { out[0].lfn };
2021        assert_eq!(first.sequence_number, 0x42); // seq 2 | 0x40
2022        assert_eq!(first.checksum, 0xAB);
2023        let second = unsafe { out[1].lfn };
2024        assert_eq!(second.sequence_number, 0x01); // no mask
2025        assert_eq!(second.checksum, 0xAB);
2026    }
2027}
2028
2029} // end io_transform!