1#![forbid(unsafe_code)]
2use crate::error::{Ext4Error, Result};
3use crate::inode::InodeReader;
4use crate::ondisk::{parse_dir_block, DirEntry, FileType};
5use std::io::{Read, Seek};
6
7const MAX_SYMLINK_DEPTH: u32 = 40;
8
9pub struct DirReader<R: Read + Seek> {
10 inode_reader: InodeReader<R>,
11}
12
13impl<R: Read + Seek> DirReader<R> {
14 pub fn new(inode_reader: InodeReader<R>) -> Self {
16 Self { inode_reader }
17 }
18
19 pub fn inode_reader(&self) -> &InodeReader<R> {
21 &self.inode_reader
22 }
23
24 pub fn inode_reader_mut(&mut self) -> &mut InodeReader<R> {
26 &mut self.inode_reader
27 }
28
29 pub fn read_dir(&self, dir_ino: u64) -> Result<Vec<DirEntry>> {
34 let block_size = self.inode_reader.block_reader().block_size() as usize;
35 let data = self.inode_reader.read_inode_data(dir_ino)?;
36
37 let mut entries = Vec::new();
38 let mut offset = 0;
39 while offset < data.len() {
40 let end = (offset + block_size).min(data.len());
41 let chunk = &data[offset..end];
42 let block_entries = parse_dir_block(chunk);
43 for e in block_entries {
44 if e.inode != 0 {
45 entries.push(e);
46 }
47 }
48 offset += block_size;
49 }
50 Ok(entries)
51 }
52
53 pub fn lookup(&self, dir_ino: u64, name: &[u8]) -> Result<Option<u64>> {
57 let entries = self.read_dir(dir_ino)?;
58 for e in entries {
59 if e.name.as_slice() == name {
60 return Ok(Some(u64::from(e.inode)));
61 }
62 }
63 Ok(None)
64 }
65
66 pub fn resolve_path(&self, path: &str) -> Result<u64> {
72 self.resolve_path_inner(path, 0)
73 }
74
75 fn resolve_path_inner(&self, path: &str, depth: u32) -> Result<u64> {
76 if depth > MAX_SYMLINK_DEPTH {
77 return Err(Ext4Error::SymlinkLoop {
78 path: path.to_string(),
79 depth,
80 });
81 }
82
83 let mut cur_ino: u64 = 2;
85
86 let components: Vec<&str> = path
87 .trim_start_matches('/')
88 .split('/')
89 .filter(|c| !c.is_empty())
90 .collect();
91
92 for component in components {
93 let next = self
95 .lookup(cur_ino, component.as_bytes())?
96 .ok_or_else(|| Ext4Error::PathNotFound(path.to_string()))?;
97
98 let inode = self.inode_reader.read_inode(next)?;
100 if inode.file_type() == FileType::Symlink {
101 let target = self.read_link(next)?;
102 let target_str = String::from_utf8_lossy(&target).into_owned();
103 if target_str.starts_with('/') {
104 cur_ino = self.resolve_path_inner(&target_str, depth + 1)?;
106 } else {
107 cur_ino = self.resolve_relative_link(cur_ino, &target_str, depth + 1)?;
112 }
113 } else {
114 cur_ino = next;
115 }
116 }
117
118 Ok(cur_ino)
119 }
120
121 fn resolve_relative_link(&self, dir_ino: u64, target: &str, depth: u32) -> Result<u64> {
124 if depth > MAX_SYMLINK_DEPTH {
125 return Err(Ext4Error::SymlinkLoop {
126 path: target.to_string(),
127 depth,
128 });
129 }
130
131 let mut cur_ino = dir_ino;
132 let components: Vec<&str> = target.split('/').filter(|c| !c.is_empty()).collect();
133
134 for component in components {
135 match component {
136 "." => {}
137 ".." => {
138 if let Some(parent) = self.lookup(cur_ino, b"..")? {
139 cur_ino = parent;
140 }
141 }
142 name => {
143 let next = self
144 .lookup(cur_ino, name.as_bytes())?
145 .ok_or_else(|| Ext4Error::PathNotFound(target.to_string()))?;
146
147 let inode = self.inode_reader.read_inode(next)?;
148 if inode.file_type() == FileType::Symlink {
149 let link_target = self.read_link(next)?;
150 let link_str = String::from_utf8_lossy(&link_target).into_owned();
151 if link_str.starts_with('/') {
152 cur_ino = self.resolve_path_inner(&link_str, depth + 1)?;
153 } else {
154 cur_ino = self.resolve_relative_link(cur_ino, &link_str, depth + 1)?;
155 }
156 } else {
157 cur_ino = next;
158 }
159 }
160 }
161 }
162
163 Ok(cur_ino)
164 }
165
166 pub fn read_link(&self, ino: u64) -> Result<Vec<u8>> {
171 let inode = self.inode_reader.read_inode(ino)?;
172 if inode.file_type() != FileType::Symlink {
173 return Err(Ext4Error::NotASymlink(format!("inode {ino}")));
174 }
175 if inode.size <= 60 && !inode.uses_extents() {
177 let len = inode.size as usize;
178 return Ok(inode.i_block[..len].to_vec());
179 }
180 let data = self.inode_reader.read_inode_data(ino)?;
182 Ok(data)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::block::BlockReader;
190 use std::io::Cursor;
191
192 fn open_minimal() -> DirReader<Cursor<Vec<u8>>> {
193 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/minimal.img");
194 let data = std::fs::read(path).expect("minimal.img required");
195 let br = BlockReader::open(Cursor::new(data)).unwrap();
196 let ir = InodeReader::new(br);
197 DirReader::new(ir)
198 }
199
200 #[test]
201 fn read_root_directory() {
202 let r = open_minimal();
203 let entries = r.read_dir(2).unwrap();
204 let names: Vec<String> = entries
205 .iter()
206 .map(super::super::ondisk::dir_entry::DirEntry::name_str)
207 .collect();
208 assert!(names.contains(&".".to_string()));
209 assert!(names.contains(&"..".to_string()));
210 assert!(names.contains(&"hello.txt".to_string()));
211 assert!(names.contains(&"subdir".to_string()));
212 assert!(names.contains(&"lost+found".to_string()));
213 }
214
215 #[test]
216 fn lookup_file_in_root() {
217 let r = open_minimal();
218 let ino = r.lookup(2, b"hello.txt").unwrap();
219 assert!(ino.is_some());
220 assert!(ino.unwrap() > 0);
221 }
222
223 #[test]
224 fn lookup_nonexistent() {
225 let r = open_minimal();
226 let ino = r.lookup(2, b"nonexistent.txt").unwrap();
227 assert!(ino.is_none());
228 }
229
230 #[test]
231 fn resolve_path_root() {
232 let r = open_minimal();
233 assert_eq!(r.resolve_path("/").unwrap(), 2);
234 }
235
236 #[test]
237 fn resolve_path_file() {
238 let r = open_minimal();
239 let ino = r.resolve_path("/hello.txt").unwrap();
240 assert!(ino > 0);
241 }
242
243 #[test]
244 fn resolve_path_nested() {
245 let r = open_minimal();
246 let ino = r.resolve_path("/subdir/nested.txt").unwrap();
247 assert!(ino > 0);
248 }
249
250 #[test]
251 fn resolve_path_not_found() {
252 let r = open_minimal();
253 let err = r.resolve_path("/nonexistent").unwrap_err();
254 assert!(matches!(err, Ext4Error::PathNotFound(_)));
255 }
256
257 #[test]
258 fn read_file_content_via_path() {
259 let mut r = open_minimal();
260 let ino = r.resolve_path("/hello.txt").unwrap();
261 let data = r.inode_reader_mut().read_inode_data(ino).unwrap();
262 assert_eq!(data, b"Hello, ext4!");
263 }
264
265 fn open_forensic() -> Option<DirReader<Cursor<Vec<u8>>>> {
266 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
267 let data = std::fs::read(path).ok()?;
268 let br = BlockReader::open(Cursor::new(data)).ok()?;
269 let ir = InodeReader::new(br);
270 Some(DirReader::new(ir))
271 }
272
273 #[test]
274 fn inode_reader_accessor() {
275 let r = open_minimal();
276 let sb = r.inode_reader().block_reader().superblock();
278 assert!(sb.block_size > 0);
279 }
280
281 #[test]
282 fn resolve_absolute_symlink() {
283 let mut r = if let Some(r) = open_forensic() {
284 r
285 } else {
286 eprintln!("skip: forensic.img not found");
287 return;
288 };
289 let hello_ino = r.resolve_path("/hello.txt").unwrap();
291 let expected = r.inode_reader_mut().read_inode_data(hello_ino).unwrap();
292 let link_ino = r.resolve_path("/abs-link").unwrap();
294 let actual = r.inode_reader_mut().read_inode_data(link_ino).unwrap();
295 assert_eq!(actual, expected);
296 }
297
298 #[test]
299 fn resolve_relative_symlink() {
300 let mut r = if let Some(r) = open_forensic() {
301 r
302 } else {
303 eprintln!("skip: forensic.img not found");
304 return;
305 };
306 let hello_ino = r.resolve_path("/hello.txt").unwrap();
307 let expected = r.inode_reader_mut().read_inode_data(hello_ino).unwrap();
308 let link_ino = r.resolve_path("/rel-link").unwrap();
309 let actual = r.inode_reader_mut().read_inode_data(link_ino).unwrap();
310 assert_eq!(actual, expected);
311 }
312
313 #[test]
314 fn resolve_deep_symlink() {
315 let mut r = if let Some(r) = open_forensic() {
316 r
317 } else {
318 eprintln!("skip: forensic.img not found");
319 return;
320 };
321 let nested_ino = r.resolve_path("/subdir/nested.txt").unwrap();
322 let expected = r.inode_reader_mut().read_inode_data(nested_ino).unwrap();
323 let link_ino = r.resolve_path("/deep-link").unwrap();
324 let actual = r.inode_reader_mut().read_inode_data(link_ino).unwrap();
325 assert_eq!(actual, expected);
326 }
327
328 #[test]
329 fn resolve_updir_symlink() {
330 let mut r = if let Some(r) = open_forensic() {
331 r
332 } else {
333 eprintln!("skip: forensic.img not found");
334 return;
335 };
336 let hello_ino = r.resolve_path("/hello.txt").unwrap();
337 let expected = r.inode_reader_mut().read_inode_data(hello_ino).unwrap();
338 let link_ino = r.resolve_path("/linkdir/up-link").unwrap();
339 let actual = r.inode_reader_mut().read_inode_data(link_ino).unwrap();
340 assert_eq!(actual, expected);
341 }
342
343 #[test]
344 fn read_link_inline() {
345 let r = if let Some(r) = open_forensic() {
346 r
347 } else {
348 eprintln!("skip: forensic.img not found");
349 return;
350 };
351 let link_ino = r.lookup(2, b"abs-link").unwrap().unwrap();
352 let target = r.read_link(link_ino).unwrap();
353 let target_str = String::from_utf8_lossy(&target);
354 assert_eq!(target_str, "/hello.txt");
355 }
356
357 #[test]
358 fn read_link_not_a_symlink() {
359 let r = if let Some(r) = open_forensic() {
360 r
361 } else {
362 eprintln!("skip: forensic.img not found");
363 return;
364 };
365 let file_ino = r.lookup(2, b"hello.txt").unwrap().unwrap();
366 let err = r.read_link(file_ino).unwrap_err();
367 assert!(matches!(err, Ext4Error::NotASymlink(_)));
368 }
369
370 #[test]
371 fn resolve_nonexistent_through_symlink() {
372 let r = if let Some(r) = open_forensic() {
373 r
374 } else {
375 eprintln!("skip: forensic.img not found");
376 return;
377 };
378 let err = r.resolve_path("/nonexistent-link");
380 assert!(err.is_err());
382 assert!(matches!(err.unwrap_err(), Ext4Error::PathNotFound(_)));
383 }
384
385 #[test]
386 fn lookup_returns_none_for_missing_forensic() {
387 let r = if let Some(r) = open_forensic() {
388 r
389 } else {
390 eprintln!("skip: forensic.img not found");
391 return;
392 };
393 let result = r.lookup(2, b"does-not-exist.xyz").unwrap();
394 assert!(result.is_none());
395 }
396
397 #[test]
398 fn read_dir_handles_all_directory_types() {
399 let mut r = if let Some(r) = open_forensic() {
400 r
401 } else {
402 eprintln!("skip: forensic.img not found");
403 return;
404 };
405 let all_inodes = r.inode_reader_mut().iter_all_inodes().unwrap();
406 let mut dir_count = 0;
407 for (ino, inode) in &all_inodes {
408 if inode.file_type() == FileType::Directory && inode.mode != 0 {
409 let entries = r.read_dir(*ino).unwrap();
410 let names: Vec<String> = entries
411 .iter()
412 .map(super::super::ondisk::dir_entry::DirEntry::name_str)
413 .collect();
414 assert!(
415 names.contains(&".".to_string()),
416 "dir ino {ino} missing '.'"
417 );
418 assert!(
419 names.contains(&"..".to_string()),
420 "dir ino {ino} missing '..'"
421 );
422 dir_count += 1;
423 }
424 }
425 assert!(dir_count > 0, "no directories found");
426 }
427}