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_auto_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};
169pub use reader::{Ext4Read, MemIoError};
170pub use uuid::Uuid;
171
172struct Ext4Inner {
173    superblock: Superblock,
174    block_group_descriptors: Vec<BlockGroupDescriptor>,
175    journal: Journal,
176    block_cache: RefCell<BlockCache>,
177
178    /// Reader providing access to the underlying storage.
179    ///
180    /// Stored as `Box<dyn Ext4Read>` rather than a generic type to make
181    /// the `Ext4` type more convenient to pass around for users of the API.
182    ///
183    /// The `Ext4Read::read` method takes `&mut self`, because readers
184    /// like `std::fs::File` are mutable. However, the `Ext4` API is
185    /// logically const -- it provides read-only access to the
186    /// filesystem. So the box is wrapped in `RefCell` to allow the
187    /// mutable method to be called with an immutable `&Ext4Inner`
188    /// reference. `RefCell` enforces at runtime that only one mutable
189    /// borrow exists at a time.
190    reader: RefCell<Box<dyn Ext4Read>>,
191}
192
193/// Read-only access to an [ext4] filesystem.
194///
195/// [ext4]: https://en.wikipedia.org/wiki/Ext4
196#[derive(Clone)]
197pub struct Ext4(Rc<Ext4Inner>);
198
199impl Ext4 {
200    /// Load an `Ext4` instance from the given `reader`.
201    ///
202    /// This reads and validates the superblock, block group
203    /// descriptors, and journal. No other data is read.
204    pub fn load(mut reader: Box<dyn Ext4Read>) -> Result<Self, Ext4Error> {
205        // The first 1024 bytes are reserved for "weird" stuff like x86
206        // boot sectors.
207        let superblock_start = 1024;
208        let mut data = vec![0; Superblock::SIZE_IN_BYTES_ON_DISK];
209        reader
210            .read(superblock_start, &mut data)
211            .map_err(Ext4Error::Io)?;
212
213        let superblock = Superblock::from_bytes(&data)?;
214        let block_cache =
215            BlockCache::new(superblock.block_size, superblock.blocks_count)?;
216
217        let mut fs = Self(Rc::new(Ext4Inner {
218            block_group_descriptors: BlockGroupDescriptor::read_all(
219                &superblock,
220                &mut *reader,
221            )?,
222            reader: RefCell::new(reader),
223            superblock,
224            // Initialize with an empty journal, because loading the
225            // journal requires a valid `Ext4` object.
226            journal: Journal::empty(),
227            block_cache: RefCell::new(block_cache),
228        }));
229
230        // Load the actual journal, if present.
231        let journal = Journal::load(&fs)?;
232        Rc::get_mut(&mut fs.0).unwrap().journal = journal;
233
234        Ok(fs)
235    }
236
237    /// Load an `Ext4` filesystem from the given `path`.
238    ///
239    /// This reads and validates the superblock and block group
240    /// descriptors. No other data is read.
241    #[cfg(feature = "std")]
242    pub fn load_from_path<P: AsRef<std::path::Path>>(
243        path: P,
244    ) -> Result<Self, Ext4Error> {
245        fn inner(path: &std::path::Path) -> Result<Ext4, Ext4Error> {
246            let file = std::fs::File::open(path)
247                .map_err(|e| Ext4Error::Io(Box::new(e)))?;
248            Ext4::load(Box::new(file))
249        }
250
251        inner(path.as_ref())
252    }
253
254    /// Get the filesystem label.
255    #[must_use]
256    pub fn label(&self) -> &Label {
257        &self.0.superblock.label
258    }
259
260    /// Get the filesystem UUID.
261    #[must_use]
262    pub fn uuid(&self) -> Uuid {
263        self.0.superblock.uuid
264    }
265
266    /// Return true if the filesystem has metadata checksums enabled,
267    /// false otherwise.
268    fn has_metadata_checksums(&self) -> bool {
269        self.0
270            .superblock
271            .read_only_compatible_features
272            .contains(ReadOnlyCompatibleFeatures::METADATA_CHECKSUMS)
273    }
274
275    /// Read the inode of the root `/` directory.
276    fn read_root_inode(&self) -> Result<Inode, Ext4Error> {
277        let root_inode_index = InodeIndex::new(2).unwrap();
278        Inode::read(self, root_inode_index)
279    }
280
281    /// Read data from a block.
282    ///
283    /// `block_index`: an absolute block within the filesystem.
284    ///
285    /// `offset_within_block`: the byte offset within the block to start
286    /// reading from.
287    ///
288    /// `dst`: byte buffer to read into. This also controls the length
289    /// of the read.
290    ///
291    /// The first 1024 bytes of the filesystem are reserved for
292    /// non-filesystem data. Reads are not allowed there.
293    ///
294    /// The read cannot cross block boundaries. This implies that:
295    /// * `offset_within_block < block_size`
296    /// * `offset_within_block + dst.len() <= block_size`
297    ///
298    /// If any of these conditions are violated, a `CorruptKind::BlockRead`
299    /// error is returned.
300    fn read_from_block(
301        &self,
302        original_block_index: FsBlockIndex,
303        offset_within_block: u32,
304        dst: &mut [u8],
305    ) -> Result<(), Ext4Error> {
306        let block_index = self.0.journal.map_block_index(original_block_index);
307
308        let err = || {
309            Ext4Error::from(CorruptKind::BlockRead {
310                block_index,
311                original_block_index,
312                offset_within_block,
313                read_len: dst.len(),
314            })
315        };
316
317        // The first 1024 bytes are reserved for non-filesystem
318        // data. This conveniently allows for something like a null
319        // pointer check.
320        if block_index == 0 && offset_within_block < 1024 {
321            return Err(err());
322        }
323
324        // Check the block index.
325        if block_index >= self.0.superblock.blocks_count {
326            return Err(err());
327        }
328
329        // The start of the read must be less than the block size.
330        let block_size = self.0.superblock.block_size;
331        if offset_within_block >= block_size {
332            return Err(err());
333        }
334
335        // The end of the read must be less than or equal to the block size.
336        let read_end = usize_from_u32(offset_within_block)
337            .checked_add(dst.len())
338            .ok_or_else(err)?;
339        if read_end > block_size {
340            return Err(err());
341        }
342
343        let mut block_cache = self.0.block_cache.borrow_mut();
344        let cached_block = block_cache.get_or_insert_blocks(
345            block_index,
346            |buf: &mut [u8]| {
347                // Get the absolute byte to start reading from.
348                let start_byte = block_index
349                    .checked_mul(block_size.to_u64())
350                    .ok_or_else(err)?;
351                self.0
352                    .reader
353                    .borrow_mut()
354                    .read(start_byte, buf)
355                    .map_err(Ext4Error::Io)
356            },
357        )?;
358
359        dst.copy_from_slice(
360            &cached_block[usize_from_u32(offset_within_block)..read_end],
361        );
362
363        Ok(())
364    }
365
366    /// Read the entire contents of a file into a `Vec<u8>`.
367    ///
368    /// Holes are filled with zero.
369    ///
370    /// Fails with `FileTooLarge` if the size of the file is too large
371    /// to fit in a [`usize`].
372    fn read_inode_file(&self, inode: &Inode) -> Result<Vec<u8>, Ext4Error> {
373        // Get the file size and initialize the output vector.
374        let file_size_in_bytes = usize::try_from(inode.metadata.size_in_bytes)
375            .map_err(|_| Ext4Error::FileTooLarge)?;
376        let mut dst = vec![0; file_size_in_bytes];
377
378        // Use `File` to read the data in chunks.
379        let mut file = File::open_inode(self, inode.clone())?;
380        let mut remaining = dst.as_mut();
381        loop {
382            let bytes_read = file.read_bytes(remaining)?;
383            if bytes_read == 0 {
384                break;
385            }
386            remaining = &mut remaining[bytes_read..];
387        }
388        Ok(dst)
389    }
390
391    /// Follow a path to get an inode.
392    fn path_to_inode(
393        &self,
394        path: Path<'_>,
395        follow: FollowSymlinks,
396    ) -> Result<Inode, Ext4Error> {
397        resolve::resolve_path(self, path, follow).map(|v| v.0)
398    }
399}
400
401/// These methods mirror the [`std::fs`][stdfs] API.
402///
403/// [stdfs]: https://doc.rust-lang.org/std/fs/index.html
404impl Ext4 {
405    /// Get the canonical, absolute form of a path with all intermediate
406    /// components normalized and symbolic links resolved.
407    ///
408    /// # Errors
409    ///
410    /// An error will be returned if:
411    /// * `path` is not absolute.
412    /// * `path` does not exist.
413    ///
414    /// This is not an exhaustive list of errors, see the
415    /// [crate documentation](crate#errors).
416    pub fn canonicalize<'p, P>(&self, path: P) -> Result<PathBuf, Ext4Error>
417    where
418        P: TryInto<Path<'p>>,
419    {
420        let path = path.try_into().map_err(|_| Ext4Error::MalformedPath)?;
421        resolve::resolve_path(self, path, FollowSymlinks::All).map(|v| v.1)
422    }
423
424    /// Open the file at `path`.
425    ///
426    /// # Errors
427    ///
428    /// An error will be returned if:
429    /// * `path` is not absolute.
430    /// * `path` does not exist.
431    /// * `path` is a directory or special file type.
432    ///
433    /// This is not an exhaustive list of errors, see the
434    /// [crate documentation](crate#errors).
435    pub fn open<'p, P>(&self, path: P) -> Result<File, Ext4Error>
436    where
437        P: TryInto<Path<'p>>,
438    {
439        File::open(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
440    }
441
442    /// Read the entire contents of a file as raw bytes.
443    ///
444    /// # Errors
445    ///
446    /// An error will be returned if:
447    /// * `path` is not absolute.
448    /// * `path` does not exist.
449    /// * `path` is a directory or special file type.
450    ///
451    /// This is not an exhaustive list of errors, see the
452    /// [crate documentation](crate#errors).
453    pub fn read<'p, P>(&self, path: P) -> Result<Vec<u8>, Ext4Error>
454    where
455        P: TryInto<Path<'p>>,
456    {
457        fn inner(fs: &Ext4, path: Path<'_>) -> Result<Vec<u8>, Ext4Error> {
458            let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
459
460            if inode.metadata.is_dir() {
461                return Err(Ext4Error::IsADirectory);
462            }
463            if !inode.metadata.file_type.is_regular_file() {
464                return Err(Ext4Error::IsASpecialFile);
465            }
466
467            fs.read_inode_file(&inode)
468        }
469
470        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
471    }
472
473    /// Read the entire contents of a file as a string.
474    ///
475    /// # Errors
476    ///
477    /// An error will be returned if:
478    /// * `path` is not absolute.
479    /// * `path` does not exist.
480    /// * `path` is a directory or special file type.
481    ///
482    /// This is not an exhaustive list of errors, see the
483    /// [crate documentation](crate#errors).
484    pub fn read_to_string<'p, P>(&self, path: P) -> Result<String, Ext4Error>
485    where
486        P: TryInto<Path<'p>>,
487    {
488        fn inner(fs: &Ext4, path: Path<'_>) -> Result<String, Ext4Error> {
489            let content = fs.read(path)?;
490            String::from_utf8(content).map_err(|_| Ext4Error::NotUtf8)
491        }
492
493        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
494    }
495
496    /// Get the target of a symbolic link.
497    ///
498    /// The final component of `path` must be a symlink. If the path
499    /// contains any symlinks in components prior to the end, they will
500    /// be fully resolved as normal.
501    ///
502    /// # Errors
503    ///
504    /// An error will be returned if:
505    /// * `path` is not absolute.
506    /// * The final component of `path` is not a symlink.
507    ///
508    /// This is not an exhaustive list of errors, see the
509    /// [crate documentation](crate#errors).
510    pub fn read_link<'p, P>(&self, path: P) -> Result<PathBuf, Ext4Error>
511    where
512        P: TryInto<Path<'p>>,
513    {
514        fn inner(fs: &Ext4, path: Path<'_>) -> Result<PathBuf, Ext4Error> {
515            let inode =
516                fs.path_to_inode(path, FollowSymlinks::ExcludeFinalComponent)?;
517            inode.symlink_target(fs)
518        }
519
520        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
521    }
522
523    /// Get an iterator over the entries in a directory.
524    ///
525    /// # Errors
526    ///
527    /// An error will be returned if:
528    /// * `path` is not absolute.
529    /// * `path` does not exist
530    /// * `path` is not a directory
531    ///
532    /// This is not an exhaustive list of errors, see the
533    /// [crate documentation](crate#errors).
534    pub fn read_dir<'p, P>(&self, path: P) -> Result<ReadDir, Ext4Error>
535    where
536        P: TryInto<Path<'p>>,
537    {
538        fn inner(fs: &Ext4, path: Path<'_>) -> Result<ReadDir, Ext4Error> {
539            let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
540
541            if !inode.metadata.is_dir() {
542                return Err(Ext4Error::NotADirectory);
543            }
544
545            ReadDir::new(fs.clone(), &inode, path.into())
546        }
547
548        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
549    }
550
551    /// Check if `path` exists.
552    ///
553    /// Returns `Ok(true)` if `path` exists, or `Ok(false)` if it does
554    /// not exist.
555    ///
556    /// # Errors
557    ///
558    /// An error will be returned if:
559    /// * `path` is not absolute.
560    ///
561    /// This is not an exhaustive list of errors, see the
562    /// [crate documentation](crate#errors).
563    pub fn exists<'p, P>(&self, path: P) -> Result<bool, Ext4Error>
564    where
565        P: TryInto<Path<'p>>,
566    {
567        fn inner(fs: &Ext4, path: Path<'_>) -> Result<bool, Ext4Error> {
568            match fs.path_to_inode(path, FollowSymlinks::All) {
569                Ok(_) => Ok(true),
570                Err(Ext4Error::NotFound) => Ok(false),
571                Err(err) => Err(err),
572            }
573        }
574
575        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
576    }
577
578    /// Get [`Metadata`] for `path`.
579    ///
580    /// # Errors
581    ///
582    /// An error will be returned if:
583    /// * `path` is not absolute.
584    /// * `path` does not exist.
585    ///
586    /// This is not an exhaustive list of errors, see the
587    /// [crate documentation](crate#errors).
588    pub fn metadata<'p, P>(&self, path: P) -> Result<Metadata, Ext4Error>
589    where
590        P: TryInto<Path<'p>>,
591    {
592        fn inner(fs: &Ext4, path: Path<'_>) -> Result<Metadata, Ext4Error> {
593            let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
594            Ok(inode.metadata)
595        }
596
597        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
598    }
599
600    /// Get [`Metadata`] for `path`.
601    ///
602    /// If the final component of `path` is a symlink, information about
603    /// the symlink itself will be returned, not the symlink's
604    /// targets. Any other symlink components of `path` are resolved as
605    /// normal.
606    ///
607    /// # Errors
608    ///
609    /// An error will be returned if:
610    /// * `path` is not absolute.
611    /// * `path` does not exist.
612    ///
613    /// This is not an exhaustive list of errors, see the
614    /// [crate documentation](crate#errors).
615    pub fn symlink_metadata<'p, P>(
616        &self,
617        path: P,
618    ) -> Result<Metadata, Ext4Error>
619    where
620        P: TryInto<Path<'p>>,
621    {
622        fn inner(fs: &Ext4, path: Path<'_>) -> Result<Metadata, Ext4Error> {
623            let inode =
624                fs.path_to_inode(path, FollowSymlinks::ExcludeFinalComponent)?;
625            Ok(inode.metadata)
626        }
627
628        inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
629    }
630}
631
632impl Debug for Ext4 {
633    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
634        // Exclude the reader field, which does not impl Debug. Even if
635        // it did, it could be annoying to print out (e.g. if the reader
636        // is a Vec it might contain many megabytes of data).
637        f.debug_struct("Ext4")
638            .field("superblock", &self.0.superblock)
639            .field("block_group_descriptors", &self.0.block_group_descriptors)
640            .finish_non_exhaustive()
641    }
642}
643
644#[cfg(feature = "std")]
645#[cfg(test)]
646mod tests {
647    use super::*;
648    use test_util::load_test_disk1;
649
650    #[test]
651    fn test_load_errors() {
652        // Not enough data.
653        assert!(matches!(
654            Ext4::load(Box::new(vec![])).unwrap_err(),
655            Ext4Error::Io(_)
656        ));
657
658        // Invalid superblock.
659        assert_eq!(
660            Ext4::load(Box::new(vec![0; 2048])).unwrap_err(),
661            CorruptKind::SuperblockMagic
662        );
663
664        // Not enough data to read the block group descriptors.
665        let mut fs_data = vec![0; 2048];
666        fs_data[1024..2048]
667            .copy_from_slice(include_bytes!("../test_data/raw_superblock.bin"));
668        assert!(matches!(
669            Ext4::load(Box::new(fs_data.clone())).unwrap_err(),
670            Ext4Error::Io(_)
671        ));
672
673        // Invalid block group descriptor checksum.
674        fs_data.resize(3048usize, 0u8);
675        assert_eq!(
676            Ext4::load(Box::new(fs_data.clone())).unwrap_err(),
677            CorruptKind::BlockGroupDescriptorChecksum(0)
678        );
679    }
680
681    /// Test that loading the data from
682    /// https://github.com/nicholasbishop/ext4-view-rs/issues/280 does not
683    /// panic.
684    #[test]
685    fn test_invalid_ext4_data() {
686        // Fill in zeros for the first 1024 bytes, then add the test data.
687        let mut data = vec![0; 1024];
688        data.extend(include_bytes!("../test_data/not_ext4.bin"));
689
690        assert_eq!(
691            Ext4::load(Box::new(data)).unwrap_err(),
692            CorruptKind::InvalidBlockSize
693        );
694    }
695
696    fn block_read_error(
697        block_index: FsBlockIndex,
698        offset_within_block: u32,
699        read_len: usize,
700    ) -> CorruptKind {
701        CorruptKind::BlockRead {
702            block_index,
703            original_block_index: block_index,
704            offset_within_block,
705            read_len,
706        }
707    }
708
709    /// Test that reading from the first 1024 bytes of the file fails.
710    #[test]
711    fn test_read_from_block_first_1024() {
712        let fs = load_test_disk1();
713        let mut dst = vec![0; 1];
714        assert_eq!(
715            fs.read_from_block(0, 1023, &mut dst).unwrap_err(),
716            block_read_error(0, 1023, 1),
717        );
718    }
719
720    /// Test that reading past the last block of the file fails.
721    #[test]
722    fn test_read_from_block_past_file_end() {
723        let fs = load_test_disk1();
724        let mut dst = vec![0; 1024];
725        assert_eq!(
726            fs.read_from_block(999_999_999, 0, &mut dst).unwrap_err(),
727            block_read_error(999_999_999, 0, 1024),
728        );
729    }
730
731    /// Test that reading at an offset >= the block size fails.
732    #[test]
733    fn test_read_from_block_invalid_offset() {
734        let fs = load_test_disk1();
735        let mut dst = vec![0; 1024];
736        assert_eq!(
737            fs.read_from_block(1, 1024, &mut dst).unwrap_err(),
738            block_read_error(1, 1024, 1024),
739        );
740    }
741
742    /// Test that reading past the end of the block fails.
743    #[test]
744    fn test_read_from_block_past_block_end() {
745        let fs = load_test_disk1();
746        let mut dst = vec![0; 25];
747        assert_eq!(
748            fs.read_from_block(1, 1000, &mut dst).unwrap_err(),
749            block_read_error(1, 1000, 25),
750        );
751    }
752
753    #[test]
754    fn test_path_to_inode() {
755        let fs = load_test_disk1();
756
757        let follow = FollowSymlinks::All;
758
759        let inode = fs
760            .path_to_inode(Path::try_from("/").unwrap(), follow)
761            .unwrap();
762        assert_eq!(inode.index.get(), 2);
763
764        // Successful lookup.
765        assert!(
766            fs.path_to_inode(Path::try_from("/empty_file").unwrap(), follow)
767                .is_ok()
768        );
769
770        // Successful lookup with a "." component.
771        assert!(
772            fs.path_to_inode(Path::try_from("/./empty_file").unwrap(), follow)
773                .is_ok()
774        );
775
776        // Successful lookup with a ".." component.
777        let inode = fs
778            .path_to_inode(Path::try_from("/empty_dir/..").unwrap(), follow)
779            .unwrap();
780        assert_eq!(inode.index.get(), 2);
781
782        // Successful lookup with symlink.
783        assert!(
784            fs.path_to_inode(Path::try_from("/sym_simple").unwrap(), follow)
785                .is_ok()
786        );
787
788        // Error: not an absolute path.
789        assert!(
790            fs.path_to_inode(Path::try_from("empty_file").unwrap(), follow)
791                .is_err()
792        );
793
794        // Error: invalid child of a valid directory.
795        assert!(
796            fs.path_to_inode(
797                Path::try_from("/empty_dir/does_not_exist").unwrap(),
798                follow
799            )
800            .is_err()
801        );
802
803        // Error: attempted to lookup child of a regular file.
804        assert!(
805            fs.path_to_inode(
806                Path::try_from("/empty_file/does_not_exist").unwrap(),
807                follow
808            )
809            .is_err()
810        );
811
812        // TODO: add deeper paths to the test disk and test here.
813    }
814}