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