Skip to main content

perl_module/resolution/use_lib/
resolve.rs

1//! Resolve extracted `use lib` paths against workspace and file locations.
2
3use std::path::{Component, Path, PathBuf};
4
5use super::UseLibPath;
6
7/// Resolve `use lib` paths against a workspace root and optional file directory.
8///
9/// - Absolute paths are accepted only when they stay under `workspace_root`.
10/// - `$FindBin::Bin`-relative paths are resolved against `file_dir` (or `workspace_root` if absent).
11/// - Other relative paths are resolved against `workspace_root`.
12pub fn resolve_use_lib_paths(
13    use_lib_paths: &[UseLibPath],
14    workspace_root: &Path,
15    file_dir: Option<&Path>,
16) -> Vec<String> {
17    let mut result = Vec::new();
18
19    for ulp in use_lib_paths {
20        let path_str = &ulp.path;
21
22        if ulp.from_findbin {
23            let base = file_dir.unwrap_or(workspace_root);
24            let Some(resolved) = normalize_findbin_path(base, path_str) else {
25                continue;
26            };
27            if resolved.strip_prefix(workspace_root).is_err() {
28                continue;
29            }
30            if let Some(s) = path_to_relative_string(&resolved, workspace_root)
31                && !result.contains(&s)
32            {
33                result.push(s);
34            }
35        } else {
36            let p = Path::new(path_str);
37            if p.is_absolute() {
38                if let Some(s) = path_to_relative_string(p, workspace_root)
39                    && !result.contains(&s)
40                {
41                    result.push(s);
42                }
43            } else {
44                let s = path_str.to_string();
45                if !result.contains(&s) {
46                    result.push(s);
47                }
48            }
49        }
50    }
51
52    result
53}
54
55fn path_to_relative_string(path: &Path, workspace_root: &Path) -> Option<String> {
56    if let Ok(rel) = path.strip_prefix(workspace_root) {
57        // Guard against lexical strip_prefix matching an embedded `..` segment.
58        // For example, `/workspace/../etc` strips the `/workspace` prefix lexically,
59        // leaving `../etc` which would escape the workspace.  Reject any result
60        // that contains a parent-directory component.
61        if rel.components().any(|c| c == std::path::Component::ParentDir) {
62            return None;
63        }
64        let s = normalize_relative_path_string(rel.to_string_lossy().as_ref());
65        if s.is_empty() { Some(".".to_string()) } else { Some(s) }
66    } else if path.is_absolute() {
67        None
68    } else {
69        let s = normalize_relative_path_string(path.to_string_lossy().as_ref());
70        Some(s)
71    }
72}
73
74fn normalize_relative_path_string(path: &str) -> String {
75    path.replace('\\', "/")
76}
77
78fn normalize_findbin_path(base: &Path, relative: &str) -> Option<PathBuf> {
79    let mut normalized = PathBuf::from(base);
80    for component in Path::new(relative).components() {
81        match component {
82            Component::CurDir => {}
83            Component::Normal(segment) => normalized.push(segment),
84            Component::ParentDir => {
85                if !normalized.pop() {
86                    return None;
87                }
88            }
89            Component::RootDir | Component::Prefix(_) => return None,
90        }
91    }
92    Some(normalized)
93}