use crate::dir::get_dir_entry_inode_by_name;
use crate::inode::Inode;
use crate::{DirEntryName, Ext4, Ext4Error, Path, PathBuf};
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum FollowSymlinks {
All,
ExcludeFinalComponent,
}
pub(crate) fn resolve_path(
fs: &Ext4,
path: Path<'_>,
follow: FollowSymlinks,
) -> Result<(Inode, PathBuf), Ext4Error> {
const MAX_SYMLINKS: usize = 40;
const MAX_PATH_LEN: usize = 4096;
const MAX_ITERATIONS: usize = 1000;
if !path.is_absolute() {
return Err(Ext4Error::NotAbsolute);
}
if path.as_ref().len() > MAX_PATH_LEN {
return Err(Ext4Error::PathTooLong);
}
let mut path = path.as_ref().to_vec();
path_dedup_sep(&mut path);
let mut num_symlinks: usize = 0;
let mut num_iterations: usize = 0;
let mut inode = fs.read_root_inode()?;
let mut index = 1;
while index < path.len() {
num_iterations = num_iterations.checked_add(1).unwrap();
assert!(num_iterations <= MAX_ITERATIONS);
let next_sep = find_next_sep(&path, index);
let comp_end = next_sep.unwrap_or(path.len());
let last_index = path.len().checked_sub(1).unwrap();
let is_last_component = next_sep.is_none() || comp_end == last_index;
let comp_plus_1: usize = comp_end.checked_add(1).unwrap();
let comp_end_with_sep = comp_plus_1.min(path.len());
let comp = &path[index..comp_end];
if !inode.metadata.is_dir() {
return Err(Ext4Error::NotADirectory);
}
let child_inode = get_dir_entry_inode_by_name(
fs,
&inode,
DirEntryName::try_from(comp).unwrap(),
)?;
if comp == b"." {
path.drain(index..comp_end_with_sep);
} else if comp == b".." {
let remove_start = find_parent_component_start(&path, index);
path.drain(remove_start..comp_end_with_sep);
index = remove_start;
inode = child_inode;
} else if child_inode.metadata.is_symlink()
&& (follow == FollowSymlinks::All || !is_last_component)
{
num_symlinks = num_symlinks.checked_add(1).unwrap();
if num_symlinks > MAX_SYMLINKS {
return Err(Ext4Error::TooManySymlinks);
}
let target = child_inode.symlink_target(fs)?;
let replace_start = if target.is_absolute() {
inode = fs.read_root_inode()?;
index = 1;
0
} else {
index
};
path.splice(
replace_start..comp_end,
target.as_ref().iter().cloned(),
);
if path.len() > MAX_PATH_LEN {
return Err(Ext4Error::PathTooLong);
}
path_dedup_sep(&mut path);
} else {
index = comp_end_with_sep;
inode = child_inode;
}
}
if path.len() > 1 && *path.last().unwrap() == Path::SEPARATOR {
if inode.metadata.is_dir() {
path.pop();
} else {
return Err(Ext4Error::NotADirectory);
}
}
let output_path = PathBuf::try_from(path).unwrap();
Ok((inode, output_path))
}
fn find_next_sep(path: &[u8], start: usize) -> Option<usize> {
assert!(start < path.len());
(start..path.len()).find(|&i| path[i] == Path::SEPARATOR)
}
fn find_prev_sep(path: &[u8], start: usize) -> Option<usize> {
assert!(start < path.len());
(0..=start).rev().find(|&i| path[i] == Path::SEPARATOR)
}
fn find_parent_component_start(path: &[u8], start: usize) -> usize {
assert!(start != 0 && start < path.len());
assert!(path[start.checked_sub(1).unwrap()] == Path::SEPARATOR);
if start == 1 {
start
} else {
let start_search_from = start.checked_sub(2).unwrap();
let prev_sep = find_prev_sep(path, start_search_from).unwrap();
prev_sep.checked_add(1).unwrap()
}
}
fn path_dedup_sep(path: &mut Vec<u8>) {
let mut i: usize = 1;
while i < path.len() {
let prev = i.checked_sub(1).unwrap();
if path[prev] == Path::SEPARATOR && path[i] == Path::SEPARATOR {
path.remove(i);
} else {
i = i.checked_add(1).unwrap();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_next_sep() {
assert_eq!(find_next_sep(b"/", 0), Some(0));
assert_eq!(find_next_sep(b"/abc/", 0), Some(0));
assert_eq!(find_next_sep(b"/abc/", 1), Some(4));
assert_eq!(find_next_sep(b"/abc", 1), None);
}
#[test]
fn test_find_prev_sep() {
assert_eq!(find_prev_sep(b"/", 0), Some(0));
assert_eq!(find_prev_sep(b"/abc/", 0), Some(0));
assert_eq!(find_prev_sep(b"/abc/", 1), Some(0));
assert_eq!(find_prev_sep(b"/abc/", 3), Some(0));
assert_eq!(find_prev_sep(b"/abc/", 4), Some(4));
assert_eq!(find_prev_sep(b"abc/", 2), None);
}
#[test]
fn test_find_parent_component_start() {
assert_eq!(find_parent_component_start(b"/ab/cde/fghi", 1), 1);
assert_eq!(find_parent_component_start(b"/ab/cde/fghi", 4), 1);
assert_eq!(find_parent_component_start(b"/ab/cde/fghi", 8), 4);
}
#[test]
fn test_path_dedup_sep() {
let mut p = b"///a///abc///".to_vec();
path_dedup_sep(&mut p);
assert_eq!(p, b"/a/abc/");
}
#[cfg(feature = "std")]
#[test]
fn test_resolve() {
let fs = &crate::test_util::load_test_disk1();
let follow = FollowSymlinks::All;
let mkp = |s| Path::new(s);
let resolve_to_root = [
"/",
"/.",
"/./",
"/..",
"/../",
"/../..",
"/../../",
"/empty_dir/..",
"/empty_dir/../",
"/empty_dir/../empty_dir/..",
"/empty_dir/../empty_dir/../",
"/dir1/dir2/sym_abs_dir/..",
"/dir1/dir2/sym_abs_dir/../",
"/dir1/dir2/sym_rel_dir/..",
"/dir1/dir2/sym_rel_dir/../",
];
for input in resolve_to_root {
let (inode, path) = resolve_path(fs, mkp(input), follow).unwrap();
assert_eq!((inode.index.get(), path.as_path()), (2, mkp("/")));
}
let (dir_inode, path) =
resolve_path(fs, mkp("/dir1/dir2"), follow).unwrap();
assert_eq!(path, "/dir1/dir2");
assert!(dir_inode.metadata.is_dir());
let (inode, path) =
resolve_path(fs, mkp("/dir1/dir2/"), follow).unwrap();
assert_eq!(path, "/dir1/dir2");
assert_eq!(inode.index, dir_inode.index);
let (inode, path) =
resolve_path(fs, mkp("/dir1/dir2/./"), follow).unwrap();
assert_eq!(path, "/dir1/dir2");
assert_eq!(inode.index, dir_inode.index);
let small_file_path = PathBuf::new("/small_file");
let (inode, path) =
resolve_path(fs, mkp("/dir1/dir2/sym_abs"), follow).unwrap();
assert_eq!(path, small_file_path);
assert_eq!(fs.read_inode_file(&inode).unwrap(), b"hello, world!");
let small_file_inode = inode.index;
let (inode, path) =
resolve_path(fs, mkp("/dir1/dir2/sym_rel"), follow).unwrap();
assert_eq!((inode.index, &path), (small_file_inode, &small_file_path));
let (inode, path) = resolve_path(
fs,
mkp("/dir1/dir2/sym_abs_dir/../small_file"),
follow,
)
.unwrap();
assert_eq!((inode.index, &path), (small_file_inode, &small_file_path));
let (inode, path) = resolve_path(
fs,
mkp("/dir1/dir2/sym_rel_dir/../small_file"),
follow,
)
.unwrap();
assert_eq!((inode.index, &path), (small_file_inode, &small_file_path));
let (inode, path) = resolve_path(
fs,
mkp("/dir1/dir2/sym_abs"),
FollowSymlinks::ExcludeFinalComponent,
)
.unwrap();
assert_eq!(path, "/dir1/dir2/sym_abs");
assert!(inode.metadata.is_symlink());
let (inode, path) = resolve_path(
fs,
mkp("/dir1/dir2/sym_rel"),
FollowSymlinks::ExcludeFinalComponent,
)
.unwrap();
assert_eq!(path, "/dir1/dir2/sym_rel");
assert!(inode.metadata.is_symlink());
assert!(matches!(
resolve_path(fs, mkp("a"), follow),
Err(Ext4Error::NotAbsolute)
));
let long_path = "/a".repeat(2049);
assert!(matches!(
resolve_path(fs, mkp(&long_path), follow),
Err(Ext4Error::PathTooLong)
));
let long_path = "a/".repeat(2030);
assert!(matches!(
resolve_path(
fs,
mkp("/sym_long").join(long_path).as_path(),
follow
),
Err(Ext4Error::PathTooLong)
));
assert!(matches!(
resolve_path(fs, mkp("/sym_loop_a"), follow),
Err(Ext4Error::TooManySymlinks)
));
assert!(matches!(
resolve_path(fs, mkp("/empty_file/path"), follow),
Err(Ext4Error::NotADirectory)
));
assert!(matches!(
resolve_path(fs, mkp("/empty_file/"), follow),
Err(Ext4Error::NotADirectory)
));
assert!(matches!(
resolve_path(
fs,
mkp("/dir1/dir2/sym_abs_dir/"),
FollowSymlinks::ExcludeFinalComponent
),
Err(Ext4Error::NotADirectory)
));
assert!(matches!(
resolve_path(fs, mkp("/empty_dir/does_not_exist"), follow),
Err(Ext4Error::NotFound)
));
}
}