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