Skip to main content

perl_dap_platform/
lib.rs

1//! Cross-platform utilities for Perl path resolution and environment setup.
2
3use anyhow::{Context, Result};
4use std::collections::HashMap;
5use std::env;
6use std::path::PathBuf;
7
8#[cfg(windows)]
9const PATH_SEPARATOR: char = ';';
10#[cfg(not(windows))]
11const PATH_SEPARATOR: char = ':';
12
13#[cfg(windows)]
14const PERL_EXECUTABLE: &str = "perl.exe";
15#[cfg(not(windows))]
16const PERL_EXECUTABLE: &str = "perl";
17
18/// Resolve the perl binary path on the current platform.
19pub fn resolve_perl_path() -> Result<PathBuf> {
20    let path_env = env::var("PATH").context("PATH environment variable not set")?;
21    resolve_perl_path_from_path_env(&path_env)
22}
23
24pub(crate) fn resolve_perl_path_from_path_env(path_env: &str) -> Result<PathBuf> {
25    for path_dir in path_env.split(PATH_SEPARATOR) {
26        let perl_path = PathBuf::from(path_dir).join(PERL_EXECUTABLE);
27        if perl_path.exists() && perl_path.is_file() {
28            return Ok(perl_path);
29        }
30    }
31
32    anyhow::bail!("perl binary not found on PATH. Please install Perl or add it to PATH.")
33}
34
35/// Normalize a file path for cross-platform compatibility.
36pub fn normalize_path(path: &std::path::Path) -> PathBuf {
37    #[cfg(target_os = "linux")]
38    {
39        if let Some(path_str) = path.to_str()
40            && path_str.starts_with("/mnt/")
41            && path_str.len() > 6
42        {
43            let drive_letter = &path_str[5..6];
44            let rest = &path_str[6..];
45            let windows_path =
46                format!("{}:{}", drive_letter.to_uppercase(), rest.replace('/', "\\"));
47            return PathBuf::from(windows_path);
48        }
49    }
50
51    #[cfg(windows)]
52    {
53        if let Some(path_str) = path.to_str() {
54            if path_str.len() >= 2
55                && path_str.chars().nth(1) == Some(':')
56                && let Some(first_char) = path_str.chars().next()
57            {
58                let drive_letter = first_char.to_uppercase();
59                let rest = &path_str[1..];
60                return PathBuf::from(format!("{}{}", drive_letter, rest));
61            }
62
63            if path_str.starts_with("\\\\") {
64                return path.to_path_buf();
65            }
66        }
67    }
68
69    #[cfg(not(windows))]
70    {
71        if let Ok(canonical) = path.canonicalize() {
72            return canonical;
73        }
74    }
75
76    path.to_path_buf()
77}
78
79/// Setup environment variables for Perl execution.
80pub fn setup_environment(include_paths: &[PathBuf]) -> HashMap<String, String> {
81    let mut env = HashMap::new();
82
83    if !include_paths.is_empty() {
84        let perl5lib = include_paths
85            .iter()
86            .map(|p| p.to_string_lossy().to_string())
87            .collect::<Vec<_>>()
88            .join(&PATH_SEPARATOR.to_string());
89
90        env.insert("PERL5LIB".to_string(), perl5lib);
91    }
92
93    env
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use perl_tdd_support::{must, must_err};
100
101    #[test]
102    fn test_resolve_perl_path() {
103        if let Ok(path) = resolve_perl_path() {
104            assert!(path.exists());
105            assert!(path.is_file());
106        }
107    }
108
109    #[test]
110    fn test_normalize_path_basic() {
111        let normalized = normalize_path(&PathBuf::from("script.pl"));
112        assert!(!normalized.as_os_str().is_empty());
113    }
114
115    #[test]
116    fn test_setup_environment_empty() {
117        let env = setup_environment(&[]);
118        assert!(!env.contains_key("PERL5LIB"));
119    }
120
121    #[test]
122    fn test_setup_environment_with_paths() {
123        let env =
124            setup_environment(&[PathBuf::from("/workspace/lib"), PathBuf::from("/custom/lib")]);
125        assert!(env.contains_key("PERL5LIB"));
126    }
127
128    #[test]
129    fn resolve_from_path_env_finds_perl_in_first_dir() {
130        use std::fs;
131        let tempdir = must(tempfile::tempdir());
132        let bin = tempdir.path().join(PERL_EXECUTABLE);
133        must(fs::write(&bin, ""));
134        #[cfg(unix)]
135        {
136            use std::os::unix::fs::PermissionsExt;
137            let mut perms = must(fs::metadata(&bin)).permissions();
138            perms.set_mode(0o755);
139            must(fs::set_permissions(&bin, perms));
140        }
141        let path_str = tempdir.path().to_string_lossy().to_string();
142        let result = resolve_perl_path_from_path_env(&path_str);
143        assert_eq!(must(result), bin);
144    }
145
146    #[test]
147    fn resolve_from_path_env_empty_path_returns_error() {
148        let result = resolve_perl_path_from_path_env("");
149        assert!(result.is_err());
150        let msg = format!("{}", must_err(result));
151        assert!(
152            msg.contains("perl") || msg.contains("PATH"),
153            "error should mention perl/PATH: {msg}"
154        );
155    }
156
157    #[test]
158    fn resolve_from_path_env_no_perl_on_path_returns_error() {
159        let tempdir = must(tempfile::tempdir());
160        let path_str = tempdir.path().to_string_lossy().to_string();
161        let result = resolve_perl_path_from_path_env(&path_str);
162        assert!(result.is_err());
163    }
164
165    #[test]
166    #[cfg(target_os = "linux")]
167    fn normalize_path_wsl_mnt_translated_to_windows_style() {
168        let wsl_path = std::path::Path::new("/mnt/c/Users/user/script.pl");
169        let normalized = normalize_path(wsl_path);
170        let s = normalized.to_string_lossy();
171        assert!(
172            s.starts_with("C:\\") || s.starts_with("C:/"),
173            "expected Windows-style path, got: {s}"
174        );
175        assert!(s.contains("Users"), "path content preserved: {s}");
176    }
177
178    #[test]
179    fn normalize_path_non_wsl_unix_path_unchanged_on_linux() {
180        let path = std::path::Path::new("/usr/local/bin/perl");
181        let normalized = normalize_path(path);
182        assert!(
183            !normalized.to_string_lossy().contains('\\'),
184            "non-WSL path should not be Windows-escaped"
185        );
186    }
187}