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