ext4_view/iters/
read_dir.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::Ext4;
10use crate::block_index::FsBlockIndex;
11use crate::checksum::Checksum;
12use crate::dir_block::DirBlock;
13use crate::dir_entry::DirEntry;
14use crate::error::{CorruptKind, Ext4Error};
15use crate::inode::{Inode, InodeFlags, InodeIndex};
16use crate::iters::file_blocks::FileBlocks;
17use crate::path::PathBuf;
18use alloc::rc::Rc;
19use alloc::vec;
20use alloc::vec::Vec;
21use core::fmt::{self, Debug, Formatter};
22
23/// Iterator over each [`DirEntry`] in a directory inode.
24pub struct ReadDir {
25    fs: Ext4,
26
27    /// Path of the directory. This is stored in an `Rc` so that it can
28    /// be shared with each `DirEntry` without cloning the path data.
29    ///
30    /// Note that this path may be empty, e.g. if `read_dir` was called
31    /// with an inode rather than a path.
32    path: Rc<PathBuf>,
33
34    /// Iterator over the blocks of the directory.
35    file_blocks: FileBlocks,
36
37    /// Current absolute block index, or `None` if the next block needs
38    /// to be fetched.
39    block_index: Option<FsBlockIndex>,
40
41    /// Whether this is the first block in the file.
42    is_first_block: bool,
43
44    /// The current block's data.
45    block: Vec<u8>,
46
47    /// The current byte offset within the block data.
48    offset_within_block: usize,
49
50    /// Whether the iterator is done (calls to `Iterator::next` will
51    /// return `None`).
52    is_done: bool,
53
54    /// Whether the directory has an htree for fast lookups. The
55    /// iterator doesn't directly use the htree, but it affects which
56    /// blocks have checksums.
57    has_htree: bool,
58
59    /// Initial checksum using values from the directory's inode. This
60    /// serves as the seed for directory block checksums.
61    checksum_base: Checksum,
62
63    /// Inode of the directory. Just used for error reporting.
64    inode: InodeIndex,
65}
66
67impl ReadDir {
68    pub(crate) fn new(
69        fs: Ext4,
70        inode: &Inode,
71        path: PathBuf,
72    ) -> Result<Self, Ext4Error> {
73        let has_htree = inode.flags.contains(InodeFlags::DIRECTORY_HTREE);
74
75        if inode.flags.contains(InodeFlags::DIRECTORY_ENCRYPTED) {
76            return Err(Ext4Error::Encrypted);
77        }
78
79        Ok(Self {
80            fs: fs.clone(),
81            path: Rc::new(path),
82            file_blocks: FileBlocks::new(fs.clone(), inode)?,
83            block_index: None,
84            is_first_block: true,
85            block: vec![0; fs.0.superblock.block_size.to_usize()],
86            offset_within_block: 0,
87            is_done: false,
88            has_htree,
89            checksum_base: inode.checksum_base.clone(),
90            inode: inode.index,
91        })
92    }
93
94    fn next_impl(&mut self) -> Result<Option<DirEntry>, Ext4Error> {
95        // Get the block index, or get the next one if not set.
96        let block_index = if let Some(block_index) = self.block_index {
97            block_index
98        } else {
99            match self.file_blocks.next() {
100                Some(Ok(block_index)) => {
101                    self.block_index = Some(block_index);
102                    self.offset_within_block = 0;
103
104                    block_index
105                }
106                Some(Err(err)) => return Err(err),
107                None => {
108                    self.is_done = true;
109                    return Ok(None);
110                }
111            }
112        };
113
114        // If a block has been fully processed, move to the next block
115        // on the next iteration.
116        let block_size = self.fs.0.superblock.block_size;
117        if self.offset_within_block >= block_size {
118            self.is_first_block = false;
119            self.block_index = None;
120            return Ok(None);
121        }
122
123        // If at the start of a new block, read it and verify the checksum.
124        if self.offset_within_block == 0 {
125            DirBlock {
126                fs: &self.fs,
127                dir_inode: self.inode,
128                block_index,
129                is_first: self.is_first_block,
130                has_htree: self.has_htree,
131                checksum_base: self.checksum_base.clone(),
132            }
133            .read(&mut self.block)?;
134        }
135
136        let (entry, entry_size) = DirEntry::from_bytes(
137            self.fs.clone(),
138            &self.block[self.offset_within_block..],
139            self.inode,
140            self.path.clone(),
141        )?;
142
143        self.offset_within_block = self
144            .offset_within_block
145            .checked_add(entry_size)
146            .ok_or(CorruptKind::DirEntry(self.inode))?;
147
148        Ok(entry)
149    }
150}
151
152impl Debug for ReadDir {
153    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
154        // Only include the path field. This matches the Debug impl for
155        // `std::fs::ReadDir`.
156        write!(f, r#"ReadDir("{:?}")"#, self.path)
157    }
158}
159
160// In pseudocode, here's what the iterator is doing:
161//
162// for block in file {
163//   verify_checksum(block);
164//   for dir_entry in block {
165//     yield dir_entry;
166//   }
167// }
168impl_result_iter!(ReadDir, DirEntry);
169
170#[cfg(feature = "std")]
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::test_util::load_test_disk1;
175
176    #[test]
177    fn test_read_dir() {
178        let fs = load_test_disk1();
179        let root_inode = fs.read_root_inode().unwrap();
180        let root_path = crate::PathBuf::new("/");
181
182        // Use the iterator to get all DirEntries in the root directory.
183        let entries: Vec<_> = ReadDir::new(fs, &root_inode, root_path)
184            .unwrap()
185            .map(|e| e.unwrap())
186            .collect();
187
188        // Check for a few expected entries.
189        assert!(entries.iter().any(|e| e.file_name() == "."));
190        assert!(entries.iter().any(|e| e.file_name() == ".."));
191        assert!(entries.iter().any(|e| e.file_name() == "empty_file"));
192        assert!(entries.iter().any(|e| e.file_name() == "empty_dir"));
193
194        // Check for something that does not exist.
195        assert!(!entries.iter().any(|e| e.file_name() == "does_not_exist"));
196    }
197}