Skip to main content

hadris_fat/
error.rs

1//! Error types for the hadris-fat crate.
2
3use core::fmt;
4
5/// Errors that can occur when working with FAT file systems.
6#[derive(Debug)]
7pub enum Error {
8    /// Invalid boot signature (expected 0xAA55)
9    InvalidBootSignature {
10        /// The signature that was found
11        found: u16,
12    },
13    /// Unsupported FAT type (FAT12/16 when FAT32 expected, or vice versa)
14    UnsupportedFatType(&'static str),
15    /// Invalid FSInfo signature
16    InvalidFsInfoSignature {
17        /// Which signature field failed validation
18        field: &'static str,
19        /// The expected signature value
20        expected: u32,
21        /// The actual signature value found
22        found: u32,
23    },
24    /// Invalid short filename (contains disallowed characters)
25    InvalidShortFilename,
26    /// Cluster number out of bounds
27    ClusterOutOfBounds {
28        /// The cluster number that was accessed
29        cluster: u32,
30        /// The maximum valid cluster number
31        max: u32,
32    },
33    /// Bad cluster marker encountered
34    BadCluster {
35        /// The cluster marked as bad
36        cluster: u32,
37    },
38    /// End of cluster chain reached unexpectedly
39    UnexpectedEndOfChain {
40        /// The last cluster in the chain
41        cluster: u32,
42    },
43    /// I/O error from the underlying storage
44    Io(hadris_io::Error),
45    /// I/O error annotated with the operation and (optionally) the sector
46    /// where it happened. Used at trust boundaries — boot sector parse,
47    /// FSInfo read, FAT chain walk entry, directory iteration head — so
48    /// callers can tell *which* on-disk structure tripped the failure
49    /// rather than just seeing an opaque [`Self::Io`].
50    IoContext {
51        /// Static label describing what was being read (e.g. `"boot sector"`).
52        op: &'static str,
53        /// Sector where the failure originated, when known.
54        sector: Option<u64>,
55        /// The wrapped underlying I/O error.
56        source: hadris_io::Error,
57    },
58    /// On-disk arithmetic on an untrusted value would overflow or underflow.
59    ///
60    /// The `context` string identifies which on-disk value triggered the
61    /// failure (e.g. `"sectors_per_fat * sector_size"` when a corrupt BPB
62    /// would push the FAT region past `usize::MAX`). This is distinct from
63    /// `ClusterOutOfBounds`, which catches *valid* arithmetic that lands
64    /// outside the cluster range.
65    CorruptFilesystem {
66        /// Static label describing which arithmetic check failed.
67        context: &'static str,
68    },
69    /// A FAT chain walk visited more clusters than the filesystem contains,
70    /// which means the chain has a loop. Always corruption — a healthy chain
71    /// is bounded by `max_cluster - 1` (clusters 0 and 1 are reserved).
72    ClusterLoop {
73        /// The cluster the walker was inspecting when it hit the limit.
74        cluster: u32,
75    },
76    /// File is not a regular file (e.g., is a directory)
77    NotAFile,
78    /// Entry is not a directory
79    NotADirectory,
80    /// Entry not found in directory
81    EntryNotFound,
82    /// Path is invalid (empty, malformed)
83    InvalidPath,
84    /// No free clusters available
85    #[cfg(feature = "write")]
86    NoFreeSpace,
87    /// Directory is full (no free entry slots)
88    #[cfg(feature = "write")]
89    DirectoryFull,
90    /// Filename is invalid or too long
91    #[cfg(feature = "write")]
92    InvalidFilename,
93    /// Entry with this name already exists
94    #[cfg(feature = "write")]
95    AlreadyExists,
96    /// Cannot delete non-empty directory
97    #[cfg(feature = "write")]
98    DirectoryNotEmpty,
99
100    /// Attempted to flip an immutable attribute bit (`DIRECTORY` or
101    /// `VOLUME_ID`) on an existing entry. Those bits identify the *kind* of
102    /// entry on disk; changing them in-place would leave the cluster chain
103    /// or root volume label inconsistent.
104    #[cfg(feature = "write")]
105    InvalidAttributeChange {
106        /// Which immutable bit the caller tried to flip.
107        bit: &'static str,
108    },
109
110    /// A [`crate::dir::FileEntry`] handle no longer matches what is on disk at
111    /// the location it was looked up from: the slot was deleted, or reused by
112    /// a different file. Mutating through such a stale handle would corrupt an
113    /// unrelated entry, so the mutating APIs (`delete`, `rename`,
114    /// `set_attributes`, `set_times`, `truncate`, and `FileWriter::finish`)
115    /// revalidate the on-disk short name first and return this instead.
116    #[cfg(feature = "write")]
117    StaleEntry,
118
119    /// A second [`crate::write::FileWriter`] was requested for a directory entry
120    /// that already has one open. Two concurrent writers would independently
121    /// allocate and cross-link the file's cluster chain, leaking the loser's
122    /// clusters when the last `finish()` wins. Drop or `finish()` the existing
123    /// writer before opening another for the same file.
124    #[cfg(feature = "write")]
125    WriterConflict,
126
127    /// A read-only [`crate::cache::FatSectorCache`] operation needed to evict
128    /// a sector to make room for a new one, but every cached sector is
129    /// dirty. The caller must call [`crate::cache::FatSectorCache::flush`]
130    /// (or [`crate::FatVolume::flush`]) before continuing — read paths can't
131    /// safely drop unwritten dirty data.
132    #[cfg(feature = "cache")]
133    CacheDirtyEviction {
134        /// The dirty sector that would have been evicted.
135        sector: u32,
136    },
137
138    /// Volume is too small for the requested format
139    #[cfg(feature = "write")]
140    VolumeTooSmall {
141        /// Requested volume size
142        size: u64,
143        /// Minimum required size
144        min_size: u64,
145    },
146
147    /// Volume is too large for the requested FAT type
148    #[cfg(feature = "write")]
149    VolumeTooLarge {
150        /// Requested volume size
151        size: u64,
152        /// Maximum supported size
153        max_size: u64,
154    },
155
156    /// Invalid format option
157    #[cfg(feature = "write")]
158    InvalidFormatOption {
159        /// The option that was invalid
160        option: &'static str,
161        /// The reason it was invalid
162        reason: &'static str,
163    },
164
165    // exFAT-specific errors
166    /// Invalid exFAT filesystem signature
167    #[cfg(feature = "unstable-exfat")]
168    ExFatInvalidSignature {
169        /// Expected signature
170        expected: [u8; 8],
171        /// Found signature
172        found: [u8; 8],
173    },
174    /// Invalid exFAT boot sector
175    #[cfg(feature = "unstable-exfat")]
176    ExFatInvalidBootSector {
177        /// Reason for invalidity
178        reason: &'static str,
179    },
180    /// Invalid exFAT boot region checksum
181    #[cfg(feature = "unstable-exfat")]
182    ExFatInvalidChecksum {
183        /// Expected checksum
184        expected: u32,
185        /// Found checksum
186        found: u32,
187    },
188    /// Invalid exFAT directory entry
189    #[cfg(feature = "unstable-exfat")]
190    ExFatInvalidEntry {
191        /// Reason for invalidity
192        reason: &'static str,
193    },
194}
195
196impl fmt::Display for Error {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        match self {
199            Self::InvalidBootSignature { found } => {
200                write!(
201                    f,
202                    "invalid boot signature: expected 0xAA55, found {found:#06x}"
203                )
204            }
205            Self::UnsupportedFatType(ty) => {
206                write!(f, "unsupported FAT type: {ty}")
207            }
208            Self::InvalidFsInfoSignature {
209                field,
210                expected,
211                found,
212            } => {
213                write!(
214                    f,
215                    "invalid FSInfo signature: {field} expected {expected:#010x}, found {found:#010x}"
216                )
217            }
218            Self::InvalidShortFilename => {
219                write!(f, "invalid short filename")
220            }
221            Self::ClusterOutOfBounds { cluster, max } => {
222                write!(f, "cluster {cluster} out of bounds (max: {max})")
223            }
224            Self::BadCluster { cluster } => {
225                write!(f, "bad cluster marker encountered at cluster {cluster}")
226            }
227            Self::UnexpectedEndOfChain { cluster } => {
228                write!(f, "unexpected end of cluster chain at cluster {cluster}")
229            }
230            Self::Io(e) => {
231                write!(f, "I/O error: {e:?}")
232            }
233            Self::IoContext { op, sector, source } => match sector {
234                Some(s) => write!(f, "I/O error reading {op} (sector {s}): {source:?}"),
235                None => write!(f, "I/O error reading {op}: {source:?}"),
236            },
237            Self::CorruptFilesystem { context } => {
238                write!(f, "corrupt filesystem: {context}")
239            }
240            Self::ClusterLoop { cluster } => {
241                write!(f, "cluster chain loop detected at cluster {cluster}")
242            }
243            Self::NotAFile => {
244                write!(f, "entry is not a file")
245            }
246            Self::NotADirectory => {
247                write!(f, "entry is not a directory")
248            }
249            Self::EntryNotFound => {
250                write!(f, "entry not found in directory")
251            }
252            Self::InvalidPath => {
253                write!(f, "path is invalid (empty or malformed)")
254            }
255            #[cfg(feature = "write")]
256            Self::NoFreeSpace => {
257                write!(f, "no free clusters available")
258            }
259            #[cfg(feature = "write")]
260            Self::DirectoryFull => {
261                write!(f, "directory is full (no free entry slots)")
262            }
263            #[cfg(feature = "write")]
264            Self::InvalidFilename => {
265                write!(f, "filename is invalid or too long")
266            }
267            #[cfg(feature = "write")]
268            Self::AlreadyExists => {
269                write!(f, "entry with this name already exists")
270            }
271            #[cfg(feature = "write")]
272            Self::DirectoryNotEmpty => {
273                write!(f, "cannot delete non-empty directory")
274            }
275            #[cfg(feature = "write")]
276            Self::InvalidAttributeChange { bit } => {
277                write!(f, "cannot change immutable attribute bit `{bit}` in place")
278            }
279            #[cfg(feature = "write")]
280            Self::StaleEntry => {
281                write!(
282                    f,
283                    "directory entry handle is stale (its on-disk slot was deleted or reused)"
284                )
285            }
286            #[cfg(feature = "write")]
287            Self::WriterConflict => {
288                write!(f, "a FileWriter is already open for this directory entry")
289            }
290            #[cfg(feature = "cache")]
291            Self::CacheDirtyEviction { sector } => {
292                write!(
293                    f,
294                    "FAT cache is full and every sector is dirty (sector {sector}); call flush() before continuing"
295                )
296            }
297            #[cfg(feature = "write")]
298            Self::VolumeTooSmall { size, min_size } => {
299                write!(
300                    f,
301                    "volume size {size} bytes is too small (minimum: {min_size} bytes)"
302                )
303            }
304            #[cfg(feature = "write")]
305            Self::VolumeTooLarge { size, max_size } => {
306                write!(
307                    f,
308                    "volume size {size} bytes is too large (maximum: {max_size} bytes)"
309                )
310            }
311            #[cfg(feature = "write")]
312            Self::InvalidFormatOption { option, reason } => {
313                write!(f, "invalid format option '{option}': {reason}")
314            }
315            #[cfg(feature = "unstable-exfat")]
316            Self::ExFatInvalidSignature { expected, found } => {
317                write!(
318                    f,
319                    "invalid exFAT signature: expected {:?}, found {:?}",
320                    core::str::from_utf8(expected).unwrap_or("<invalid>"),
321                    core::str::from_utf8(found).unwrap_or("<invalid>")
322                )
323            }
324            #[cfg(feature = "unstable-exfat")]
325            Self::ExFatInvalidBootSector { reason } => {
326                write!(f, "invalid exFAT boot sector: {reason}")
327            }
328            #[cfg(feature = "unstable-exfat")]
329            Self::ExFatInvalidChecksum { expected, found } => {
330                write!(
331                    f,
332                    "invalid exFAT checksum: expected {expected:#010x}, found {found:#010x}"
333                )
334            }
335            #[cfg(feature = "unstable-exfat")]
336            Self::ExFatInvalidEntry { reason } => {
337                write!(f, "invalid exFAT directory entry: {reason}")
338            }
339        }
340    }
341}
342
343#[cfg(feature = "std")]
344impl std::error::Error for Error {}
345
346impl<E: hadris_io::IoError> From<hadris_io::Error<E>> for Error {
347    fn from(e: hadris_io::Error<E>) -> Self {
348        Self::Io(e.erase())
349    }
350}
351
352/// Manual `defmt::Format` impl rather than `derive`, because the wrapped
353/// `hadris_io::Error` does not (yet) implement `Format`. The Io variant logs
354/// without details; everything else mirrors the Display output.
355#[cfg(feature = "defmt")]
356impl defmt::Format for Error {
357    fn format(&self, f: defmt::Formatter) {
358        match self {
359            Self::InvalidBootSignature { found } => {
360                defmt::write!(
361                    f,
362                    "invalid boot signature: expected 0xAA55, found {=u16:#06x}",
363                    *found
364                )
365            }
366            Self::UnsupportedFatType(ty) => defmt::write!(f, "unsupported FAT type: {=str}", *ty),
367            Self::InvalidFsInfoSignature {
368                field,
369                expected,
370                found,
371            } => defmt::write!(
372                f,
373                "invalid FSInfo signature: {=str} expected {=u32:#010x}, found {=u32:#010x}",
374                *field,
375                *expected,
376                *found
377            ),
378            Self::InvalidShortFilename => defmt::write!(f, "invalid short filename"),
379            Self::ClusterOutOfBounds { cluster, max } => {
380                defmt::write!(
381                    f,
382                    "cluster {=u32} out of bounds (max: {=u32})",
383                    *cluster,
384                    *max
385                )
386            }
387            Self::BadCluster { cluster } => {
388                defmt::write!(f, "bad cluster marker at cluster {=u32}", *cluster)
389            }
390            Self::UnexpectedEndOfChain { cluster } => {
391                defmt::write!(f, "unexpected end of cluster chain at {=u32}", *cluster)
392            }
393            // hadris_io::Error doesn't implement defmt::Format yet — log a
394            // generic message rather than dragging the dependency tree.
395            Self::Io(_) => defmt::write!(f, "I/O error"),
396            Self::IoContext { op, sector, .. } => match sector {
397                Some(s) => defmt::write!(f, "I/O error reading {=str} (sector {=u64})", *op, *s),
398                None => defmt::write!(f, "I/O error reading {=str}", *op),
399            },
400            Self::CorruptFilesystem { context } => {
401                defmt::write!(f, "corrupt filesystem: {=str}", *context)
402            }
403            Self::ClusterLoop { cluster } => {
404                defmt::write!(f, "cluster chain loop at {=u32}", *cluster)
405            }
406            Self::NotAFile => defmt::write!(f, "entry is not a file"),
407            Self::NotADirectory => defmt::write!(f, "entry is not a directory"),
408            Self::EntryNotFound => defmt::write!(f, "entry not found"),
409            Self::InvalidPath => defmt::write!(f, "path is invalid"),
410            #[cfg(feature = "write")]
411            Self::NoFreeSpace => defmt::write!(f, "no free clusters"),
412            #[cfg(feature = "write")]
413            Self::DirectoryFull => defmt::write!(f, "directory is full"),
414            #[cfg(feature = "write")]
415            Self::InvalidFilename => defmt::write!(f, "filename invalid or too long"),
416            #[cfg(feature = "write")]
417            Self::AlreadyExists => defmt::write!(f, "entry already exists"),
418            #[cfg(feature = "write")]
419            Self::DirectoryNotEmpty => defmt::write!(f, "directory not empty"),
420            #[cfg(feature = "write")]
421            Self::InvalidAttributeChange { bit } => {
422                defmt::write!(f, "cannot change immutable attribute bit `{=str}`", *bit)
423            }
424            #[cfg(feature = "write")]
425            Self::StaleEntry => {
426                defmt::write!(f, "directory entry handle is stale (deleted or reused)")
427            }
428            #[cfg(feature = "write")]
429            Self::WriterConflict => {
430                defmt::write!(f, "a FileWriter is already open for this directory entry")
431            }
432            #[cfg(feature = "cache")]
433            Self::CacheDirtyEviction { sector } => defmt::write!(
434                f,
435                "FAT cache full and every sector dirty (sector {=u32}); flush() needed",
436                *sector
437            ),
438            #[cfg(feature = "write")]
439            Self::VolumeTooSmall { size, min_size } => defmt::write!(
440                f,
441                "volume size {=u64} too small (min: {=u64})",
442                *size,
443                *min_size
444            ),
445            #[cfg(feature = "write")]
446            Self::VolumeTooLarge { size, max_size } => defmt::write!(
447                f,
448                "volume size {=u64} too large (max: {=u64})",
449                *size,
450                *max_size
451            ),
452            #[cfg(feature = "write")]
453            Self::InvalidFormatOption { option, reason } => {
454                defmt::write!(
455                    f,
456                    "invalid format option `{=str}`: {=str}",
457                    *option,
458                    *reason
459                )
460            }
461            #[cfg(feature = "unstable-exfat")]
462            Self::ExFatInvalidSignature { .. } => defmt::write!(f, "invalid exFAT signature"),
463            #[cfg(feature = "unstable-exfat")]
464            Self::ExFatInvalidBootSector { reason } => {
465                defmt::write!(f, "invalid exFAT boot sector: {=str}", *reason)
466            }
467            #[cfg(feature = "unstable-exfat")]
468            Self::ExFatInvalidChecksum { expected, found } => defmt::write!(
469                f,
470                "invalid exFAT checksum: expected {=u32:#010x}, found {=u32:#010x}",
471                *expected,
472                *found
473            ),
474            #[cfg(feature = "unstable-exfat")]
475            Self::ExFatInvalidEntry { reason } => {
476                defmt::write!(f, "invalid exFAT directory entry: {=str}", *reason)
477            }
478        }
479    }
480}
481
482/// Result type alias for FAT operations.
483pub type Result<T> = core::result::Result<T, Error>;