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