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 read-only [`crate::cache::FatSectorCache`] operation needed to evict
111    /// a sector to make room for a new one, but every cached sector is
112    /// dirty. The caller must call [`crate::cache::FatSectorCache::flush`]
113    /// (or [`crate::FatVolume::flush`]) before continuing — read paths can't
114    /// safely drop unwritten dirty data.
115    #[cfg(feature = "cache")]
116    CacheDirtyEviction {
117        /// The dirty sector that would have been evicted.
118        sector: u32,
119    },
120
121    /// Volume is too small for the requested format
122    #[cfg(feature = "write")]
123    VolumeTooSmall {
124        /// Requested volume size
125        size: u64,
126        /// Minimum required size
127        min_size: u64,
128    },
129
130    /// Volume is too large for the requested FAT type
131    #[cfg(feature = "write")]
132    VolumeTooLarge {
133        /// Requested volume size
134        size: u64,
135        /// Maximum supported size
136        max_size: u64,
137    },
138
139    /// Invalid format option
140    #[cfg(feature = "write")]
141    InvalidFormatOption {
142        /// The option that was invalid
143        option: &'static str,
144        /// The reason it was invalid
145        reason: &'static str,
146    },
147
148    // exFAT-specific errors
149    /// Invalid exFAT filesystem signature
150    #[cfg(feature = "unstable-exfat")]
151    ExFatInvalidSignature {
152        /// Expected signature
153        expected: [u8; 8],
154        /// Found signature
155        found: [u8; 8],
156    },
157    /// Invalid exFAT boot sector
158    #[cfg(feature = "unstable-exfat")]
159    ExFatInvalidBootSector {
160        /// Reason for invalidity
161        reason: &'static str,
162    },
163    /// Invalid exFAT boot region checksum
164    #[cfg(feature = "unstable-exfat")]
165    ExFatInvalidChecksum {
166        /// Expected checksum
167        expected: u32,
168        /// Found checksum
169        found: u32,
170    },
171    /// Invalid exFAT directory entry
172    #[cfg(feature = "unstable-exfat")]
173    ExFatInvalidEntry {
174        /// Reason for invalidity
175        reason: &'static str,
176    },
177}
178
179impl fmt::Display for Error {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        match self {
182            Self::InvalidBootSignature { found } => {
183                write!(
184                    f,
185                    "invalid boot signature: expected 0xAA55, found {found:#06x}"
186                )
187            }
188            Self::UnsupportedFatType(ty) => {
189                write!(f, "unsupported FAT type: {ty}")
190            }
191            Self::InvalidFsInfoSignature {
192                field,
193                expected,
194                found,
195            } => {
196                write!(
197                    f,
198                    "invalid FSInfo signature: {field} expected {expected:#010x}, found {found:#010x}"
199                )
200            }
201            Self::InvalidShortFilename => {
202                write!(f, "invalid short filename")
203            }
204            Self::ClusterOutOfBounds { cluster, max } => {
205                write!(f, "cluster {cluster} out of bounds (max: {max})")
206            }
207            Self::BadCluster { cluster } => {
208                write!(f, "bad cluster marker encountered at cluster {cluster}")
209            }
210            Self::UnexpectedEndOfChain { cluster } => {
211                write!(f, "unexpected end of cluster chain at cluster {cluster}")
212            }
213            Self::Io(e) => {
214                write!(f, "I/O error: {e:?}")
215            }
216            Self::IoContext { op, sector, source } => match sector {
217                Some(s) => write!(f, "I/O error reading {op} (sector {s}): {source:?}"),
218                None => write!(f, "I/O error reading {op}: {source:?}"),
219            },
220            Self::CorruptFilesystem { context } => {
221                write!(f, "corrupt filesystem: {context}")
222            }
223            Self::ClusterLoop { cluster } => {
224                write!(f, "cluster chain loop detected at cluster {cluster}")
225            }
226            Self::NotAFile => {
227                write!(f, "entry is not a file")
228            }
229            Self::NotADirectory => {
230                write!(f, "entry is not a directory")
231            }
232            Self::EntryNotFound => {
233                write!(f, "entry not found in directory")
234            }
235            Self::InvalidPath => {
236                write!(f, "path is invalid (empty or malformed)")
237            }
238            #[cfg(feature = "write")]
239            Self::NoFreeSpace => {
240                write!(f, "no free clusters available")
241            }
242            #[cfg(feature = "write")]
243            Self::DirectoryFull => {
244                write!(f, "directory is full (no free entry slots)")
245            }
246            #[cfg(feature = "write")]
247            Self::InvalidFilename => {
248                write!(f, "filename is invalid or too long")
249            }
250            #[cfg(feature = "write")]
251            Self::AlreadyExists => {
252                write!(f, "entry with this name already exists")
253            }
254            #[cfg(feature = "write")]
255            Self::DirectoryNotEmpty => {
256                write!(f, "cannot delete non-empty directory")
257            }
258            #[cfg(feature = "write")]
259            Self::InvalidAttributeChange { bit } => {
260                write!(f, "cannot change immutable attribute bit `{bit}` in place")
261            }
262            #[cfg(feature = "cache")]
263            Self::CacheDirtyEviction { sector } => {
264                write!(
265                    f,
266                    "FAT cache is full and every sector is dirty (sector {sector}); call flush() before continuing"
267                )
268            }
269            #[cfg(feature = "write")]
270            Self::VolumeTooSmall { size, min_size } => {
271                write!(
272                    f,
273                    "volume size {size} bytes is too small (minimum: {min_size} bytes)"
274                )
275            }
276            #[cfg(feature = "write")]
277            Self::VolumeTooLarge { size, max_size } => {
278                write!(
279                    f,
280                    "volume size {size} bytes is too large (maximum: {max_size} bytes)"
281                )
282            }
283            #[cfg(feature = "write")]
284            Self::InvalidFormatOption { option, reason } => {
285                write!(f, "invalid format option '{option}': {reason}")
286            }
287            #[cfg(feature = "unstable-exfat")]
288            Self::ExFatInvalidSignature { expected, found } => {
289                write!(
290                    f,
291                    "invalid exFAT signature: expected {:?}, found {:?}",
292                    core::str::from_utf8(expected).unwrap_or("<invalid>"),
293                    core::str::from_utf8(found).unwrap_or("<invalid>")
294                )
295            }
296            #[cfg(feature = "unstable-exfat")]
297            Self::ExFatInvalidBootSector { reason } => {
298                write!(f, "invalid exFAT boot sector: {reason}")
299            }
300            #[cfg(feature = "unstable-exfat")]
301            Self::ExFatInvalidChecksum { expected, found } => {
302                write!(
303                    f,
304                    "invalid exFAT checksum: expected {expected:#010x}, found {found:#010x}"
305                )
306            }
307            #[cfg(feature = "unstable-exfat")]
308            Self::ExFatInvalidEntry { reason } => {
309                write!(f, "invalid exFAT directory entry: {reason}")
310            }
311        }
312    }
313}
314
315#[cfg(feature = "std")]
316impl std::error::Error for Error {}
317
318impl<E: hadris_io::IoError> From<hadris_io::Error<E>> for Error {
319    fn from(e: hadris_io::Error<E>) -> Self {
320        Self::Io(e.erase())
321    }
322}
323
324/// Manual `defmt::Format` impl rather than `derive`, because the wrapped
325/// `hadris_io::Error` does not (yet) implement `Format`. The Io variant logs
326/// without details; everything else mirrors the Display output.
327#[cfg(feature = "defmt")]
328impl defmt::Format for Error {
329    fn format(&self, f: defmt::Formatter) {
330        match self {
331            Self::InvalidBootSignature { found } => {
332                defmt::write!(
333                    f,
334                    "invalid boot signature: expected 0xAA55, found {=u16:#06x}",
335                    *found
336                )
337            }
338            Self::UnsupportedFatType(ty) => defmt::write!(f, "unsupported FAT type: {=str}", *ty),
339            Self::InvalidFsInfoSignature {
340                field,
341                expected,
342                found,
343            } => defmt::write!(
344                f,
345                "invalid FSInfo signature: {=str} expected {=u32:#010x}, found {=u32:#010x}",
346                *field,
347                *expected,
348                *found
349            ),
350            Self::InvalidShortFilename => defmt::write!(f, "invalid short filename"),
351            Self::ClusterOutOfBounds { cluster, max } => {
352                defmt::write!(
353                    f,
354                    "cluster {=u32} out of bounds (max: {=u32})",
355                    *cluster,
356                    *max
357                )
358            }
359            Self::BadCluster { cluster } => {
360                defmt::write!(f, "bad cluster marker at cluster {=u32}", *cluster)
361            }
362            Self::UnexpectedEndOfChain { cluster } => {
363                defmt::write!(f, "unexpected end of cluster chain at {=u32}", *cluster)
364            }
365            // hadris_io::Error doesn't implement defmt::Format yet — log a
366            // generic message rather than dragging the dependency tree.
367            Self::Io(_) => defmt::write!(f, "I/O error"),
368            Self::IoContext { op, sector, .. } => match sector {
369                Some(s) => defmt::write!(f, "I/O error reading {=str} (sector {=u64})", *op, *s),
370                None => defmt::write!(f, "I/O error reading {=str}", *op),
371            },
372            Self::CorruptFilesystem { context } => {
373                defmt::write!(f, "corrupt filesystem: {=str}", *context)
374            }
375            Self::ClusterLoop { cluster } => {
376                defmt::write!(f, "cluster chain loop at {=u32}", *cluster)
377            }
378            Self::NotAFile => defmt::write!(f, "entry is not a file"),
379            Self::NotADirectory => defmt::write!(f, "entry is not a directory"),
380            Self::EntryNotFound => defmt::write!(f, "entry not found"),
381            Self::InvalidPath => defmt::write!(f, "path is invalid"),
382            #[cfg(feature = "write")]
383            Self::NoFreeSpace => defmt::write!(f, "no free clusters"),
384            #[cfg(feature = "write")]
385            Self::DirectoryFull => defmt::write!(f, "directory is full"),
386            #[cfg(feature = "write")]
387            Self::InvalidFilename => defmt::write!(f, "filename invalid or too long"),
388            #[cfg(feature = "write")]
389            Self::AlreadyExists => defmt::write!(f, "entry already exists"),
390            #[cfg(feature = "write")]
391            Self::DirectoryNotEmpty => defmt::write!(f, "directory not empty"),
392            #[cfg(feature = "write")]
393            Self::InvalidAttributeChange { bit } => {
394                defmt::write!(f, "cannot change immutable attribute bit `{=str}`", *bit)
395            }
396            #[cfg(feature = "cache")]
397            Self::CacheDirtyEviction { sector } => defmt::write!(
398                f,
399                "FAT cache full and every sector dirty (sector {=u32}); flush() needed",
400                *sector
401            ),
402            #[cfg(feature = "write")]
403            Self::VolumeTooSmall { size, min_size } => defmt::write!(
404                f,
405                "volume size {=u64} too small (min: {=u64})",
406                *size,
407                *min_size
408            ),
409            #[cfg(feature = "write")]
410            Self::VolumeTooLarge { size, max_size } => defmt::write!(
411                f,
412                "volume size {=u64} too large (max: {=u64})",
413                *size,
414                *max_size
415            ),
416            #[cfg(feature = "write")]
417            Self::InvalidFormatOption { option, reason } => {
418                defmt::write!(
419                    f,
420                    "invalid format option `{=str}`: {=str}",
421                    *option,
422                    *reason
423                )
424            }
425            #[cfg(feature = "unstable-exfat")]
426            Self::ExFatInvalidSignature { .. } => defmt::write!(f, "invalid exFAT signature"),
427            #[cfg(feature = "unstable-exfat")]
428            Self::ExFatInvalidBootSector { reason } => {
429                defmt::write!(f, "invalid exFAT boot sector: {=str}", *reason)
430            }
431            #[cfg(feature = "unstable-exfat")]
432            Self::ExFatInvalidChecksum { expected, found } => defmt::write!(
433                f,
434                "invalid exFAT checksum: expected {=u32:#010x}, found {=u32:#010x}",
435                *expected,
436                *found
437            ),
438            #[cfg(feature = "unstable-exfat")]
439            Self::ExFatInvalidEntry { reason } => {
440                defmt::write!(f, "invalid exFAT directory entry: {=str}", *reason)
441            }
442        }
443    }
444}
445
446/// Result type alias for FAT operations.
447pub type Result<T> = core::result::Result<T, Error>;