Skip to main content

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}