1use crate::block_io::BlockDevice;
18use crate::dir::{self, DirEntry};
19use crate::error::{Error, Result};
20use crate::htree;
21use crate::indirect;
22use crate::inline_data;
23use crate::inode::{Inode, InodeFlags};
24use crate::superblock::Superblock;
25
26pub const EXT4_ROOT_INODE: u32 = 2;
28
29pub fn lookup<F>(
41 dev: &dyn BlockDevice,
42 sb: &Superblock,
43 read_inode: &mut F,
44 path: &str,
45) -> Result<u32>
46where
47 F: FnMut(u32) -> Result<Inode>,
48{
49 let csum = crate::checksum::Checksummer {
67 seed: 0,
68 enabled: false,
69 };
70 lookup_with_csum(dev, sb, read_inode, path, &csum)
71}
72
73pub fn lookup_with_csum<F>(
80 dev: &dyn BlockDevice,
81 sb: &Superblock,
82 read_inode: &mut F,
83 path: &str,
84 csum: &crate::checksum::Checksummer,
85) -> Result<u32>
86where
87 F: FnMut(u32) -> Result<Inode>,
88{
89 lookup_bytes_with_csum(dev, sb, read_inode, path.as_bytes(), csum)
90}
91
92pub fn lookup_bytes_with_csum<F>(
101 dev: &dyn BlockDevice,
102 sb: &Superblock,
103 read_inode: &mut F,
104 path: &[u8],
105 csum: &crate::checksum::Checksummer,
106) -> Result<u32>
107where
108 F: FnMut(u32) -> Result<Inode>,
109{
110 let mut current_ino: u32 = EXT4_ROOT_INODE;
111
112 for name in split_path(path) {
113 let inode = read_inode(current_ino)?;
114 if !inode.is_dir() {
115 return Err(Error::NotADirectory);
116 }
117 current_ino = find_entry(dev, sb, current_ino, &inode, name, csum)?;
118 }
119
120 Ok(current_ino)
121}
122
123pub(crate) fn find_entry(
128 dev: &dyn BlockDevice,
129 sb: &Superblock,
130 dir_ino: u32,
131 dir_inode: &Inode,
132 name: &[u8],
133 csum: &crate::checksum::Checksummer,
134) -> Result<u32> {
135 crate::file_io::refuse_encrypted_names(dir_inode)?;
136 if dir_inode.has_inline_data() {
140 return find_inline(dev, sb, dir_ino, dir_inode, name, csum);
141 }
142
143 let has_filetype = sb.feature_incompat & crate::features::Incompat::FILETYPE.bits() != 0;
144 let block_size = sb.block_size();
145
146 if (dir_inode.flags & InodeFlags::INDEX.bits()) != 0 {
148 if let Some(found) =
149 find_entry_htree(dev, sb, dir_ino, dir_inode, name, has_filetype, csum)?
150 {
151 return Ok(found);
152 }
153 }
156
157 find_entry_linear(
158 dev,
159 sb,
160 dir_ino,
161 dir_inode,
162 name,
163 has_filetype,
164 block_size,
165 csum,
166 )
167}
168
169#[allow(clippy::too_many_arguments)]
171fn find_entry_linear(
172 dev: &dyn BlockDevice,
173 _sb: &Superblock,
174 dir_ino: u32,
175 dir_inode: &Inode,
176 name: &[u8],
177 has_filetype: bool,
178 block_size: u32,
179 csum: &crate::checksum::Checksummer,
180) -> Result<u32> {
181 let dir_size = dir_inode.size;
182 let total_blocks = dir_size.div_ceil(block_size as u64);
183 let gen = dir_inode.generation;
184
185 let mut block = vec![0u8; block_size as usize];
186 for logical in 0..total_blocks {
187 let phys = match indirect::map_logical_any(
188 &dir_inode.block,
189 dir_inode.flags,
190 dev,
191 block_size,
192 logical,
193 )? {
194 Some(p) => p,
195 None => continue,
196 };
197 dev.read_at(phys * block_size as u64, &mut block)?;
198
199 match dir::parse_block_verified(&block, has_filetype, dir_ino, gen, csum) {
203 Ok(entries) => {
204 for entry in entries {
205 if entry.name == name {
206 return Ok(entry.inode);
207 }
208 }
209 }
210 Err(_) if logical == 0 && (dir_inode.flags & InodeFlags::INDEX.bits()) != 0 => {
211 continue;
214 }
215 Err(e) => return Err(e),
216 }
217 }
218
219 Err(Error::NotFound)
220}
221
222fn find_entry_htree(
227 dev: &dyn BlockDevice,
228 sb: &Superblock,
229 dir_ino: u32,
230 dir_inode: &Inode,
231 name: &[u8],
232 has_filetype: bool,
233 csum: &crate::checksum::Checksummer,
234) -> Result<Option<u32>> {
235 let block_size = sb.block_size();
236
237 let phys0 =
241 match indirect::map_logical_any(&dir_inode.block, dir_inode.flags, dev, block_size, 0)? {
242 Some(p) => p,
243 None => return Ok(None),
244 };
245 let mut root_block = vec![0u8; block_size as usize];
246 dev.read_at(phys0 * block_size as u64, &mut root_block)?;
247
248 let read_dx_block = |logical: u32| -> Result<Vec<u8>> {
252 let phys = indirect::map_logical_any(
253 &dir_inode.block,
254 dir_inode.flags,
255 dev,
256 block_size,
257 logical as u64,
258 )?
259 .ok_or(Error::CorruptDirEntry("htree pointed at sparse block"))?;
260 let mut buf = vec![0u8; block_size as usize];
261 dev.read_at(phys * block_size as u64, &mut buf)?;
262 Ok(buf)
263 };
264
265 let leaf_logical = match htree::lookup_leaf_with(
266 name,
267 &root_block,
268 &sb.hash_seed,
269 sb.unsigned_hash(),
270 read_dx_block,
271 )? {
272 Some(b) => b,
273 None => return Ok(None),
274 };
275
276 let phys = indirect::map_logical_any(
278 &dir_inode.block,
279 dir_inode.flags,
280 dev,
281 block_size,
282 leaf_logical as u64,
283 )?
284 .ok_or(Error::CorruptDirEntry("htree leaf at sparse block"))?;
285 let mut leaf = vec![0u8; block_size as usize];
286 dev.read_at(phys * block_size as u64, &mut leaf)?;
287
288 if csum.enabled
290 && dir::has_csum_tail(&leaf)
291 && !csum.verify_dir_entry_tail(dir_ino, dir_inode.generation, &leaf)
292 {
293 return Err(Error::BadChecksum {
294 what: "directory block",
295 });
296 }
297
298 for entry in dir::DirBlockIter::new(&leaf, has_filetype) {
299 let entry: DirEntry = entry?;
300 if entry.name == name {
301 return Ok(Some(entry.inode));
302 }
303 }
304
305 Ok(None)
308}
309
310fn find_inline(
315 dev: &dyn BlockDevice,
316 sb: &Superblock,
317 dir_ino: u32,
318 dir_inode: &Inode,
319 name: &[u8],
320 csum: &crate::checksum::Checksummer,
321) -> Result<u32> {
322 let has_filetype = sb.feature_incompat & crate::features::Incompat::FILETYPE.bits() != 0;
323 let in_block = inline_data::dir_entries(dir_ino, dir_inode, &[], has_filetype)?;
324 if let Some(entry) = in_block.iter().find(|e| e.name == name) {
325 return Ok(entry.inode);
326 }
327 if dir_inode.size as usize <= inline_data::INLINE_BLOCK_SIZE {
328 return Err(Error::NotFound);
329 }
330 let raw = read_raw_inode(dev, sb, dir_ino, dir_inode, csum)?;
331 let continuation =
332 inline_data::dir_continuation(dev, dir_inode, &raw, sb.inode_size, sb.block_size())?;
333 for entry in dir::DirBlockIter::new(&continuation, has_filetype) {
334 let entry = entry?;
335 if entry.name == name {
336 return Ok(entry.inode);
337 }
338 }
339 Err(Error::NotFound)
340}
341
342fn read_raw_inode(
348 dev: &dyn BlockDevice,
349 sb: &Superblock,
350 ino: u32,
351 expected: &Inode,
352 csum: &crate::checksum::Checksummer,
353) -> Result<Vec<u8>> {
354 if ino == 0 || ino > sb.inodes_count || sb.inodes_per_group == 0 {
355 return Err(Error::InvalidInode(ino));
356 }
357 let group = u64::from((ino - 1) / sb.inodes_per_group);
358 if group >= sb.block_group_count() {
359 return Err(Error::InvalidInode(ino));
360 }
361 let block_size = u64::from(sb.block_size());
362 let desc_size = sb.desc_size as usize;
363 let (desc_block, desc_off) = sb.descriptor_location(group);
364 let mut table = vec![0u8; block_size as usize];
365 dev.read_at(
366 desc_block
367 .checked_mul(block_size)
368 .ok_or(Error::Corrupt("group descriptor block number overflow"))?,
369 &mut table,
370 )?;
371 let desc = table
372 .get(desc_off..desc_off + desc_size)
373 .ok_or(Error::Corrupt("group descriptor outside its block"))?;
374 let bgd = crate::bgd::BlockGroupDescriptor::parse(desc, sb.desc_size)?;
375 let inode_size = u64::from(sb.inode_size);
376 let local = u64::from((ino - 1) % sb.inodes_per_group);
377 let offset = bgd
378 .inode_table
379 .checked_mul(block_size)
380 .and_then(|b| b.checked_add(local * inode_size))
381 .ok_or(Error::Corrupt("inode table offset overflow"))?;
382 let mut raw = vec![0u8; inode_size as usize];
383 dev.read_at(offset, &mut raw)?;
384 let parsed = Inode::parse(&raw)?;
385 if csum.enabled && !csum.verify_inode(ino, parsed.generation, &raw) {
386 return Err(Error::BadChecksum { what: "inode" });
387 }
388 if parsed.block != expected.block
389 || parsed.size != expected.size
390 || parsed.generation != expected.generation
391 {
392 return Err(Error::Corrupt(
393 "inline directory: the inode table disagrees with the inode being looked up",
394 ));
395 }
396 Ok(raw)
397}
398
399fn split_path(path: &[u8]) -> impl Iterator<Item = &[u8]> {
402 path.split(|&b| b == b'/').filter(|s| !s.is_empty())
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn split_path_basic() {
411 let split = |p: &'static [u8]| split_path(p).collect::<Vec<_>>();
412 let none: Vec<&[u8]> = Vec::new();
413 assert_eq!(split(b""), none);
414 assert_eq!(split(b"/"), none);
415 assert_eq!(split(b"/foo"), vec![&b"foo"[..]]);
416 assert_eq!(split(b"/foo/bar"), vec![&b"foo"[..], b"bar"]);
417 assert_eq!(split(b"foo/bar"), vec![&b"foo"[..], b"bar"]);
418 assert_eq!(split(b"/foo//bar/"), vec![&b"foo"[..], b"bar"]);
419 assert_eq!(split(b"///"), none);
420 }
421
422 #[test]
425 fn split_path_keeps_bytes_that_are_not_utf8() {
426 let parts: Vec<&[u8]> = split_path(b"/d\xff/caf\xe9.txt").collect();
427 assert_eq!(parts, vec![&b"d\xff"[..], b"caf\xe9.txt"]);
428 }
429
430 mod needs_host {
432 use super::*;
433 use crate::bgd;
434 use crate::block_io::FileDevice;
435 use crate::extent;
436 use crate::fs::Filesystem;
437 use std::sync::Arc;
438
439 fn read_inode_fn(fs: &Filesystem) -> impl FnMut(u32) -> Result<Inode> + '_ {
442 move |ino: u32| {
443 let (block, offset) = bgd::locate_inode(&fs.sb, &fs.groups, ino)?;
444 let block_data = fs.read_block(block)?;
445 let inode_size = fs.sb.inode_size as usize;
446 let off = offset as usize;
447 Inode::parse(&block_data[off..off + inode_size])
448 }
449 }
450
451 #[test]
452 fn root_resolves_to_inode_2() {
453 let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
454 let file = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
455 let dev: Arc<dyn BlockDevice> = Arc::new(file);
456 let fs = Filesystem::mount(dev.clone()).expect("mount");
457 let mut reader = read_inode_fn(&fs);
458
459 for root_path in ["/", "", "///"] {
460 let ino = lookup(dev.as_ref(), &fs.sb, &mut reader, root_path)
461 .unwrap_or_else(|e| panic!("lookup({root_path:?}) failed: {e}"));
462 assert_eq!(ino, EXT4_ROOT_INODE, "path {root_path:?}");
463 }
464 }
465
466 #[test]
467 fn missing_path_returns_not_found() {
468 let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
469 let file = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
470 let dev: Arc<dyn BlockDevice> = Arc::new(file);
471 let fs = Filesystem::mount(dev.clone()).expect("mount");
472 let mut reader = read_inode_fn(&fs);
473
474 let result = lookup(
475 dev.as_ref(),
476 &fs.sb,
477 &mut reader,
478 "/this-does-not-exist-xyz",
479 );
480 assert!(matches!(result, Err(Error::NotFound)), "got {result:?}");
481 }
482
483 #[test]
484 fn non_dir_component_returns_not_a_directory() {
485 let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
486 let file = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
487 let dev: Arc<dyn BlockDevice> = Arc::new(file);
488 let fs = Filesystem::mount(dev.clone()).expect("mount");
489 let mut reader = read_inode_fn(&fs);
490
491 let root = reader(EXT4_ROOT_INODE).expect("root inode");
493 let block_size = fs.sb.block_size();
494 let total_blocks = root.size.div_ceil(block_size as u64);
495 let has_filetype =
496 fs.sb.feature_incompat & crate::features::Incompat::FILETYPE.bits() != 0;
497
498 let mut reg_file_name: Option<Vec<u8>> = None;
499 'outer: for logical in 0..total_blocks {
500 if let Some(phys) =
501 extent::map_logical(&root.block, dev.as_ref(), block_size, logical)
502 .expect("map logical")
503 {
504 let mut blk = vec![0u8; block_size as usize];
505 dev.read_at(phys * block_size as u64, &mut blk).unwrap();
506 for entry in dir::DirBlockIter::new(&blk, has_filetype) {
507 let e = entry.expect("entry");
508 if e.file_type == dir::DirEntryType::RegFile {
509 reg_file_name = Some(e.name);
510 break 'outer;
511 }
512 }
513 }
514 }
515
516 let name = reg_file_name.expect(
517 "ext4-basic.img has a regular file (test.txt) in its root \
518 (test-disks/guest-build-images.sh)",
519 );
520 let name_str = std::str::from_utf8(&name).expect("name utf8");
521 let bad_path = format!("/{name_str}/child");
522
523 let result = lookup(dev.as_ref(), &fs.sb, &mut reader, &bad_path);
524 assert!(
525 matches!(result, Err(Error::NotADirectory)),
526 "got {result:?} for path {bad_path}"
527 );
528 }
529 }
530}