1use std::io;
20use std::path::{Path, PathBuf};
21
22#[must_use]
31pub fn resolve_path(path: impl AsRef<Path>) -> PathBuf {
32 let path = path.as_ref();
33 let path_str = path.to_string_lossy();
34
35 if !path_str.starts_with('~') {
36 return path.to_path_buf();
37 }
38
39 if path_str.len() > 1 && !path_str[1..].starts_with('/') && !path_str[1..].starts_with('\\') {
43 return path.to_path_buf();
44 }
45
46 let Some(home) = home_dir() else {
47 return path.to_path_buf();
48 };
49
50 if path_str == "~" {
51 return home;
52 }
53
54 let rest = &path_str[2..];
56 home.join(rest)
57}
58
59pub fn ensure_dir(path: impl AsRef<Path>) -> io::Result<()> {
68 std::fs::create_dir_all(path)
69}
70
71#[must_use]
77#[cfg(target_os = "linux")]
78pub fn is_wsl() -> bool {
79 std::fs::read_to_string("/proc/version")
80 .map(|v| v.to_ascii_lowercase().contains("microsoft"))
81 .unwrap_or(false)
82}
83
84#[must_use]
88#[cfg(not(target_os = "linux"))]
89pub fn is_wsl() -> bool {
90 false
91}
92
93#[must_use]
102pub fn to_wsl_path(path: impl AsRef<Path>) -> PathBuf {
103 let s = path.as_ref().to_string_lossy();
104
105 let bytes = s.as_bytes();
107 if bytes.len() >= 3
108 && bytes[0].is_ascii_alphabetic()
109 && bytes[1] == b':'
110 && (bytes[2] == b'\\' || bytes[2] == b'/')
111 {
112 let drive = (bytes[0] as char).to_ascii_lowercase();
113 let rest = s[3..].replace('\\', "/");
114 return PathBuf::from(format!("/mnt/{drive}/{rest}"));
115 }
116
117 PathBuf::from(s.replace('\\', "/"))
119}
120
121#[must_use]
132pub fn from_wsl_path(path: impl AsRef<Path>) -> PathBuf {
133 let s = path.as_ref().to_string_lossy();
134
135 if s.starts_with("/mnt/") && s.len() >= 7 {
136 let bytes = s.as_bytes();
137 if bytes[5].is_ascii_alphabetic() && bytes[6] == b'/' {
141 let drive = (bytes[5] as char).to_ascii_uppercase();
142 let rest = s[7..].replace('/', "\\");
143 return PathBuf::from(format!("{drive}:\\{rest}"));
144 }
145 }
146
147 PathBuf::from(s.into_owned())
148}
149
150#[cfg(unix)]
152fn home_dir() -> Option<PathBuf> {
153 std::env::var_os("HOME").map(PathBuf::from)
154}
155
156#[cfg(windows)]
158fn home_dir() -> Option<PathBuf> {
159 std::env::var_os("USERPROFILE")
160 .or_else(|| std::env::var_os("HOME"))
161 .map(PathBuf::from)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_resolve_path_tilde() {
170 let home = home_dir().unwrap();
171 assert_eq!(resolve_path("~"), home);
172 assert_eq!(resolve_path("~/config.toml"), home.join("config.toml"));
173 assert_eq!(
174 resolve_path("~/.euxis/config.toml"),
175 home.join(".euxis/config.toml")
176 );
177 }
178
179 #[test]
180 fn test_resolve_path_no_tilde() {
181 let p = Path::new("/usr/local/bin");
182 assert_eq!(resolve_path(p), p.to_path_buf());
183
184 let rel = Path::new("relative/path");
185 assert_eq!(resolve_path(rel), rel.to_path_buf());
186 }
187
188 #[test]
189 fn test_ensure_dir() {
190 let tmp = tempfile::tempdir().unwrap();
191 let nested = tmp.path().join("a").join("b").join("c");
192 assert!(!nested.exists());
193 ensure_dir(&nested).unwrap();
194 assert!(nested.is_dir());
195 }
196
197 #[test]
198 fn test_to_wsl_path_drive_letters() {
199 assert_eq!(
200 to_wsl_path(r"C:\Users\Name\file.txt"),
201 PathBuf::from("/mnt/c/Users/Name/file.txt")
202 );
203 assert_eq!(
204 to_wsl_path(r"D:\Projects\src"),
205 PathBuf::from("/mnt/d/Projects/src")
206 );
207 assert_eq!(
209 to_wsl_path("E:/data/log.txt"),
210 PathBuf::from("/mnt/e/data/log.txt")
211 );
212 }
213
214 #[test]
215 fn test_to_wsl_path_non_windows() {
216 assert_eq!(
218 to_wsl_path("/usr/local/bin"),
219 PathBuf::from("/usr/local/bin")
220 );
221 assert_eq!(to_wsl_path("relative/path"), PathBuf::from("relative/path"));
222 }
223
224 #[test]
225 fn test_resolve_path_tilde_otheruser() {
226 let p = Path::new("~otheruser/downloads");
228 assert_eq!(resolve_path(p), p.to_path_buf());
229 }
230
231 #[test]
232 fn test_from_wsl_path_drive_letters() {
233 assert_eq!(
234 from_wsl_path("/mnt/c/Users/Name/file.txt"),
235 PathBuf::from("C:\\Users\\Name\\file.txt")
236 );
237 assert_eq!(
238 from_wsl_path("/mnt/d/Projects/src"),
239 PathBuf::from("D:\\Projects\\src")
240 );
241 }
242
243 #[test]
244 fn test_from_wsl_path_passthrough() {
245 assert_eq!(
247 from_wsl_path("/usr/local/bin"),
248 PathBuf::from("/usr/local/bin")
249 );
250 assert_eq!(
252 from_wsl_path("/mnt/data/shared"),
253 PathBuf::from("/mnt/data/shared")
254 );
255 assert_eq!(
256 from_wsl_path("/mnt/wslg/x.socket"),
257 PathBuf::from("/mnt/wslg/x.socket")
258 );
259 assert_eq!(
260 from_wsl_path("/mnt/cache/app"),
261 PathBuf::from("/mnt/cache/app")
262 );
263 }
264
265 #[test]
266 fn test_wsl_roundtrip() {
267 let win = r"C:\Users\Name\file.txt";
268 let wsl = to_wsl_path(win);
269 let back = from_wsl_path(&wsl);
270 assert_eq!(back, PathBuf::from(win));
271 }
272
273 #[test]
274 fn test_is_wsl_smoke() {
275 let _ = is_wsl();
277 }
278}