Skip to main content

ext4fs/
error.rs

1#![forbid(unsafe_code)]
2
3use std::fmt;
4use std::io;
5
6/// All errors produced by the ext4fs library.
7#[derive(Debug)]
8pub enum Ext4Error {
9    /// I/O error from the underlying reader.
10    Io(io::Error),
11    /// The superblock magic number was not 0xEF53.
12    InvalidMagic { found: u16 },
13    /// A superblock field is invalid or out of range.
14    InvalidSuperblock(String),
15    /// The filesystem uses an incompatible feature we cannot handle.
16    UnsupportedFeature(String),
17    /// The requested inode number is out of range.
18    InodeOutOfRange { ino: u64, max: u64 },
19    /// The requested block number is out of range.
20    BlockOutOfRange { block: u64, max: u64 },
21    /// A metadata structure is corrupt.
22    CorruptMetadata {
23        structure: &'static str,
24        detail: String,
25    },
26    /// CRC32C checksum mismatch.
27    ChecksumMismatch {
28        structure: &'static str,
29        expected: u32,
30        computed: u32,
31    },
32    /// Path does not exist.
33    PathNotFound(String),
34    /// Expected a directory, found something else.
35    NotADirectory(String),
36    /// Expected a symlink, found something else.
37    NotASymlink(String),
38    /// Too many levels of symbolic links.
39    SymlinkLoop { path: String, depth: u32 },
40    /// Filesystem has no journal.
41    NoJournal,
42    /// Journal data is corrupt.
43    JournalCorrupt(String),
44    /// Could not recover file data.
45    RecoveryFailed { ino: u64, reason: String },
46    /// Insufficient data to parse a structure.
47    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
115/// Convenience alias.
116pub 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}