1use 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
18pub 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
35pub 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
79pub 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}