#![deny(unsafe_code)]
#![warn(rust_2018_idioms)]
#![warn(missing_docs)]
#![warn(clippy::all)]
use std::path::{Path, PathBuf};
use perl_module_path::module_name_to_path;
use perl_path_security::validate_workspace_path;
#[must_use]
pub fn resolve_module_path(
root: &Path,
module_name: &str,
include_paths: &[String],
) -> Option<PathBuf> {
let relative_path = module_name_to_path(module_name);
for base in include_paths {
let candidate = if base == "." {
root.join(&relative_path)
} else {
root.join(base).join(&relative_path)
};
let safe_candidate = match validate_workspace_path(&candidate, root) {
Ok(path) => path,
Err(_) => continue,
};
if safe_candidate.exists() {
return Some(safe_candidate);
}
}
Some(root.join("lib").join(relative_path))
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use super::resolve_module_path;
#[test]
fn returns_first_safe_candidate() -> Result<(), Box<dyn std::error::Error>> {
let temp = tempdir()?;
let root = temp.path().to_path_buf();
let module_file = root.join("lib").join("Foo").join("Bar.pm");
fs::create_dir_all(module_file.parent().ok_or("no parent")?)?;
fs::write(&module_file, "package Foo::Bar; 1;")?;
let resolved = resolve_module_path(&root, "Foo::Bar", &["lib".to_string()]);
assert_eq!(resolved, Some(root.join("lib").join("Foo/Bar.pm")));
Ok(())
}
#[test]
fn rejects_traversal_candidate_and_falls_back_to_lib() -> Result<(), Box<dyn std::error::Error>>
{
let root = tempfile::tempdir()?.path().to_path_buf();
let resolved = resolve_module_path(&root, "Escaped::Target", &["..".to_string()]);
assert_eq!(resolved, Some(root.join("lib").join("Escaped/Target.pm")));
Ok(())
}
#[test]
fn accepts_current_directory_as_include_path() -> Result<(), Box<dyn std::error::Error>> {
let temp = tempfile::tempdir()?;
let root = temp.path().to_path_buf();
let module_file = root.join("Local").join("Only.pm");
std::fs::create_dir_all(module_file.parent().ok_or("no parent")?)?;
std::fs::write(&module_file, "package Local::Only; 1;")?;
let resolved = resolve_module_path(&root, "Local::Only", &[".".to_string()]);
assert_eq!(resolved, Some(root.join("Local/Only.pm")));
Ok(())
}
}