perl_module/resolution/path.rs
1//! Workspace-aware Perl module path resolution.
2//!
3//! Convert a Perl module name into a canonical filesystem path candidate
4//! under a workspace root.
5
6use std::path::{Path, PathBuf};
7
8use crate::path::module_name_to_path;
9use perl_parser_core::path_security::validate_workspace_path;
10
11/// Resolve a Perl module name to a workspace-relative filesystem path candidate.
12///
13/// The search order is:
14/// 1. Each configured include path in order:
15/// - Relative paths are resolved under `root` and validated against traversal.
16/// - Absolute paths are treated as literal external roots.
17/// 2. Fallback to `root/lib/<module>.pm`.
18#[must_use]
19pub fn resolve_module_path(
20 root: &Path,
21 module_name: &str,
22 include_paths: &[String],
23) -> Option<PathBuf> {
24 let relative_path = module_name_to_path(module_name);
25
26 for base in include_paths {
27 let base_path = Path::new(base);
28 let candidate = if base_path.is_absolute() {
29 base_path.join(&relative_path)
30 } else if base == "." {
31 root.join(&relative_path)
32 } else {
33 root.join(base).join(&relative_path)
34 };
35
36 // For relative paths, validate safety (traversal prevention) but keep
37 // the original candidate so the returned path stays relative to `root`
38 // without canonicalization (canonicalize expands 8.3 short names on
39 // Windows, making the result inconsistent with the caller-supplied root).
40 if !base_path.is_absolute() && validate_workspace_path(&candidate, root).is_err() {
41 continue;
42 }
43
44 if candidate.exists() {
45 return Some(candidate);
46 }
47 }
48
49 Some(root.join("lib").join(relative_path))
50}