Skip to main content

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