toolkit/bootstrap/host/paths/
home_dir.rs1use std::{
2 env,
3 path::{Path, PathBuf},
4};
5
6#[derive(Debug, thiserror::Error)]
8pub enum HomeDirError {
9 #[error("HOME environment variable is not set")]
10 HomeMissing,
11 #[error("failed to get executable path: {0}")]
12 ExecutablePathError(String),
13 #[error("IO error: {0}")]
14 Io(#[from] std::io::Error),
15}
16
17#[must_use]
18pub fn default_home_dir() -> PathBuf {
19 env::home_dir()
20 .or_else(|| env::current_dir().ok())
21 .unwrap_or_else(env::temp_dir)
22}
23
24pub fn expand_tilde(raw: &str) -> Result<PathBuf, HomeDirError> {
33 #[cfg(target_os = "windows")]
34 {
35 if raw.starts_with('~') {
36 let user_home = env::home_dir()
37 .ok_or_else(|| env::var("HOME"))
38 .map_err(|_| HomeDirError::HomeMissing)?;
39 if raw == "~" {
40 Ok(user_home)
41 } else if let Some(rest) = raw.strip_prefix("~/").or_else(|| raw.strip_prefix("~\\")) {
42 Ok(Path::new(&user_home).join(rest))
43 } else {
44 let rest = raw.trim_start_matches('~');
46 let rest = rest.trim_start_matches(['/', '\\']);
47 Ok(Path::new(&user_home).join(rest))
48 }
49 } else {
50 Ok(PathBuf::from(raw))
51 }
52 }
53
54 #[cfg(not(target_os = "windows"))]
55 {
56 if let Some(stripped) = raw.strip_prefix("~/") {
57 let home = env::home_dir().ok_or(HomeDirError::HomeMissing)?;
58 Ok(Path::new(&home).join(stripped))
59 } else if raw == "~" {
60 let home = env::home_dir().ok_or(HomeDirError::HomeMissing)?;
61 Ok(home)
62 } else {
63 Ok(PathBuf::from(raw))
64 }
65 }
66}
67
68pub fn normalize_path(raw: &str) -> Result<PathBuf, HomeDirError> {
78 let expanded = expand_tilde(raw)?;
80
81 if expanded.is_absolute() {
83 return Ok(expanded);
84 }
85
86 std::path::absolute(raw).map_err(|err| {
87 HomeDirError::ExecutablePathError(format!("path '{raw}' is invalid due to: {err}"))
88 })
89}
90
91#[cfg(test)]
92#[cfg_attr(coverage_nightly, coverage(off))]
93mod tests {
94 use super::*;
95 use tempfile::tempdir;
96
97 #[cfg(not(target_os = "windows"))]
101 #[test]
102 fn expand_tilde_with_path() {
103 let tmp = tempdir().unwrap();
104 let tmp_path = tmp.path().to_str().unwrap();
105
106 temp_env::with_var("HOME", Some(tmp_path), || {
107 let result = super::expand_tilde("~/bin/app").unwrap();
108 assert!(result.is_absolute());
109 assert!(result.ends_with("bin/app"));
110 });
111 }
112
113 #[cfg(not(target_os = "windows"))]
114 #[test]
115 fn expand_tilde_only() {
116 let tmp = tempdir().unwrap();
117 let tmp_path = tmp.path().to_str().unwrap();
118
119 temp_env::with_var("HOME", Some(tmp_path), || {
120 let result = expand_tilde("~").unwrap();
121 assert_eq!(result, tmp.path());
122 });
123 }
124
125 #[test]
126 fn expand_tilde_no_tilde() {
127 let result = expand_tilde("/usr/bin/app").unwrap();
128 assert_eq!(result, PathBuf::from("/usr/bin/app"));
129 }
130
131 #[cfg(target_os = "windows")]
132 #[test]
133 fn windows_expand_tilde_with_path() {
134 let tmp = tempdir().unwrap();
135 let tmp_path = tmp.path().to_str().unwrap();
136
137 temp_env::with_var("USERPROFILE", Some(tmp_path), || {
138 let result = expand_tilde("~/bin/app").unwrap();
139 assert!(result.is_absolute());
140 assert!(result.ends_with("bin\\app") || result.ends_with("bin/app"));
141 });
142 }
143
144 #[cfg(not(target_os = "windows"))]
148 #[test]
149 fn normalize_absolute_path() {
150 let result = normalize_path("/usr/bin/myapp").unwrap();
151 assert_eq!(result, PathBuf::from("/usr/bin/myapp"));
152 }
153
154 #[cfg(target_os = "windows")]
155 #[test]
156 fn windows_normalize_absolute_path() {
157 let result = normalize_path("C:\\bin\\myapp.exe").unwrap();
158 assert_eq!(result, PathBuf::from("C:\\bin\\myapp.exe"));
159 }
160
161 #[cfg(not(target_os = "windows"))]
162 #[test]
163 fn normalize_tilde_path() {
164 let tmp = tempdir().unwrap();
165 let tmp_path = tmp.path().to_str().unwrap();
166
167 temp_env::with_var("HOME", Some(tmp_path), || {
168 let result = normalize_path("~/bin/myapp").unwrap();
169 assert!(result.is_absolute());
170 assert!(result.starts_with(tmp_path));
171 assert!(result.ends_with("bin/myapp"));
172 });
173 }
174
175 #[test]
176 fn normalize_filename_only() {
177 let result = normalize_path("myapp.exe").unwrap();
178 let cwd = env::current_dir().unwrap();
179 assert_eq!(result, cwd.join("myapp.exe"));
180 }
181
182 #[test]
183 #[cfg(not(target_os = "windows"))]
184 fn normalize_relative_path_resolves_to_absolute() {
185 let err = normalize_path("./bin/myapp").unwrap();
186 assert!(err.is_absolute());
187 assert!(err.ends_with("bin/myapp"));
188 }
189
190 #[cfg(target_os = "windows")]
191 #[test]
192 fn windows_normalize_relative_path_resolves_to_absolute() {
193 let err = normalize_path(".\\bin\\myapp").unwrap();
194 assert!(err.is_absolute());
195 assert!(err.ends_with("bin\\myapp"));
196 }
197}