Skip to main content

ext4_view/
lib.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
9//! This crate provides read-only access to [ext4] filesystems. It also
10//! works with [ext2] filesystems.
11//!
12//! The main entry point is the [`Ext4`] struct.
13//!
14//! [ext2]: https://en.wikipedia.org/wiki/Ext2
15//! [ext4]: https://en.wikipedia.org/wiki/Ext4
16//!
17//! # Example
18//!
19//! This example reads the filesystem data from a byte vector, then
20//! looks at files and directories in the filesystem.
21//!
22//! ```
23//! use ext4_view::{Ext4, Ext4Error, Metadata};
24//!
25//! fn in_memory_example(fs_data: Vec<u8>) -> Result<(), Ext4Error> {
26//!     let fs = Ext4::load(Box::new(fs_data)).unwrap();
27//!
28//!     let path = "/some/file";
29//!
30//!     // Read a file's contents.
31//!     let file_data: Vec<u8> = fs.read(path)?;
32//!
33//!     // Read a file's contents as a string.
34//!     let file_str: String = fs.read_to_string(path)?;
35//!
36//!     // Check whether a path exists.
37//!     let exists: bool = fs.exists(path)?;
38//!
39//!     // Get metadata (file type, permissions, etc).
40//!     let metadata: Metadata = fs.metadata(path)?;
41//!
42//!     // Print each entry in a directory.
43//!     for entry in fs.read_dir("/some/dir")? {
44//!         let entry = entry?;
45//!         println!("{}", entry.path().display());
46//!     }
47//!
48//!     Ok(())
49//! }
50//! ```
51//! # Loading a filesystem
52//!
53//! Call [`Ext4::load`] to load a filesystem. The source data can be
54//! anything that implements the [`Ext4Read`] trait. The simplest form
55//! of source data is a `Vec<u8>` containing the whole filesystem.
56//!
57//! If the `std` feature is enabled, [`Ext4Read`] is implemented for
58//! [`std::fs::File`]. As a shortcut, you can also use
59//! [`Ext4::load_from_path`] to open a path and read the filesystem from
60//! it.
61//!
62//! For other cases, implement [`Ext4Read`] for your data source. This
63//! trait has a single method which reads bytes into a byte slice.
64//!
65//! Note that the underlying data should never be changed while the
66//! filesystem is in use.
67//!
68//! # Paths
69//!
70//! Paths in the filesystem are represented by [`Path`] and
71//! [`PathBuf`]. These types are similar to the types of the same names
72//! in [`std::path`].
73//!
74//! Functions that take a path as input accept a variety of types
75//! including strings.
76//!
77//! # Errors
78//!
79//! Most functions return [`Ext4Error`] on failure. This type is broadly
80//! similar to [`std::io::Error`], with a few notable additions:
81//! * Errors that come from the underlying reader are returned as
82//!   [`Ext4Error::Io`].
83//! * If the filesystem is corrupt in some way, [`Ext4Error::Corrupt`]
84//!   is returned.
85//! * If the filesystem can't be read due to a limitation of the
86//!   library, [`Ext4Error::Incompatible`] is returned. Please [file a
87//!   bug][issues] if you encounter an incompatibility so we know to
88//!   prioritize a fix!
89//!
90//! Some functions list specific errors that may occur. These lists are
91//! not exhaustive; calling code should be prepared to handle other
92//! errors such as [`Ext4Error::Io`].
93//!
94//! [issues]: https://github.com/nicholasbishop/ext4-view-rs/issues
95
96#![cfg_attr(not(any(feature = "std", test)), no_std)]
97#![cfg_attr(docsrs, feature(doc_cfg))]
98#![forbid(unsafe_code)]
99#![warn(
100    clippy::arithmetic_side_effects,
101    clippy::as_conversions,
102    clippy::must_use_candidate,
103    clippy::use_self
104)]
105#![warn(missing_docs)]
106#![warn(unreachable_pub)]
107
108extern crate alloc;
109
110mod block_cache;
111mod block_group;
112mod block_index;
113mod block_size;
114mod checksum;
115mod dir;
116mod dir_block;
117mod dir_entry;
118mod dir_entry_hash;
119mod dir_htree;
120mod error;
121mod extent;
122mod features;
123mod file;
124mod file_type;
125mod format;
126mod inode;
127mod iters;
128mod journal;
129mod label;
130mod metadata;
131mod path;
132mod reader;
133mod resolve;
134mod superblock;
135mod util;
136mod uuid;
137
138#[cfg(all(test, feature = "std"))]
139mod test_util;
140
141use alloc::boxed::Box;
142use alloc::rc::Rc;
143use alloc::string::String;
144use alloc::vec;
145use alloc::vec::Vec;
146use block_cache::BlockCache;
147use block_group::BlockGroupDescriptor;
148use block_index::FsBlockIndex;
149use core::cell::RefCell;
150use core::fmt::{self, Debug, Formatter};
151use error::CorruptKind;
152use features::ReadOnlyCompatibleFeatures;
153use inode::{Inode, InodeIndex};
154use journal::Journal;
155use resolve::FollowSymlinks;
156use superblock::Superblock;
157use util::usize_from_u32;
158
159pub use dir_entry::{DirEntry, DirEntryName, DirEntryNameError};
160pub use error::{Corrupt, Ext4Error, Incompatible};
161pub use features::IncompatibleFeatures;
162pub use file::File;
163pub use file_type::FileType;
164pub use format::BytesDisplay;
165pub use iters::read_dir::ReadDir;
166pub use label::Label;
167pub use metadata::Metadata;
168pub use path::{Component, Components, Path, PathBuf, PathError};
169#[cfg(feature = "async")]
170pub use reader::Ext4ReadAsync;
171pub use reader::{Ext4Read, MemIoError};
172pub use uuid::Uuid;
173
174struct Ext4Inner {
175    superblock: Superblock,
176    block_group_descriptors: Vec<BlockGroupDescriptor>,
177    journal: Journal,
178    block_cache: RefCell<BlockCache>,
179
180    /// Reader providing access to the underlying storage.
181    ///
182    /// Stored as `Box<dyn Ext4Read>` rather than a generic type to make
183    /// the `Ext4` type more convenient to pass around for users of the API.
184    ///
185    /// The `Ext4Read::read` method takes `&mut self`, because readers
186    /// like `std::fs::File` are mutable. However, the `Ext4` API is
187    /// logically const -- it provides read-only access to the
188    /// filesystem. So the box is wrapped in `RefCell` to allow the
189    /// mutable method to be called with an immutable `&Ext4Inner`
190    /// reference. `RefCell` enforces at runtime that only one mutable
191    /// borrow exists at a time.
192    reader: RefCell<Box<dyn Ext4Read>>,
193}
194
195/// Read-only access to an [ext4] filesystem.
196///
197/// [ext4]: https://en.wikipedia.org/wiki/Ext4
198#[derive(Clone)]
199pub struct Ext4(Rc<Ext4Inner>);
200
201impl Ext4 {
202    /// Load an `Ext4` instance from the given `reader`.
203    ///
204    /// This reads and validates the superblock, block group
205    /// descriptors, and journal. No other data is read.
206    pub fn load(mut reader: Box<dyn Ext4Read>) -> Result<Self, Ext4Error> {
207        // The first 1024 bytes are reserved for "weird" stuff like x86
208        // boot sectors.
209        let superblock_start = 1024;
210        let mut data = vec![0; Superblock::SIZE_IN_BYTES_ON_DISK];
211        reader
212            .read(superblock_start, &mut data)
213            .map_err(Ext4Error::Io)?;
214
215        let superblock = Superblock::from_bytes(&data)?;
216        let block_cache =
217            BlockCache::new(superblock.block_size, superblock.blocks_count)?;
218
219        let mut fs = Self(Rc::new(Ext4Inner {
220            block_group_descriptors: BlockGroupDescriptor::read_all(
221                &superblock,
222                &mut *reader,
223            )?,
224            reader: RefCell::new(reader),
225            superblock,
226            // Initialize with an empty journal, because loading the
227            // journal requires a valid `Ext4` object.
228            journal: Journal::empty(),
229            block_cache: RefCell::new(block_cache),
230        }));
231
232        // Load the actual journal, if present.
233        let journal = Journal::load(&fs)?;
234        Rc::get_mut(&mut fs.0).unwrap().journal = journal;
235
236        Ok(fs)
237    }
238
239    /// Load an `Ext4` instance from an async-first reader.
240    ///
241    /// This is an adapter around [`Ext4::load`]. The filesystem parser
242    /// remains synchronous, but each backing read is bridged through the
243    /// provided async reader.
244    #[cfg(feature = "async")]
245    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
246    pub async fn load_async(
247        reader: Box<dyn Ext4ReadAsync>,
248    ) -> Result<Self, Ext4Error> {
249        Self::load(Box::new(reader::BlockingAsyncReader::new(reader)))
250    }
251
252    /// Load an `Ext4` filesystem from the given `path`.
253    ///
254    /// This reads and validates the superblock and block group
255    /// descriptors. No other data is read.
256    #[cfg(feature = "std")]
257    pub fn load_from_path<P: AsRef<std::path::Path>>(
258        path: P,
259    ) -> Result<Self, Ext4Error> {
260        fn inner(path: &std::path::Path) -> Result<Ext4, Ext4Error> {
261            let file = std::fs::File::open(path)
262                .map_err(|e| Ext4Error::Io(Box::new(e)))?;
263            Ext4::load(Box::new(file))
264        }
265
266        inner(path.as_ref())
267    }
268
269    /// Get the filesystem label.
270    #[must_use]
271    pub fn label(&self) -> &Label {
272        &self.0.superblock.label
273    }
274
275    /// Get the filesystem UUID.
276    #[must_use]
277    pub fn uuid(&self) -> Uuid {
278        self.0.superblock.uuid
279    }
280
281    /// Return true if the filesystem has metadata checksums enabled,
282    /// false otherwise.
283    fn has_metadata_checksums(&self) -> bool {
284        self.0
285            .superblock
286            .read_only_compatible_features
287            .contains(ReadOnlyCompatibleFeatures::METADATA_CHECKSUMS)
288    }
289
290    /// Read the inode of the root `/` directory.
291    fn read_root_inode(&self) -> Result<Inode, Ext4Error> {
292        let root_inode_index = InodeIndex::new(2).unwrap();
293        Inode::read(self, root_inode_index)
294    }
295
296    /// Read data from a block.
297    ///
298    /// `block_index`: an absolute block within the filesystem.
299    ///
300    /// `offset_within_block`: the byte offset within the block to start
301    /// reading from.
302    ///
303    /// `dst`: byte buffer to read into. This also controls the length
304    /// of the read.
305    ///
306    /// The first 1024 bytes of the filesystem are reserved for
307    /// non-filesystem data. Reads are not allowed there.
308    ///
309    /// The read cannot cross block boundaries. This implies that:
310    /// * `offset_within_block < block_size`
311    /// * `offset_within_block + dst.len() <= block_size`
312    ///
313    /// If any of these conditions are violated, a `CorruptKind::BlockRead`
314    /// error is returned.
315    fn read_from_block(
316        &self,
317        original_block_index: FsBlockIndex,
318        offset_within_block: u32,
319        dst: &mut [u8],
320    ) -> Result<(), Ext4Error> {
321        let block_index = self.0.journal.map_block_index(original_block_index);
322
323        let err = || {
324            Ext4Error::from(CorruptKind::BlockRead {
325                block_index,
326                original_block_index,
327                offset_within_block,
328                read_len: dst.len(),
329            })
330        };
331
332        // The first 1024 bytes are reserved for non-filesystem
333        // data. This conveniently allows for something like a null
334        // pointer check.
335        if block_index == 0 && offset_within_block < 1024 {
336            return Err(err());
337        }
338
339        // Check the block index.
340        if block_index >= self.0.superblock.blocks_count {
341            return Err(err());
342        }
343
344        // The start of the read must be less than the block size.
345        let block_size = self.0.superblock.block_size;
346        if offset_within_block >= block_size {
347            return Err(err());
348        }
349
350        // The end of the read must be less than or equal to the block size.
351        let read_end = usize_from_u32(offset_within_block)
352            .checked_add(dst.len())
353            .ok_or_else(err)?;
354        if read_end > block_size {
355            return Err(err());
356        }
357
358        let mut block_cache = self.0.block_cache.borrow_mut();
359        let cached_block = block_cache.get_or_insert_blocks(
360            block_index,
361            |buf: &mut [u8]| {
362                // Get the absolute byte to start reading from.
363                let start_byte = block_index
364                    .checked_mul(block_size.to_u64())
365                    .ok_or_else(err)?;
366                self.0
367                    .reader
368                    .borrow_mut()
369                    .read(start_byte, buf)
370                    .map_err(Ext4Error::Io)
371            },
372        )?;
373
374        dst.copy_from_slice(
375            &cached_block[usize_from_u32(offset_within_block)..read_end],
376        );
377
378        Ok(())
379    }
380
381    /// Read the entire contents of a file into a `Vec<u8>`.
382    ///
383    /// Holes are filled with zero.
384    ///
385    /// Fails with `FileTooLarge` if the size of the file is too large
386    /// to fit in a [`usize`].
387    fn read_inode_file(&self, inode: &Inode) -> Result<Vec<u8>, Ext4Error> {
388        // Get the file size and initialize the output vector.
389        let file_size_in_bytes = usize::try_from(inode.metadata.size_in_bytes)
390            .map_err(|_| Ext4Error::FileTooLarge)?;
391        let mut dst = vec![0; file_size_in_bytes];
392
393        // Use `File` to read the data in chunks.
394        let mut file = File::open_inode(self, inode.clone())?;
395        let mut remaining = dst.as_mut();
396        loop {
397            let bytes_read = file.read_bytes(remaining)?;
398            if bytes_read == 0 {
399                break;
400            }
401            remaining = &mut remaining[bytes_read..];
402        }
403        Ok(dst)
404    }
405
406    /// Follow a path to get an inode.
407    fn path_to_inode(
408        &self,
409        path: Path<'_>,
410        follow: FollowSymlinks,
411    ) -> Result<Inode, Ext4Error> {
412        resolve::resolve_path(self, path, follow).map(|v| v.0)
413    }
414}
415
416/// These methods mirror the [`std::fs`][stdfs] API.
417///
418/// [stdfs]: https://doc.rust-lang.org/std/fs/index.html
419impl Ext4 {
420    /// Get the canonical, absolute form of a path with all intermediate
421    /// components normalized and symbolic links resolved.
422    ///
423    /// # Errors
424    ///
425    /// An error will be returned if:
426    /// * `path` is not absolute.
427    /// * `path` does not exist.
428    ///
429    /// This is not an exhaustive list of errors, see the
430    /// [crate documentation](crate#errors).
431    pub fn canonicalize<'p, P>(&self, path: P) -> Result<PathBuf, Ext4Error>
432    where
433        P: TryInto<Path<'p>>,
434    {
435        let path = path.try_into().map_err(|_| Ext4Error::MalformedPath)?;
436        resolve::resolve_path(self, path, FollowSymlinks::All).map(|v| v.1)
437    }
438
439    /// Open the file at `path`.
440    ///
441    /// # Errors
442    ///
443    /// An error will be returned if:
444    /// * `path` is not absolute.
445    /// * `path` does not exist.
446    /// * `path` is a directory or special file type.
447    ///
448    /// This is not an exhaustive list of errors, see the
449    /// [crate documentation](crate#errors).
450    pub fn open<'p, P>(&self, path: P) -> Result<File, Ext4Error>
451    where
452        P: TryInto<Path<'p>>,
453    {
454        File::open(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
455    }
456
457    /// Read the entire contents of a file as raw bytes.
458    ///
459    /// # Errors
460    ///
461    /// An error will be returned if:
462    /// * `path` is not absolute.
463    /// * `path` does not exist.
464    /// * `path` is a directory or special file type.
465    ///
466    /// This is not an exhaustive list of errors, see the
467    /// [crate documentation](crate#errors).
468    pub fn read<'p, P>(&self, path: P) -> Result<Vec<u8>, Ext4Error>
469    where
470        P: TryInto<Path<'p>>,
471    {
472        fn inner(fs: &Ext4, path: Path<'_>) -> Result<Vec<u8>, Ext4Error> {
473            let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
474
475            if inode.metadata.is_dir() {
476                return Err(Ext4Error::IsADirectory);
477            }
478            if !inode.metadata.file_type.is_regular_file() {
479                return Err(Ext4Error::IsASpecialFile);
480            }
481
482            fs.read_inode_file(&inode)
483        }
484
485        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
486    }
487
488    /// Read the entire contents of a file as a string.
489    ///
490    /// # Errors
491    ///
492    /// An error will be returned if:
493    /// * `path` is not absolute.
494    /// * `path` does not exist.
495    /// * `path` is a directory or special file type.
496    ///
497    /// This is not an exhaustive list of errors, see the
498    /// [crate documentation](crate#errors).
499    pub fn read_to_string<'p, P>(&self, path: P) -> Result<String, Ext4Error>
500    where
501        P: TryInto<Path<'p>>,
502    {
503        fn inner(fs: &Ext4, path: Path<'_>) -> Result<String, Ext4Error> {
504            let content = fs.read(path)?;
505            String::from_utf8(content).map_err(|_| Ext4Error::NotUtf8)
506        }
507
508        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
509    }
510
511    /// Get the target of a symbolic link.
512    ///
513    /// The final component of `path` must be a symlink. If the path
514    /// contains any symlinks in components prior to the end, they will
515    /// be fully resolved as normal.
516    ///
517    /// # Errors
518    ///
519    /// An error will be returned if:
520    /// * `path` is not absolute.
521    /// * The final component of `path` is not a symlink.
522    ///
523    /// This is not an exhaustive list of errors, see the
524    /// [crate documentation](crate#errors).
525    pub fn read_link<'p, P>(&self, path: P) -> Result<PathBuf, Ext4Error>
526    where
527        P: TryInto<Path<'p>>,
528    {
529        fn inner(fs: &Ext4, path: Path<'_>) -> Result<PathBuf, Ext4Error> {
530            let inode =
531                fs.path_to_inode(path, FollowSymlinks::ExcludeFinalComponent)?;
532            inode.symlink_target(fs)
533        }
534
535        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
536    }
537
538    /// Get an iterator over the entries in a directory.
539    ///
540    /// # Errors
541    ///
542    /// An error will be returned if:
543    /// * `path` is not absolute.
544    /// * `path` does not exist.
545    /// * `path` is not a directory.
546    ///
547    /// This is not an exhaustive list of errors, see the
548    /// [crate documentation](crate#errors).
549    pub fn read_dir<'p, P>(&self, path: P) -> Result<ReadDir, Ext4Error>
550    where
551        P: TryInto<Path<'p>>,
552    {
553        fn inner(fs: &Ext4, path: Path<'_>) -> Result<ReadDir, Ext4Error> {
554            let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
555
556            if !inode.metadata.is_dir() {
557                return Err(Ext4Error::NotADirectory);
558            }
559
560            ReadDir::new(fs.clone(), &inode, path.into())
561        }
562
563        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
564    }
565
566    /// Check if `path` exists.
567    ///
568    /// Returns `Ok(true)` if `path` exists, or `Ok(false)` if it does
569    /// not exist.
570    ///
571    /// # Errors
572    ///
573    /// An error will be returned if:
574    /// * `path` is not absolute.
575    ///
576    /// This is not an exhaustive list of errors, see the
577    /// [crate documentation](crate#errors).
578    pub fn exists<'p, P>(&self, path: P) -> Result<bool, Ext4Error>
579    where
580        P: TryInto<Path<'p>>,
581    {
582        fn inner(fs: &Ext4, path: Path<'_>) -> Result<bool, Ext4Error> {
583            match fs.path_to_inode(path, FollowSymlinks::All) {
584                Ok(_) => Ok(true),
585                Err(Ext4Error::NotFound) => Ok(false),
586                Err(err) => Err(err),
587            }
588        }
589
590        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
591    }
592
593    /// Get [`Metadata`] for `path`.
594    ///
595    /// # Errors
596    ///
597    /// An error will be returned if:
598    /// * `path` is not absolute.
599    /// * `path` does not exist.
600    ///
601    /// This is not an exhaustive list of errors, see the
602    /// [crate documentation](crate#errors).
603    pub fn metadata<'p, P>(&self, path: P) -> Result<Metadata, Ext4Error>
604    where
605        P: TryInto<Path<'p>>,
606    {
607        fn inner(fs: &Ext4, path: Path<'_>) -> Result<Metadata, Ext4Error> {
608            let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
609            Ok(inode.metadata)
610        }
611
612        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
613    }
614
615    /// Get [`Metadata`] for `path`.
616    ///
617    /// If the final component of `path` is a symlink, information about
618    /// the symlink itself will be returned, not the symlink's
619    /// targets. Any other symlink components of `path` are resolved as
620    /// normal.
621    ///
622    /// # Errors
623    ///
624    /// An error will be returned if:
625    /// * `path` is not absolute.
626    /// * `path` does not exist.
627    ///
628    /// This is not an exhaustive list of errors, see the
629    /// [crate documentation](crate#errors).
630    pub fn symlink_metadata<'p, P>(
631        &self,
632        path: P,
633    ) -> Result<Metadata, Ext4Error>
634    where
635        P: TryInto<Path<'p>>,
636    {
637        fn inner(fs: &Ext4, path: Path<'_>) -> Result<Metadata, Ext4Error> {
638            let inode =
639                fs.path_to_inode(path, FollowSymlinks::ExcludeFinalComponent)?;
640            Ok(inode.metadata)
641        }
642
643        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
644    }
645}
646
647impl Debug for Ext4 {
648    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
649        // Exclude the reader field, which does not impl Debug. Even if
650        // it did, it could be annoying to print out (e.g. if the reader
651        // is a Vec it might contain many megabytes of data).
652        f.debug_struct("Ext4")
653            .field("superblock", &self.0.superblock)
654            .field("block_group_descriptors", &self.0.block_group_descriptors)
655            .finish_non_exhaustive()
656    }
657}
658
659#[cfg(feature = "std")]
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use test_util::load_test_disk1;
664
665    #[test]
666    fn test_load_errors() {
667        // Not enough data.
668        assert!(matches!(
669            Ext4::load(Box::new(vec![])).unwrap_err(),
670            Ext4Error::Io(_)
671        ));
672
673        // Invalid superblock.
674        assert_eq!(
675            Ext4::load(Box::new(vec![0; 2048])).unwrap_err(),
676            CorruptKind::SuperblockMagic
677        );
678
679        // Not enough data to read the block group descriptors.
680        let mut fs_data = vec![0; 2048];
681        fs_data[1024..2048]
682            .copy_from_slice(include_bytes!("../test_data/raw_superblock.bin"));
683        assert!(matches!(
684            Ext4::load(Box::new(fs_data.clone())).unwrap_err(),
685            Ext4Error::Io(_)
686        ));
687
688        // Invalid block group descriptor checksum.
689        fs_data.resize(3048usize, 0u8);
690        assert_eq!(
691            Ext4::load(Box::new(fs_data.clone())).unwrap_err(),
692            CorruptKind::BlockGroupDescriptorChecksum(0)
693        );
694    }
695
696    /// Test that loading the data from
697    /// https://github.com/nicholasbishop/ext4-view-rs/issues/280 does not
698    /// panic.
699    #[test]
700    fn test_invalid_ext4_data() {
701        // Fill in zeros for the first 1024 bytes, then add the test data.
702        let mut data = vec![0; 1024];
703        data.extend(include_bytes!("../test_data/not_ext4.bin"));
704
705        assert_eq!(
706            Ext4::load(Box::new(data)).unwrap_err(),
707            CorruptKind::InvalidBlockSize
708        );
709    }
710
711    fn block_read_error(
712        block_index: FsBlockIndex,
713        offset_within_block: u32,
714        read_len: usize,
715    ) -> CorruptKind {
716        CorruptKind::BlockRead {
717            block_index,
718            original_block_index: block_index,
719            offset_within_block,
720            read_len,
721        }
722    }
723
724    /// Test that reading from the first 1024 bytes of the file fails.
725    #[test]
726    fn test_read_from_block_first_1024() {
727        let fs = load_test_disk1();
728        let mut dst = vec![0; 1];
729        assert_eq!(
730            fs.read_from_block(0, 1023, &mut dst).unwrap_err(),
731            block_read_error(0, 1023, 1),
732        );
733    }
734
735    /// Test that reading past the last block of the file fails.
736    #[test]
737    fn test_read_from_block_past_file_end() {
738        let fs = load_test_disk1();
739        let mut dst = vec![0; 1024];
740        assert_eq!(
741            fs.read_from_block(999_999_999, 0, &mut dst).unwrap_err(),
742            block_read_error(999_999_999, 0, 1024),
743        );
744    }
745
746    /// Test that reading at an offset >= the block size fails.
747    #[test]
748    fn test_read_from_block_invalid_offset() {
749        let fs = load_test_disk1();
750        let mut dst = vec![0; 1024];
751        assert_eq!(
752            fs.read_from_block(1, 1024, &mut dst).unwrap_err(),
753            block_read_error(1, 1024, 1024),
754        );
755    }
756
757    /// Test that reading past the end of the block fails.
758    #[test]
759    fn test_read_from_block_past_block_end() {
760        let fs = load_test_disk1();
761        let mut dst = vec![0; 25];
762        assert_eq!(
763            fs.read_from_block(1, 1000, &mut dst).unwrap_err(),
764            block_read_error(1, 1000, 25),
765        );
766    }
767
768    #[test]
769    fn test_path_to_inode() {
770        let fs = load_test_disk1();
771
772        let follow = FollowSymlinks::All;
773
774        let inode = fs
775            .path_to_inode(Path::try_from("/").unwrap(), follow)
776            .unwrap();
777        assert_eq!(inode.index.get(), 2);
778
779        // Successful lookup.
780        assert!(
781            fs.path_to_inode(Path::try_from("/empty_file").unwrap(), follow)
782                .is_ok()
783        );
784
785        // Successful lookup with a "." component.
786        assert!(
787            fs.path_to_inode(Path::try_from("/./empty_file").unwrap(), follow)
788                .is_ok()
789        );
790
791        // Successful lookup with a ".." component.
792        let inode = fs
793            .path_to_inode(Path::try_from("/empty_dir/..").unwrap(), follow)
794            .unwrap();
795        assert_eq!(inode.index.get(), 2);
796
797        // Successful lookup with symlink.
798        assert!(
799            fs.path_to_inode(Path::try_from("/sym_simple").unwrap(), follow)
800                .is_ok()
801        );
802
803        // Error: not an absolute path.
804        assert!(
805            fs.path_to_inode(Path::try_from("empty_file").unwrap(), follow)
806                .is_err()
807        );
808
809        // Error: invalid child of a valid directory.
810        assert!(
811            fs.path_to_inode(
812                Path::try_from("/empty_dir/does_not_exist").unwrap(),
813                follow
814            )
815            .is_err()
816        );
817
818        // Error: attempted to lookup child of a regular file.
819        assert!(
820            fs.path_to_inode(
821                Path::try_from("/empty_file/does_not_exist").unwrap(),
822                follow
823            )
824            .is_err()
825        );
826
827        // TODO: add deeper paths to the test disk and test here.
828    }
829}