ext4_view/
error.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use crate::block_size::BlockSize;
10use crate::features::IncompatibleFeatures;
11use crate::inode::{InodeIndex, InodeMode};
12use alloc::boxed::Box;
13use core::error::Error;
14use core::fmt::{self, Debug, Display, Formatter};
15use core::num::NonZero;
16
17/// Boxed error, used for IO errors. This is similar in spirit to
18/// `anyhow::Error`, although a much simpler implementation.
19pub(crate) type BoxedError = Box<dyn Error + Send + Sync + 'static>;
20
21/// Common error type for all [`Ext4`] operations.
22///
23/// [`Ext4`]: crate::Ext4
24#[derive(Debug)]
25#[non_exhaustive]
26pub enum Ext4Error {
27    /// An operation that requires an absolute path was attempted on a
28    /// relative path.
29    NotAbsolute,
30
31    /// An operation that requires a symlink was attempted on a
32    /// non-symlink file.
33    NotASymlink,
34
35    /// A path points to a non-existent file.
36    NotFound,
37
38    /// An operation that requires a non-directory path was attempted on
39    /// a directory path.
40    IsADirectory,
41
42    /// An operation that requires a directory path was attempted on a
43    /// non-directory path.
44    NotADirectory,
45
46    /// An operation that requires a regular file (or a symlink to a
47    /// regular file) was attempted on a special file (fifo, character
48    /// device, block device, or socket).
49    IsASpecialFile,
50
51    /// The file cannot be read into memory because it is too large.
52    FileTooLarge,
53
54    /// Data is not valid UTF-8.
55    NotUtf8,
56
57    /// Data cannot be converted into a valid path.
58    MalformedPath,
59
60    /// Path is too long.
61    ///
62    /// Maximum path length is not strictly enforced by this library for
63    /// all paths, but during path resolution the length may not exceed
64    /// 4096 bytes.
65    PathTooLong,
66
67    /// Path could not be resolved because it contains too many levels
68    /// of symbolic links.
69    TooManySymlinks,
70
71    /// Attempted to read an encrypted file.
72    ///
73    /// Only unencrypted files are currently supported. Please file an
74    /// [issue] if you have a use case for reading encrypted files.
75    ///
76    /// [issue]: https://github.com/nicholasbishop/ext4-view-rs/issues/new
77    Encrypted,
78
79    /// An IO operation failed. This error comes from the [`Ext4Read`]
80    /// passed to [`Ext4::load`].
81    ///
82    /// [`Ext4::load`]: crate::Ext4::load
83    /// [`Ext4Read`]: crate::Ext4Read
84    Io(
85        /// Underlying error.
86        BoxedError,
87    ),
88
89    /// The filesystem is not supported by this library. This does not
90    /// indicate a problem with the filesystem, or with the calling
91    /// code. Please file a feature request and include the incompatible
92    /// features.
93    Incompatible(Incompatible),
94
95    /// The filesystem is corrupt in some way.
96    Corrupt(Corrupt),
97}
98
99impl Ext4Error {
100    /// If the error type is [`Ext4Error::Io`], get the underlying error.
101    #[must_use]
102    pub fn as_io(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
103        if let Self::Io(err) = self {
104            Some(&**err)
105        } else {
106            None
107        }
108    }
109}
110
111impl Display for Ext4Error {
112    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113        match self {
114            Self::NotAbsolute => write!(f, "path is not absolute"),
115            Self::NotASymlink => write!(f, "path is not a symlink"),
116            Self::NotFound => write!(f, "file not found"),
117            Self::IsADirectory => write!(f, "path is a directory"),
118            Self::NotADirectory => write!(f, "path is not a directory"),
119            Self::IsASpecialFile => write!(f, "path is a special file"),
120            Self::FileTooLarge => {
121                write!(f, "file is too large to store in memory")
122            }
123            Self::NotUtf8 => write!(f, "data is not utf-8"),
124            Self::MalformedPath => write!(f, "data is not a valid path"),
125            Self::PathTooLong => write!(f, "path is too long"),
126            Self::TooManySymlinks => {
127                write!(f, "too many levels of symbolic links")
128            }
129            Self::Encrypted => write!(f, "file is encrypted"),
130            // TODO: if the `Error` trait ever makes it into core, stop
131            // printing `err` here and return it via `Error::source` instead.
132            Self::Io(err) => write!(f, "io error: {err}"),
133            Self::Incompatible(i) => write!(f, "incompatible filesystem: {i}"),
134            Self::Corrupt(c) => write!(f, "corrupt filesystem: {c}"),
135        }
136    }
137}
138
139impl Error for Ext4Error {}
140
141#[cfg(feature = "std")]
142impl From<Ext4Error> for std::io::Error {
143    fn from(e: Ext4Error) -> Self {
144        use std::io::ErrorKind::*;
145
146        // TODO: Rust 1.83 adds NotADirectory, IsADirectory, and
147        // FileTooLarge to std::io::Error; use those after bumping the
148        // MSRV.
149        match e {
150            Ext4Error::IsADirectory
151            | Ext4Error::IsASpecialFile
152            | Ext4Error::MalformedPath
153            | Ext4Error::NotADirectory
154            | Ext4Error::NotASymlink
155            | Ext4Error::NotAbsolute => InvalidInput.into(),
156            Ext4Error::Corrupt(_)
157            | Ext4Error::FileTooLarge
158            | Ext4Error::Incompatible(_)
159            | Ext4Error::PathTooLong
160            | Ext4Error::TooManySymlinks => Self::other(e),
161            Ext4Error::Io(inner) => Self::other(inner),
162            Ext4Error::NotFound => NotFound.into(),
163            Ext4Error::NotUtf8 => InvalidData.into(),
164            Ext4Error::Encrypted => PermissionDenied.into(),
165        }
166    }
167}
168
169/// Error type used in [`Ext4Error::Corrupt`] when the filesystem is
170/// corrupt in some way.
171#[derive(Clone, Eq, PartialEq)]
172pub struct Corrupt(CorruptKind);
173
174impl Debug for Corrupt {
175    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
176        <CorruptKind as Debug>::fmt(&self.0, f)
177    }
178}
179
180impl Display for Corrupt {
181    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
182        <CorruptKind as Display>::fmt(&self.0, f)
183    }
184}
185
186#[derive(Clone, Debug, Eq, PartialEq)]
187#[non_exhaustive]
188pub(crate) enum CorruptKind {
189    /// Superblock magic is invalid.
190    SuperblockMagic,
191
192    /// Superblock checksum is invalid.
193    SuperblockChecksum,
194
195    /// The block size in the superblock is invalid.
196    InvalidBlockSize,
197
198    /// The number of block groups does not fit in a [`u32`].
199    TooManyBlockGroups,
200
201    /// The number of inodes per block group is zero.
202    InodesPerBlockGroup,
203
204    /// The inode size exceeds the block size.
205    InodeSize,
206
207    /// The journal inode in the superblock is invalid.
208    JournalInode,
209
210    /// Invalid first data block.
211    FirstDataBlock(
212        /// First data block.
213        u32,
214    ),
215
216    /// Invalid block group descriptor.
217    BlockGroupDescriptor(
218        /// Block group number.
219        u32,
220    ),
221
222    /// Block group descriptor checksum is invalid.
223    BlockGroupDescriptorChecksum(
224        /// Block group number.
225        u32,
226    ),
227
228    /// Journal size is invalid.
229    JournalSize,
230
231    /// Journal magic is invalid.
232    JournalMagic,
233
234    /// Journal superblock checksum is invalid.
235    JournalSuperblockChecksum,
236
237    /// Journal block size does not match the filesystem block size.
238    JournalBlockSize,
239
240    /// Journal does not have the expected number of blocks.
241    JournalTruncated,
242
243    /// Journal first commit doesn't match the sequence number in the superblock.
244    JournalSequence,
245
246    /// Journal commit block checksum is invalid.
247    JournalCommitBlockChecksum,
248
249    /// Journal descriptor block checksum is invalid.
250    JournalDescriptorBlockChecksum,
251
252    /// Journal descriptor tag checksum is invalid.
253    JournalDescriptorTagChecksum,
254
255    /// Journal sequence number overflowed.
256    JournalSequenceOverflow,
257
258    /// Journal has a truncated descriptor block. Either it is missing a
259    /// tag with the `LAST_TAG` flag set, or the final tag does have
260    /// that flag set but there are not enough bytes to read the full
261    /// tag.
262    JournalDescriptorBlockTruncated,
263
264    /// An inode's checksum is invalid.
265    InodeChecksum(InodeIndex),
266
267    /// An inode is too small.
268    InodeTruncated { inode: InodeIndex, size: usize },
269
270    /// An inode's block group is invalid.
271    InodeBlockGroup {
272        inode: InodeIndex,
273        block_group: u32,
274        num_block_groups: usize,
275    },
276
277    /// Failed to calculate an inode's location.
278    ///
279    /// This error can be returned by various calculations in
280    /// `get_inode_location`. The fields here are sufficient to
281    /// reconstruct which specific calculation failed.
282    InodeLocation {
283        inode: InodeIndex,
284        block_group: u32,
285        inodes_per_block_group: NonZero<u32>,
286        inode_size: u16,
287        block_size: BlockSize,
288        inode_table_first_block: u64,
289    },
290
291    /// An inode's file type is invalid.
292    InodeFileType { inode: InodeIndex, mode: InodeMode },
293
294    /// The target of a symlink is not a valid path.
295    SymlinkTarget(InodeIndex),
296
297    /// The number of blocks in a file exceeds 2^32.
298    TooManyBlocksInFile,
299
300    /// An extent's magic is invalid.
301    ExtentMagic(InodeIndex),
302
303    /// An extent's checksum is invalid.
304    ExtentChecksum(InodeIndex),
305
306    /// An extent's depth is greater than five.
307    ExtentDepth(InodeIndex),
308
309    /// Not enough data is present to read an extent node.
310    ExtentNotEnoughData(InodeIndex),
311
312    /// An extent points to an invalid block.
313    ExtentBlock(InodeIndex),
314
315    /// An extent node's size exceeds the block size.
316    ExtentNodeSize(InodeIndex),
317
318    /// A directory block's checksum is invalid.
319    DirBlockChecksum(InodeIndex),
320
321    // TODO: consider breaking this down into more specific problems.
322    /// A directory entry is invalid.
323    DirEntry(InodeIndex),
324
325    /// Invalid read of a block.
326    BlockRead {
327        /// Absolute block index.
328        block_index: u64,
329
330        /// Absolute block index, without remapping from the journal. If
331        /// this block was not remapped by the journal, this field will
332        /// be the same as `block_index`.
333        original_block_index: u64,
334
335        /// Offset in bytes within the block.
336        offset_within_block: u32,
337
338        /// Length in bytes of the read.
339        read_len: usize,
340    },
341}
342
343impl Display for CorruptKind {
344    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
345        match self {
346            Self::SuperblockMagic => write!(f, "invalid superblock magic"),
347            Self::SuperblockChecksum => {
348                write!(f, "invalid superblock checksum")
349            }
350            Self::InvalidBlockSize => write!(f, "invalid block size"),
351            Self::TooManyBlockGroups => write!(f, "too many block groups"),
352            Self::InodesPerBlockGroup => {
353                write!(f, "inodes per block group is zero")
354            }
355            Self::InodeSize => write!(f, "inode size is invalid"),
356            Self::JournalInode => write!(f, "invalid journal inode"),
357            Self::FirstDataBlock(block) => {
358                write!(f, "invalid first data block: {block}")
359            }
360            Self::BlockGroupDescriptor(block_group_num) => {
361                write!(f, "block group descriptor {block_group_num} is invalid")
362            }
363            Self::BlockGroupDescriptorChecksum(block_group_num) => write!(
364                f,
365                "invalid checksum for block group descriptor {block_group_num}"
366            ),
367            Self::JournalSize => {
368                write!(f, "journal size is invalid")
369            }
370            Self::JournalMagic => {
371                write!(f, "journal magic is invalid")
372            }
373            Self::JournalSuperblockChecksum => {
374                write!(f, "journal superblock checksum is invalid")
375            }
376            Self::JournalBlockSize => {
377                write!(
378                    f,
379                    "journal block size does not match filesystem block size"
380                )
381            }
382            Self::JournalTruncated => write!(f, "journal is truncated"),
383            Self::JournalSequence => write!(
384                f,
385                "journal's first commit doesn't match the expected sequence"
386            ),
387            Self::JournalCommitBlockChecksum => {
388                write!(f, "journal commit block checksum is invalid")
389            }
390            Self::JournalDescriptorBlockChecksum => {
391                write!(f, "journal descriptor block checksum is invalid")
392            }
393            Self::JournalDescriptorTagChecksum => {
394                write!(f, "journal descriptor tag checksum is invalid")
395            }
396            Self::JournalSequenceOverflow => {
397                write!(f, "journal sequence number overflowed")
398            }
399            Self::JournalDescriptorBlockTruncated => {
400                write!(f, "journal descriptor block is truncated")
401            }
402            Self::InodeChecksum(inode) => {
403                write!(f, "invalid checksum for inode {inode}")
404            }
405            Self::InodeTruncated { inode, size } => {
406                write!(f, "inode {inode} is truncated: size={size}")
407            }
408            Self::InodeBlockGroup {
409                inode,
410                block_group,
411                num_block_groups,
412            } => {
413                write!(
414                    f,
415                    "inode {inode} has an invalid block group index: block_group={block_group}, num_block_groups={num_block_groups}"
416                )
417            }
418            Self::InodeLocation {
419                inode,
420                block_group,
421                inodes_per_block_group,
422                inode_size,
423                block_size,
424                inode_table_first_block,
425            } => {
426                write!(
427                    f,
428                    "inode {inode} has invalid location: block_group={block_group}, inodes_per_block_group={inodes_per_block_group}, inode_size={inode_size}, block_size={block_size}, inode_table_first_block={inode_table_first_block}"
429                )
430            }
431            Self::InodeFileType { inode, mode } => {
432                write!(
433                    f,
434                    "inode {inode} has invalid file type: mode=0x{mode:04x}",
435                    mode = mode.bits()
436                )
437            }
438            Self::SymlinkTarget(inode) => {
439                write!(f, "inode {inode} has an invalid symlink path")
440            }
441            Self::TooManyBlocksInFile => write!(f, "too many blocks in file"),
442            Self::ExtentMagic(inode) => {
443                write!(f, "extent in inode {inode} has invalid magic")
444            }
445            Self::ExtentChecksum(inode) => {
446                write!(f, "extent in inode {inode} has an invalid checksum")
447            }
448            Self::ExtentDepth(inode) => {
449                write!(f, "extent in inode {inode} has an invalid depth")
450            }
451            Self::ExtentNotEnoughData(inode) => {
452                write!(f, "extent data in inode {inode} is invalid")
453            }
454            Self::ExtentBlock(inode) => {
455                write!(f, "extent in inode {inode} points to an invalid block")
456            }
457            Self::ExtentNodeSize(inode) => {
458                write!(
459                    f,
460                    "extent in inode {inode} has a node with an invalid size"
461                )
462            }
463            Self::DirBlockChecksum(inode) => write!(
464                f,
465                "directory block in inode {inode} has an invalid checksum"
466            ),
467            Self::DirEntry(inode) => {
468                write!(f, "invalid directory entry in inode {inode}")
469            }
470            Self::BlockRead {
471                block_index,
472                original_block_index,
473                offset_within_block,
474                read_len,
475            } => {
476                write!(
477                    f,
478                    "invalid read of length {read_len} from block {block_index} (originally {original_block_index}) at offset {offset_within_block}"
479                )
480            }
481        }
482    }
483}
484
485impl PartialEq<CorruptKind> for Ext4Error {
486    fn eq(&self, ck: &CorruptKind) -> bool {
487        if let Self::Corrupt(c) = self {
488            c.0 == *ck
489        } else {
490            false
491        }
492    }
493}
494
495impl From<CorruptKind> for Ext4Error {
496    fn from(c: CorruptKind) -> Self {
497        Self::Corrupt(Corrupt(c))
498    }
499}
500
501/// Error type used in [`Ext4Error::Incompatible`] when the filesystem
502/// cannot be read due to incomplete support in this library.
503#[derive(Clone, Eq, PartialEq)]
504pub struct Incompatible(IncompatibleKind);
505
506impl Debug for Incompatible {
507    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
508        <IncompatibleKind as Debug>::fmt(&self.0, f)
509    }
510}
511
512impl Display for Incompatible {
513    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
514        <IncompatibleKind as Display>::fmt(&self.0, f)
515    }
516}
517
518#[derive(Clone, Debug, Eq, PartialEq)]
519#[non_exhaustive]
520pub(crate) enum IncompatibleKind {
521    /// One or more required features are missing.
522    MissingRequiredFeatures(
523        /// The missing features.
524        IncompatibleFeatures,
525    ),
526
527    /// One or more unsupported features are present.
528    #[allow(clippy::enum_variant_names)]
529    UnsupportedFeatures(
530        /// The unsupported features.
531        IncompatibleFeatures,
532    ),
533
534    /// The directory hash algorithm is not supported.
535    DirectoryHash(
536        /// The algorithm identifier.
537        u8,
538    ),
539
540    /// The journal superblock type is not supported.
541    JournalSuperblockType(
542        /// Raw journal block type.
543        u32,
544    ),
545
546    /// The journal checksum type is not supported.
547    JournalChecksumType(
548        /// Raw journal checksum type.
549        u8,
550    ),
551
552    /// One or more required journal features are missing.
553    MissingRequiredJournalFeatures(
554        /// The missing feature bits.
555        u32,
556    ),
557
558    /// One or more unsupported journal features are present.
559    #[allow(clippy::enum_variant_names)]
560    UnsupportedJournalFeatures(
561        /// The unsupported feature bits.
562        u32,
563    ),
564
565    /// The journal contains an unsupported block type.
566    JournalBlockType(
567        /// Raw journal block type.
568        u32,
569    ),
570
571    /// The journal contains an escaped block.
572    JournalBlockEscaped,
573}
574
575impl Display for IncompatibleKind {
576    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
577        match self {
578            Self::MissingRequiredFeatures(feat) => {
579                write!(f, "missing required features: {feat:?}")
580            }
581            Self::UnsupportedFeatures(feat) => {
582                write!(f, "unsupported features: {feat:?}")
583            }
584            Self::DirectoryHash(algorithm) => {
585                write!(f, "unsupported directory hash algorithm: {algorithm}")
586            }
587            Self::JournalSuperblockType(val) => {
588                write!(f, "journal superblock type is not supported: {val}")
589            }
590            Self::JournalBlockType(val) => {
591                write!(f, "journal block type is not supported: {val}")
592            }
593            Self::JournalBlockEscaped => {
594                write!(f, "journal contains an escaped data block")
595            }
596            Self::JournalChecksumType(val) => {
597                write!(f, "journal checksum type is not supported: {val}")
598            }
599            Self::MissingRequiredJournalFeatures(feat) => {
600                write!(f, "missing required journal features: {feat:?}")
601            }
602            Self::UnsupportedJournalFeatures(feat) => {
603                write!(f, "unsupported journal features: {feat:?}")
604            }
605        }
606    }
607}
608
609impl PartialEq<IncompatibleKind> for Ext4Error {
610    fn eq(&self, other: &IncompatibleKind) -> bool {
611        if let Self::Incompatible(Incompatible(i)) = self {
612            i == other
613        } else {
614            false
615        }
616    }
617}
618
619impl From<IncompatibleKind> for Ext4Error {
620    fn from(k: IncompatibleKind) -> Self {
621        Self::Incompatible(Incompatible(k))
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628
629    /// Test the `Display` and `Debug` impls for a corruption error.
630    ///
631    /// Only one `CorruptKind` variant is tested, the focus of the test
632    /// is the formatting of the nested error type:
633    /// `Ext4Error::Corrupt(Corrupt(CorruptKind))`
634    #[test]
635    fn test_corrupt_format() {
636        let err: Ext4Error = CorruptKind::BlockRead {
637            block_index: 123,
638            original_block_index: 124,
639            offset_within_block: 456,
640            read_len: 789,
641        }
642        .into();
643
644        assert_eq!(
645            format!("{err}"),
646            "corrupt filesystem: invalid read of length 789 from block 123 (originally 124) at offset 456"
647        );
648
649        assert_eq!(
650            format!("{err:?}"),
651            "Corrupt(BlockRead { block_index: 123, original_block_index: 124, offset_within_block: 456, read_len: 789 })"
652        );
653    }
654
655    /// Test the `Display` and `Debug` impls for an `Incompatible` error.
656    ///
657    /// Only one `IncompatibleKind` variant is tested, the focus of the test
658    /// is the formatting of the nested error type:
659    /// `Ext4Error::Incompatible(Incompatible(IncompatibleKind))`
660    #[test]
661    fn test_incompatible_format() {
662        let err: Ext4Error = IncompatibleKind::DirectoryHash(123).into();
663
664        assert_eq!(
665            format!("{err}"),
666            "incompatible filesystem: unsupported directory hash algorithm: 123"
667        );
668
669        assert_eq!(format!("{err:?}"), "Incompatible(DirectoryHash(123))");
670    }
671}