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