perl_module_resolution_path/
lib.rs1#![deny(unsafe_code)]
7#![warn(rust_2018_idioms)]
8#![warn(missing_docs)]
9#![warn(clippy::all)]
10
11use std::path::{Path, PathBuf};
12
13use perl_module_path::module_name_to_path;
14use perl_path_security::validate_workspace_path;
15
16#[must_use]
22pub fn resolve_module_path(
23 root: &Path,
24 module_name: &str,
25 include_paths: &[String],
26) -> Option<PathBuf> {
27 let relative_path = module_name_to_path(module_name);
28
29 for base in include_paths {
30 let candidate = if base == "." {
31 root.join(&relative_path)
32 } else {
33 root.join(base).join(&relative_path)
34 };
35
36 let safe_candidate = match validate_workspace_path(&candidate, root) {
37 Ok(path) => path,
38 Err(_) => continue,
39 };
40
41 if safe_candidate.exists() {
42 return Some(safe_candidate);
43 }
44 }
45
46 Some(root.join("lib").join(relative_path))
47}
48
49#[cfg(test)]
50mod tests {
51 use std::fs;
52 use tempfile::tempdir;
53
54 use super::resolve_module_path;
55
56 #[test]
57 fn returns_first_safe_candidate() -> Result<(), Box<dyn std::error::Error>> {
58 let temp = tempdir()?;
59 let root = temp.path().to_path_buf();
60 let module_file = root.join("lib").join("Foo").join("Bar.pm");
61
62 fs::create_dir_all(module_file.parent().ok_or("no parent")?)?;
63 fs::write(&module_file, "package Foo::Bar; 1;")?;
64
65 let resolved = resolve_module_path(&root, "Foo::Bar", &["lib".to_string()]);
66 assert_eq!(resolved, Some(root.join("lib").join("Foo/Bar.pm")));
67 Ok(())
68 }
69
70 #[test]
71 fn rejects_traversal_candidate_and_falls_back_to_lib() -> Result<(), Box<dyn std::error::Error>>
72 {
73 let root = tempfile::tempdir()?.path().to_path_buf();
74 let resolved = resolve_module_path(&root, "Escaped::Target", &["..".to_string()]);
75
76 assert_eq!(resolved, Some(root.join("lib").join("Escaped/Target.pm")));
77 Ok(())
78 }
79
80 #[test]
81 fn accepts_current_directory_as_include_path() -> Result<(), Box<dyn std::error::Error>> {
82 let temp = tempfile::tempdir()?;
83 let root = temp.path().to_path_buf();
84 let module_file = root.join("Local").join("Only.pm");
85
86 std::fs::create_dir_all(module_file.parent().ok_or("no parent")?)?;
87 std::fs::write(&module_file, "package Local::Only; 1;")?;
88
89 let resolved = resolve_module_path(&root, "Local::Only", &[".".to_string()]);
90 assert_eq!(resolved, Some(root.join("Local/Only.pm")));
91 Ok(())
92 }
93}