1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::fs::{self, File};
4use std::io::Read;
5#[cfg(target_family = "windows")]
6use std::path::Prefix;
7use std::path::{Component, Path, PathBuf};
8
9use crate::errors::*;
10
11pub trait ToUtf8 {
12 fn to_utf8(&self) -> Result<&str>;
13}
14
15impl ToUtf8 for OsStr {
16 fn to_utf8(&self) -> Result<&str> {
17 self.to_str()
18 .ok_or_else(|| eyre::eyre!("unable to convert `{self:?}` to UTF-8 string"))
19 }
20}
21
22impl ToUtf8 for Path {
23 fn to_utf8(&self) -> Result<&str> {
24 self.as_os_str().to_utf8()
25 }
26}
27
28pub trait PathExt {
29 fn as_posix(&self) -> Result<String>;
30 #[cfg(target_family = "windows")]
31 fn as_wslpath(&self) -> Result<String>;
32}
33
34#[cfg(target_family = "windows")]
35fn format_prefix(prefix: &str) -> Result<String> {
36 match prefix {
37 "" => eyre::bail!("Error: got empty windows prefix"),
38 _ => Ok(format!("/mnt/{}", prefix.to_lowercase())),
39 }
40}
41
42#[cfg(target_family = "windows")]
43fn fmt_disk(disk: u8) -> String {
44 (disk as char).to_string()
45}
46
47#[cfg(target_family = "windows")]
48fn fmt_unc(server: &std::ffi::OsStr, volume: &std::ffi::OsStr) -> Result<String> {
49 let server = server.to_utf8()?;
50 let volume = volume.to_utf8()?;
51 let bytes = volume.as_bytes();
52 if server == "localhost"
53 && bytes.len() == 2
54 && bytes[1] == b'$'
55 && matches!(bytes[0], b'A'..=b'Z' | b'a'..=b'z')
56 {
57 Ok(fmt_disk(bytes[0]))
58 } else {
59 Ok(format!("{}/{}", server, volume))
60 }
61}
62
63impl PathExt for Path {
64 fn as_posix(&self) -> Result<String> {
65 if cfg!(target_os = "windows") {
66 let push = |p: &mut String, c: &str| {
67 if !p.is_empty() && p != "/" {
68 p.push('/');
69 }
70 p.push_str(c);
71 };
72
73 let mut output = String::new();
75 for component in self.components() {
76 match component {
77 Component::Prefix(prefix) => {
78 eyre::bail!("unix paths cannot handle windows prefix {prefix:?}.")
79 }
80 Component::RootDir => output = "/".to_owned(),
81 Component::CurDir => push(&mut output, "."),
82 Component::ParentDir => push(&mut output, ".."),
83 Component::Normal(path) => push(&mut output, path.to_utf8()?),
84 }
85 }
86 Ok(output)
87 } else {
88 self.to_utf8().map(|x| x.to_owned())
89 }
90 }
91
92 #[cfg(target_family = "windows")]
95 fn as_wslpath(&self) -> Result<String> {
96 let path = canonicalize(self)?;
97
98 let push = |p: &mut String, c: &str, r: bool| {
99 if !r {
100 p.push('/');
101 }
102 p.push_str(c);
103 };
104 let mut output = String::new();
106 let mut root_prefix = String::new();
107 let mut was_root = false;
108 for component in path.components() {
109 match component {
110 Component::Prefix(prefix) => {
111 root_prefix = match prefix.kind() {
112 Prefix::Verbatim(verbatim) => verbatim.to_utf8()?.to_owned(),
113 Prefix::VerbatimUNC(server, volume) => fmt_unc(server, volume)?,
114 Prefix::VerbatimDisk(disk) => fmt_disk(disk),
117 Prefix::UNC(server, volume) => fmt_unc(server, volume)?,
118 Prefix::DeviceNS(ns) => ns.to_utf8()?.to_owned(),
119 Prefix::Disk(disk) => fmt_disk(disk),
120 }
121 }
122 Component::RootDir => output = format!("{}/", format_prefix(&root_prefix)?),
123 Component::CurDir => push(&mut output, ".", was_root),
124 Component::ParentDir => push(&mut output, "..", was_root),
125 Component::Normal(path) => push(&mut output, path.to_utf8()?, was_root),
126 }
127 was_root = component == Component::RootDir;
128 }
129
130 if was_root {
132 output.truncate(output.len() - 1);
133 }
134
135 Ok(output)
136 }
137}
138
139pub fn read<P>(path: P) -> Result<String>
140where
141 P: AsRef<Path>,
142{
143 read_(path.as_ref())
144}
145
146fn read_(path: &Path) -> Result<String> {
147 let mut s = String::new();
148 File::open(path)
149 .wrap_err_with(|| format!("couldn't open {path:?}"))?
150 .read_to_string(&mut s)
151 .wrap_err_with(|| format!("couldn't read {path:?}"))?;
152 Ok(s)
153}
154
155pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf> {
156 _canonicalize(path.as_ref())
157 .wrap_err_with(|| format!("when canonicalizing path `{:?}`", path.as_ref()))
158}
159
160fn _canonicalize(path: &Path) -> Result<PathBuf> {
161 #[cfg(target_os = "windows")]
162 {
163 dunce::canonicalize(&path).map_err(Into::into)
165 }
166 #[cfg(not(target_os = "windows"))]
167 {
168 Path::new(&path).canonicalize().map_err(Into::into)
169 }
170}
171
172pub fn pretty_path(path: impl AsRef<Path>, strip: impl for<'a> Fn(&'a str) -> bool) -> String {
174 let path = path.as_ref();
175 let file_stem = path.file_stem();
177 let file_name = path.file_name();
178 let path = if let (Some(file_stem), Some(file_name)) = (file_stem, file_name) {
179 if let Some(file_name) = file_name.to_str() {
180 if strip(file_name) {
181 Cow::Borrowed(file_stem)
182 } else {
183 Cow::Borrowed(path.as_os_str())
184 }
185 } else {
186 Cow::Borrowed(path.as_os_str())
187 }
188 } else {
189 maybe_canonicalize(path)
190 };
191
192 if let Some(path) = path.to_str() {
193 shell_escape(path).to_string()
194 } else {
195 format!("{path:?}")
196 }
197}
198
199pub fn shell_escape(string: &str) -> Cow<'_, str> {
200 let escape: &[char] = if cfg!(target_os = "windows") {
201 &['%', '$', '`', '!', '"']
202 } else {
203 &['$', '\'', '\\', '!', '"']
204 };
205
206 if string.contains(escape) {
207 Cow::Owned(format!("{string:?}"))
208 } else if string.contains(' ') {
209 Cow::Owned(format!("\"{string}\""))
210 } else {
211 Cow::Borrowed(string)
212 }
213}
214
215#[must_use]
216pub fn maybe_canonicalize(path: &Path) -> Cow<'_, OsStr> {
217 canonicalize(path).map_or_else(
218 |_| path.as_os_str().into(),
219 |p| Cow::Owned(p.as_os_str().to_owned()),
220 )
221}
222
223pub fn write_file(path: impl AsRef<Path>, overwrite: bool) -> Result<File> {
224 let path = path.as_ref();
225 fs::create_dir_all(
226 &path
227 .parent()
228 .ok_or_else(|| eyre::eyre!("could not find parent directory for `{path:?}`"))?,
229 )
230 .wrap_err_with(|| format!("couldn't create directory `{path:?}`"))?;
231
232 let mut open = fs::OpenOptions::new();
233 open.write(true);
234
235 if overwrite {
236 open.truncate(true).create(true);
237 } else {
238 open.create_new(true);
239 }
240
241 open.open(&path)
242 .wrap_err(format!("couldn't write to file `{path:?}`"))
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use std::fmt::Debug;
249
250 macro_rules! p {
251 ($path:expr) => {
252 Path::new($path)
253 };
254 }
255
256 fn result_eq<T: PartialEq + Eq + Debug>(x: Result<T>, y: Result<T>) {
257 match (x, y) {
258 (Ok(x), Ok(y)) => assert_eq!(x, y),
259 (x, y) => panic!("assertion failed: `(left == right)`\nleft: {x:?}\nright: {y:?}"),
260 }
261 }
262
263 #[test]
264 fn as_posix() {
265 result_eq(p!(".").join("..").as_posix(), Ok("./..".to_owned()));
266 result_eq(p!(".").join("/").as_posix(), Ok("/".to_owned()));
267 result_eq(p!("foo").join("bar").as_posix(), Ok("foo/bar".to_owned()));
268 result_eq(p!("/foo").join("bar").as_posix(), Ok("/foo/bar".to_owned()));
269 }
270
271 #[test]
272 #[cfg(target_family = "windows")]
273 fn as_posix_prefix() {
274 assert_eq!(p!("C:").join(".."), p!("C:.."));
275 assert!(p!("C:").join("..").as_posix().is_err());
276 }
277
278 #[test]
279 #[cfg(target_family = "windows")]
280 fn as_wslpath() {
281 result_eq(p!(r"C:\").as_wslpath(), Ok("/mnt/c".to_owned()));
282 result_eq(p!(r"C:\Users").as_wslpath(), Ok("/mnt/c/Users".to_owned()));
283 result_eq(
284 p!(r"\\localhost\c$\Users").as_wslpath(),
285 Ok("/mnt/c/Users".to_owned()),
286 );
287 result_eq(p!(r"\\.\C:\").as_wslpath(), Ok("/mnt/c".to_owned()));
288 result_eq(
289 p!(r"\\.\C:\Users").as_wslpath(),
290 Ok("/mnt/c/Users".to_owned()),
291 );
292 }
293
294 #[test]
295 #[cfg(target_family = "windows")]
296 fn pretty_path_windows() {
297 assert_eq!(
298 pretty_path("C:\\path\\bin\\cargo.exe", |f| f.contains("cargo")),
299 "cargo".to_owned()
300 );
301 assert_eq!(
302 pretty_path("C:\\Program Files\\Docker\\bin\\docker.exe", |_| false),
303 "\"C:\\Program Files\\Docker\\bin\\docker.exe\"".to_owned()
304 );
305 assert_eq!(
306 pretty_path("C:\\Program Files\\single'quote\\cargo.exe", |c| c
307 .contains("cargo")),
308 "cargo".to_owned()
309 );
310 assert_eq!(
311 pretty_path("C:\\Program Files\\single'quote\\cargo.exe", |_| false),
312 "\"C:\\Program Files\\single'quote\\cargo.exe\"".to_owned()
313 );
314 assert_eq!(
315 pretty_path("C:\\Program Files\\%not_var%\\cargo.exe", |_| false),
316 "\"C:\\\\Program Files\\\\%not_var%\\\\cargo.exe\"".to_owned()
317 );
318 }
319
320 #[test]
321 #[cfg(target_family = "unix")]
322 fn pretty_path_linux() {
323 assert_eq!(
324 pretty_path("/usr/bin/cargo", |f| f.contains("cargo")),
325 "cargo".to_owned()
326 );
327 assert_eq!(
328 pretty_path("/home/user/my rust/bin/cargo", |_| false),
329 "\"/home/user/my rust/bin/cargo\"".to_owned(),
330 );
331 assert_eq!(
332 pretty_path("/home/user/single'quote/cargo", |c| c.contains("cargo")),
333 "cargo".to_owned()
334 );
335 assert_eq!(
336 pretty_path("/home/user/single'quote/cargo", |_| false),
337 "\"/home/user/single'quote/cargo\"".to_owned()
338 );
339 }
340}