use std::path::{Component, Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathError {
OutsideRoot { resolved: String, root: String },
NotFound { resolved: String, root: String },
NoRoot { input: String },
}
impl PathError {
pub fn message(&self) -> String {
match self {
PathError::OutsideRoot { resolved, root } => format!(
"path is outside the analysis root: {resolved} (root: {root}). \
loregrep only reads files inside the directory it was pointed at"
),
PathError::NotFound { resolved, root } => format!(
"file not found: {resolved} (resolved relative to the analysis root {root}). \
hint: pass a path as returned by get_repository_tree or search_functions"
),
PathError::NoRoot { input } => format!(
"cannot resolve {input:?}: no analysis root is known for this index, \
and the path is not in it. Re-scan the repository, or pass a path \
exactly as returned by another tool"
),
}
}
}
fn lexically_normalize(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !out.pop() {
out.push("..");
}
}
other => out.push(other.as_os_str()),
}
}
out
}
pub fn resolve_within_root(root: &Path, input: &str) -> Result<PathBuf, PathError> {
let root_real = std::fs::canonicalize(root).unwrap_or_else(|_| lexically_normalize(root));
let candidate = if Path::new(input).is_absolute() {
PathBuf::from(input)
} else {
root_real.join(input)
};
let normalized = lexically_normalize(&candidate);
let resolved = std::fs::canonicalize(&normalized).unwrap_or(normalized);
if !resolved.starts_with(&root_real) {
return Err(PathError::OutsideRoot {
resolved: resolved.display().to_string(),
root: root_real.display().to_string(),
});
}
if !resolved.exists() {
return Err(PathError::NotFound {
resolved: resolved.display().to_string(),
root: root_real.display().to_string(),
});
}
Ok(resolved)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn fixture() -> (TempDir, PathBuf) {
let dir = TempDir::new().unwrap();
fs::create_dir_all(dir.path().join("src")).unwrap();
fs::write(dir.path().join("src/main.rs"), "fn main() {}").unwrap();
let root = dir.path().to_path_buf();
(dir, root)
}
#[test]
fn relative_input_resolves_against_the_root_not_the_cwd() {
let (_d, root) = fixture();
let resolved = resolve_within_root(&root, "src/main.rs").unwrap();
assert!(resolved.ends_with("src/main.rs"));
assert!(resolved.starts_with(std::fs::canonicalize(&root).unwrap()));
}
#[test]
fn leading_dot_slash_is_accepted() {
let (_d, root) = fixture();
assert!(resolve_within_root(&root, "./src/main.rs").is_ok());
}
#[test]
fn absolute_path_inside_the_root_is_accepted() {
let (_d, root) = fixture();
let abs = root.join("src/main.rs");
assert!(resolve_within_root(&root, abs.to_str().unwrap()).is_ok());
}
#[test]
fn absolute_path_outside_the_root_is_refused() {
let (_d, root) = fixture();
let err = resolve_within_root(&root, "/etc/hosts").unwrap_err();
assert!(matches!(err, PathError::OutsideRoot { .. }), "{err:?}");
assert!(err.message().contains("outside the analysis root"));
assert!(err.message().contains("/etc/hosts"));
}
#[test]
fn parent_traversal_out_of_the_root_is_refused() {
let (_d, root) = fixture();
let err = resolve_within_root(&root, "../../../../etc/hosts").unwrap_err();
assert!(matches!(err, PathError::OutsideRoot { .. }), "{err:?}");
}
#[test]
fn traversal_that_returns_inside_the_root_is_allowed() {
let (_d, root) = fixture();
assert!(resolve_within_root(&root, "src/../src/main.rs").is_ok());
}
#[test]
fn missing_file_inside_the_root_names_the_resolved_path() {
let (_d, root) = fixture();
let err = resolve_within_root(&root, "src/nope.rs").unwrap_err();
match &err {
PathError::NotFound { resolved, .. } => assert!(resolved.ends_with("src/nope.rs")),
other => panic!("expected NotFound, got {other:?}"),
}
assert!(err.message().contains("src/nope.rs"));
assert!(err.message().contains("get_repository_tree"));
}
#[test]
fn a_symlink_pointing_out_of_the_root_is_refused() {
let (_d, root) = fixture();
let link = root.join("escape.rs");
if std::os::unix::fs::symlink("/etc/hosts", &link).is_ok() {
let err = resolve_within_root(&root, "escape.rs").unwrap_err();
assert!(matches!(err, PathError::OutsideRoot { .. }), "{err:?}");
}
}
}