1#![forbid(unsafe_code)]
2
3use std::fmt;
4use std::io;
5
6#[derive(Debug)]
8pub enum Ext4Error {
9 Io(io::Error),
11 InvalidMagic { found: u16 },
13 InvalidSuperblock(String),
15 UnsupportedFeature(String),
17 InodeOutOfRange { ino: u64, max: u64 },
19 BlockOutOfRange { block: u64, max: u64 },
21 CorruptMetadata {
23 structure: &'static str,
24 detail: String,
25 },
26 ChecksumMismatch {
28 structure: &'static str,
29 expected: u32,
30 computed: u32,
31 },
32 PathNotFound(String),
34 NotADirectory(String),
36 NotASymlink(String),
38 SymlinkLoop { path: String, depth: u32 },
40 NoJournal,
42 JournalCorrupt(String),
44 RecoveryFailed { ino: u64, reason: String },
46 TooShort {
48 structure: &'static str,
49 expected: usize,
50 found: usize,
51 },
52}
53
54impl fmt::Display for Ext4Error {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::Io(e) => write!(f, "I/O error: {e}"),
58 Self::InvalidMagic { found } => write!(
59 f,
60 "invalid superblock magic: 0x{found:04X} (expected 0xEF53)"
61 ),
62 Self::InvalidSuperblock(msg) => write!(f, "invalid superblock: {msg}"),
63 Self::UnsupportedFeature(feat) => write!(f, "unsupported feature: {feat}"),
64 Self::InodeOutOfRange { ino, max } => write!(f, "inode {ino} out of range (max {max})"),
65 Self::BlockOutOfRange { block, max } => {
66 write!(f, "block {block} out of range (max {max})")
67 }
68 Self::CorruptMetadata { structure, detail } => {
69 write!(f, "corrupt {structure}: {detail}")
70 }
71 Self::ChecksumMismatch {
72 structure,
73 expected,
74 computed,
75 } => {
76 write!(f, "checksum mismatch in {structure}: expected 0x{expected:08X}, computed 0x{computed:08X}")
77 }
78 Self::PathNotFound(p) => write!(f, "path not found: {p}"),
79 Self::NotADirectory(p) => write!(f, "not a directory: {p}"),
80 Self::NotASymlink(p) => write!(f, "not a symlink: {p}"),
81 Self::SymlinkLoop { path, depth } => {
82 write!(f, "symlink loop at {path} (depth {depth})")
83 }
84 Self::NoJournal => write!(f, "filesystem has no journal"),
85 Self::JournalCorrupt(msg) => write!(f, "journal corrupt: {msg}"),
86 Self::RecoveryFailed { ino, reason } => {
87 write!(f, "recovery failed for inode {ino}: {reason}")
88 }
89 Self::TooShort {
90 structure,
91 expected,
92 found,
93 } => {
94 write!(f, "{structure}: need {expected} bytes, got {found}")
95 }
96 }
97 }
98}
99
100impl std::error::Error for Ext4Error {
101 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
102 match self {
103 Self::Io(e) => Some(e),
104 _ => None,
105 }
106 }
107}
108
109impl From<io::Error> for Ext4Error {
110 fn from(e: io::Error) -> Self {
111 Self::Io(e)
112 }
113}
114
115pub type Result<T> = std::result::Result<T, Ext4Error>;
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::error::Error;
122
123 #[test]
124 fn display_io() {
125 let err = Ext4Error::Io(io::Error::new(io::ErrorKind::NotFound, "gone"));
126 let msg = err.to_string();
127 assert!(msg.contains("I/O error"), "got: {msg}");
128 assert!(msg.contains("gone"), "got: {msg}");
129 }
130
131 #[test]
132 fn display_invalid_magic() {
133 let err = Ext4Error::InvalidMagic { found: 0xBEEF };
134 let msg = err.to_string();
135 assert!(msg.contains("BEEF"), "got: {msg}");
136 assert!(msg.contains("0xEF53"), "got: {msg}");
137 }
138
139 #[test]
140 fn display_invalid_superblock() {
141 let err = Ext4Error::InvalidSuperblock("bad field".into());
142 let msg = err.to_string();
143 assert!(msg.contains("invalid superblock"), "got: {msg}");
144 assert!(msg.contains("bad field"), "got: {msg}");
145 }
146
147 #[test]
148 fn display_unsupported_feature() {
149 let err = Ext4Error::UnsupportedFeature("inline_data".into());
150 let msg = err.to_string();
151 assert!(msg.contains("unsupported feature"), "got: {msg}");
152 assert!(msg.contains("inline_data"), "got: {msg}");
153 }
154
155 #[test]
156 fn display_inode_out_of_range() {
157 let err = Ext4Error::InodeOutOfRange { ino: 999, max: 100 };
158 let msg = err.to_string();
159 assert!(msg.contains("999"), "got: {msg}");
160 assert!(msg.contains("100"), "got: {msg}");
161 }
162
163 #[test]
164 fn display_block_out_of_range() {
165 let err = Ext4Error::BlockOutOfRange { block: 50, max: 10 };
166 let msg = err.to_string();
167 assert!(msg.contains("50"), "got: {msg}");
168 assert!(msg.contains("10"), "got: {msg}");
169 }
170
171 #[test]
172 fn display_corrupt_metadata() {
173 let err = Ext4Error::CorruptMetadata {
174 structure: "group_desc",
175 detail: "bad checksum".into(),
176 };
177 let msg = err.to_string();
178 assert!(msg.contains("corrupt group_desc"), "got: {msg}");
179 assert!(msg.contains("bad checksum"), "got: {msg}");
180 }
181
182 #[test]
183 fn display_checksum_mismatch() {
184 let err = Ext4Error::ChecksumMismatch {
185 structure: "inode",
186 expected: 0xDEAD_BEEF,
187 computed: 0xCAFE_BABE,
188 };
189 let msg = err.to_string();
190 assert!(msg.contains("inode"), "got: {msg}");
191 assert!(msg.contains("DEADBEEF"), "got: {msg}");
192 assert!(msg.contains("CAFEBABE"), "got: {msg}");
193 }
194
195 #[test]
196 fn display_path_not_found() {
197 let err = Ext4Error::PathNotFound("/missing".into());
198 let msg = err.to_string();
199 assert!(msg.contains("path not found"), "got: {msg}");
200 assert!(msg.contains("/missing"), "got: {msg}");
201 }
202
203 #[test]
204 fn display_not_a_directory() {
205 let err = Ext4Error::NotADirectory("/file".into());
206 let msg = err.to_string();
207 assert!(msg.contains("not a directory"), "got: {msg}");
208 }
209
210 #[test]
211 fn display_not_a_symlink() {
212 let err = Ext4Error::NotASymlink("/regular".into());
213 let msg = err.to_string();
214 assert!(msg.contains("not a symlink"), "got: {msg}");
215 }
216
217 #[test]
218 fn display_symlink_loop() {
219 let err = Ext4Error::SymlinkLoop {
220 path: "/a".into(),
221 depth: 40,
222 };
223 let msg = err.to_string();
224 assert!(msg.contains("symlink loop"), "got: {msg}");
225 assert!(msg.contains("40"), "got: {msg}");
226 }
227
228 #[test]
229 fn display_no_journal() {
230 let err = Ext4Error::NoJournal;
231 let msg = err.to_string();
232 assert!(msg.contains("no journal"), "got: {msg}");
233 }
234
235 #[test]
236 fn display_journal_corrupt() {
237 let err = Ext4Error::JournalCorrupt("truncated".into());
238 let msg = err.to_string();
239 assert!(msg.contains("journal corrupt"), "got: {msg}");
240 assert!(msg.contains("truncated"), "got: {msg}");
241 }
242
243 #[test]
244 fn display_recovery_failed() {
245 let err = Ext4Error::RecoveryFailed {
246 ino: 42,
247 reason: "zeroed".into(),
248 };
249 let msg = err.to_string();
250 assert!(msg.contains("42"), "got: {msg}");
251 assert!(msg.contains("zeroed"), "got: {msg}");
252 }
253
254 #[test]
255 fn display_too_short() {
256 let err = Ext4Error::TooShort {
257 structure: "extent_header",
258 expected: 12,
259 found: 4,
260 };
261 let msg = err.to_string();
262 assert!(msg.contains("extent_header"), "got: {msg}");
263 assert!(msg.contains("12"), "got: {msg}");
264 assert!(msg.contains('4'), "got: {msg}");
265 }
266
267 #[test]
268 fn source_io_returns_some() {
269 let err = Ext4Error::Io(io::Error::new(io::ErrorKind::BrokenPipe, "pipe"));
270 assert!(err.source().is_some());
271 }
272
273 #[test]
274 fn source_non_io_returns_none() {
275 let err = Ext4Error::NoJournal;
276 assert!(err.source().is_none());
277
278 let err = Ext4Error::InvalidMagic { found: 0 };
279 assert!(err.source().is_none());
280
281 let err = Ext4Error::PathNotFound("x".into());
282 assert!(err.source().is_none());
283 }
284
285 #[test]
286 fn from_io_error() {
287 let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "denied");
288 let ext4_err: Ext4Error = io_err.into();
289 match ext4_err {
290 Ext4Error::Io(_) => {}
291 other => panic!("expected Io variant, got: {other:?}"),
292 }
293 }
294}