1use std::{
2 borrow::Cow,
3 ffi::OsStr,
4 path::{Component, Path, PathBuf, Prefix},
5};
6
7use keepcalm::SharedGlobalMut;
8use serde::Serialize;
9use tempfile::TempDir;
10
11static CANONICAL_TEMP_DIR: SharedGlobalMut<PathBuf> = SharedGlobalMut::new_lazy(|| {
12 let tmp = if cfg!(target_vendor = "apple") {
13 Path::new("/tmp").to_owned()
14 } else {
15 std::env::temp_dir()
16 };
17 match dunce::canonicalize(&tmp) {
18 Ok(canonical) => canonical,
19 Err(_) => tmp,
20 }
21});
22
23static CANONICAL_CWD: SharedGlobalMut<Option<PathBuf>> = SharedGlobalMut::new_lazy(|| {
24 let cwd = std::env::current_dir().ok()?;
25 match dunce::canonicalize(&cwd) {
26 Ok(canonical) => Some(canonical),
27 Err(_) => Some(cwd),
28 }
29});
30
31static CANONICAL_HOME_DIR: SharedGlobalMut<Option<PathBuf>> = SharedGlobalMut::new_lazy(|| {
32 dirs::home_dir().map(|home| dunce::canonicalize(&home).unwrap_or(home))
33});
34
35#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
36pub struct NicePathBuf {
37 path: PathBuf,
38}
39
40impl serde::Serialize for NicePathBuf {
41 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
42 where
43 S: serde::Serializer,
44 {
45 serializer.serialize_str(&self.path.display().to_string())
46 }
47}
48
49impl<'de> serde::Deserialize<'de> for NicePathBuf {
50 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51 where
52 D: serde::Deserializer<'de>,
53 {
54 let s = String::deserialize(deserializer)?;
55 Ok(Self::new(&s))
56 }
57}
58
59impl From<&'_ NicePathBuf> for NicePathBuf {
60 fn from(path: &NicePathBuf) -> Self {
61 path.clone()
62 }
63}
64
65impl From<&'_ Path> for NicePathBuf {
66 fn from(path: &Path) -> Self {
67 NicePathBuf::new(path)
68 }
69}
70
71impl AsRef<Path> for NicePathBuf {
72 fn as_ref(&self) -> &Path {
73 &self.path
74 }
75}
76
77impl NicePathBuf {
78 pub fn new(path: impl AsRef<Path>) -> Self {
79 Self {
80 path: path.as_ref().to_path_buf(),
81 }
82 }
83
84 pub fn exists(&self) -> std::io::Result<bool> {
85 std::fs::exists(&self.path)
86 }
87
88 pub fn join(&self, other: impl AsRef<Path>) -> Self {
89 Self {
90 path: self.path.join(other.as_ref()),
91 }
92 }
93
94 pub fn create_dir_all(&self) -> std::io::Result<()> {
95 std::fs::create_dir_all(&self.path)
96 }
97
98 pub fn remove_dir_all(&self) -> std::io::Result<()> {
99 std::fs::remove_dir_all(&self.path)
100 }
101
102 pub fn parent(&self) -> Option<NicePathBuf> {
103 self.path.parent().map(NicePathBuf::new)
104 }
105
106 pub fn cwd() -> NicePathBuf {
107 let cwd = std::env::current_dir().expect("Couldn't get current directory");
108 cwd.into()
109 }
110
111 pub fn env_string(&self) -> String {
117 let path = &self.path;
118 let canonical = canonicalize_path(path);
119 if cfg!(target_vendor = "apple") {
120 if let Ok(tmp) = canonical.strip_prefix(CANONICAL_TEMP_DIR.read()) {
121 format!("/tmp/{}", tmp.display())
122 } else {
123 canonical.display().to_string()
124 }
125 } else {
126 canonical.display().to_string()
127 }
128 }
129}
130
131impl From<PathBuf> for NicePathBuf {
132 fn from(path: PathBuf) -> Self {
133 Self { path }
134 }
135}
136
137impl From<String> for NicePathBuf {
138 fn from(path: String) -> Self {
139 Self {
140 path: PathBuf::from(path),
141 }
142 }
143}
144
145impl std::fmt::Display for NicePathBuf {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 write_pretty_path(false, &self.path, f)
148 }
149}
150
151impl std::fmt::Debug for NicePathBuf {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 write_pretty_path(true, &self.path, f)
154 }
155}
156
157pub struct NiceTempDir {
158 path: TempDir,
159}
160
161impl Default for NiceTempDir {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl NiceTempDir {
168 pub fn new() -> Self {
169 let path = if cfg!(target_vendor = "apple") {
170 tempfile::Builder::new()
171 .tempdir_in("/tmp")
172 .expect("Couldn't create tempdir")
173 } else {
174 tempfile::tempdir().expect("Couldn't create tempdir")
175 };
176 debug_assert!(path.path().is_absolute());
177 debug_assert!(matches!(std::fs::exists(path.path()), Ok(true)));
178 Self { path }
179 }
180
181 pub fn exists(&self) -> Result<bool, std::io::Error> {
182 std::fs::exists(self.path.path())
183 }
184
185 pub fn remove_dir_all(self) -> std::io::Result<()> {
186 self.path.close()
187 }
188
189 pub fn join(&self, other: impl AsRef<Path>) -> NicePathBuf {
190 NicePathBuf::new(self.path.path().join(other.as_ref()))
191 }
192
193 pub fn file_name(&self) -> Option<&OsStr> {
194 self.path.path().file_name()
195 }
196
197 pub fn env_string(&self) -> String {
198 NicePathBuf::from(self).env_string()
199 }
200}
201
202impl std::fmt::Display for NiceTempDir {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 write!(f, "{}", NicePathBuf::new(self.path.path()))
205 }
206}
207
208impl From<&'_ NiceTempDir> for NicePathBuf {
209 fn from(tempdir: &NiceTempDir) -> Self {
210 NicePathBuf::new(tempdir.path.path())
211 }
212}
213
214fn canonicalize_path(path: &Path) -> Cow<'_, Path> {
216 if let Ok(path) = dunce::canonicalize(path) {
217 return path.into();
218 }
219
220 let mut components = path.components();
221 let Some(last) = components.next_back() else {
222 return path.into();
223 };
224
225 let mut rest = PathBuf::from(last.as_os_str());
226
227 let mut path = path;
230 while let Some(parent) = path.parent() {
231 if let Ok(mut path) = dunce::canonicalize(parent) {
232 for component in rest.components() {
233 match component {
234 Component::ParentDir => {
235 if let Some(parent) = path.parent() {
236 path = parent.to_path_buf();
237 }
238 }
239 Component::CurDir => {}
240 _ => {
241 path = path.join(component.as_os_str());
242 }
243 }
244 }
245 return path.into();
246 }
247
248 path = parent;
249 let mut components = path.components();
250 let Some(last) = components.next_back() else {
251 return path.into();
252 };
253
254 rest = PathBuf::from(last.as_os_str()).join(rest);
255 }
256
257 path.into()
258}
259
260fn write_pretty_path(
261 debug: bool,
262 path: &Path,
263 f: &mut std::fmt::Formatter<'_>,
264) -> std::fmt::Result {
265 let tmp = &*CANONICAL_TEMP_DIR.read();
266 let home = &*CANONICAL_HOME_DIR.read();
267 let cwd = &*CANONICAL_CWD.read();
268
269 let mut canon_path = canonicalize_path(path);
270
271 if cfg!(target_vendor = "apple")
273 && canon_path.is_absolute()
274 && let Ok(without_private) = canon_path.strip_prefix("/private")
275 {
276 canon_path = Path::new("/").join(without_private).into();
277 }
278
279 if let Some(cwd) = cwd
282 && let Ok(path) = canon_path.strip_prefix(cwd)
283 {
284 if debug {
285 write_debug_path(f, path)?;
286 } else {
287 write!(f, "{}", path.display())?;
288 }
289 return Ok(());
290 }
291
292 if !cfg!(unix) && !cfg!(windows) {
294 if debug {
295 write_debug_path(f, path)?;
296 } else {
297 write!(f, "{}", path.display())?;
298 }
299 return Ok(());
300 }
301
302 if let Ok(path) = canon_path.strip_prefix(tmp) {
304 if cfg!(unix) {
305 let path = Path::new("/tmp").join(path);
306 if debug {
307 write_debug_path(f, &path)?;
308 } else {
309 write!(f, "{}", path.display())?;
310 }
311 } else if cfg!(windows) {
312 let path = Path::new("%TEMP%").join(path);
313 if debug {
314 write_debug_path(f, &path)?;
315 } else {
316 write!(f, "{}", path.display())?;
317 }
318 }
319 return Ok(());
320 }
321
322 if debug {
324 if cfg!(windows)
326 && let Some(Component::Prefix(prefix)) = canon_path.components().next()
327 {
328 if let Prefix::VerbatimDisk(_) = prefix.kind() {
330 return f.write_str(&format!("<{}>", canon_path.display()).replace(r"\\?\", ""));
331 }
332 }
333
334 write_debug_path(f, &canon_path)?;
335 return Ok(());
336 }
337
338 if let Some(home) = home
340 && let Ok(path) = canon_path.strip_prefix(home)
341 {
342 if cfg!(unix) {
343 write!(f, "~/{}", path.display())?;
344 } else if cfg!(windows) {
345 write!(f, "%USERPROFILE%\\{}", path.display())?;
346 }
347 return Ok(());
348 }
349
350 if cfg!(windows)
352 && let Some(Component::Prefix(prefix)) = canon_path.components().next()
353 && let Prefix::VerbatimDisk(_) = prefix.kind()
354 {
355 return write!(
356 f,
357 "{}",
358 canon_path.display().to_string().replace(r"\\?\", "")
359 );
360 }
361
362 write!(f, "{}", canon_path.display())
363}
364
365fn write_debug_path(f: &mut std::fmt::Formatter<'_>, path: &Path) -> std::fmt::Result {
366 if cfg!(windows) {
367 write!(f, "<{}>", path.display())
368 } else {
369 write!(f, "{path:?}")
370 }
371}
372
373#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, derive_more::Error, derive_more::Display)]
374pub enum ShellParseError {
375 #[display("unmatched quote ({_0})")]
376 UnmatchedQuote(#[error(not(source))] char),
377 #[display("invalid hex escape ({_0})")]
378 InvalidHexEscape(#[error(not(source))] char),
379}
380
381#[derive(derive_more::Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
383pub enum ShellBit {
384 #[debug("{_0:?}")]
387 Literal(String),
388 #[debug("{_0:?}")]
391 Quoted(String),
392}
393
394impl PartialEq<str> for ShellBit {
395 fn eq(&self, other: &str) -> bool {
396 match self {
397 ShellBit::Literal(s) => s == other,
398 ShellBit::Quoted(s) => s == other,
399 }
400 }
401}
402
403impl PartialEq<&'_ str> for ShellBit {
404 fn eq(&self, other: &&str) -> bool {
405 match self {
406 ShellBit::Literal(s) => s == other,
407 ShellBit::Quoted(s) => s == other,
408 }
409 }
410}
411
412impl std::fmt::Display for ShellBit {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 match self {
415 ShellBit::Literal(s) => f.write_str(s),
416 ShellBit::Quoted(s) => f.write_str(s),
417 }
418 }
419}
420
421impl Serialize for ShellBit {
422 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
423 where
424 S: serde::Serializer,
425 {
426 match self {
428 ShellBit::Literal(s) => serializer.serialize_str(s),
429 ShellBit::Quoted(s) => serializer.serialize_str(s),
430 }
431 }
432}
433
434pub fn shell_split(input: &str) -> Result<Vec<ShellBit>, ShellParseError> {
436 let mut result = Vec::new();
437 let mut in_string = None;
438 let mut in_escape = false;
439 let mut in_hex_escape = 0;
440 let mut hex_accum = 0;
441 let mut accum = String::new();
442
443 for c in input.chars() {
444 match in_hex_escape {
445 2 => {
446 in_hex_escape = 1;
447 if c.is_ascii_hexdigit() {
448 hex_accum = c.to_digit(16).unwrap();
449 continue;
450 } else {
451 return Err(ShellParseError::InvalidHexEscape(c));
452 }
453 }
454 1 => {
455 in_hex_escape = 0;
456 if c.is_ascii_hexdigit() {
457 hex_accum = hex_accum * 16 + c.to_digit(16).unwrap();
458 accum.push(char::from_u32(hex_accum).unwrap());
459 continue;
460 } else {
461 return Err(ShellParseError::InvalidHexEscape(c));
462 }
463 }
464 _ => {}
465 }
466
467 if in_escape {
468 in_escape = false;
469 match c {
470 'a' => accum.push('\x07'),
472 'b' => accum.push('\x08'),
474 'f' => accum.push('\x0c'),
476 'n' => accum.push('\n'),
478 'r' => accum.push('\r'),
480 't' => accum.push('\t'),
482 'v' => accum.push('\x0b'),
484 'e' => accum.push('\x1b'),
486 '0' => accum.push('\0'),
488
489 '"' => accum.push('"'),
490 'x' => in_hex_escape = 2,
491 _ => {
492 accum.push('\\');
493 accum.push(c);
494 }
495 }
496 continue;
497 }
498
499 if let Some(string_char) = in_string {
500 if string_char == '\'' {
501 if c == string_char {
502 in_string = None;
503 result.push(ShellBit::Literal(std::mem::take(&mut accum)));
504 } else {
505 accum.push(c);
506 }
507 } else if c == '\\' {
508 in_escape = true;
509 } else if c == string_char {
510 in_string = None;
511 if c == '"' {
512 result.push(ShellBit::Quoted(std::mem::take(&mut accum)));
513 }
514 } else {
515 accum.push(c);
516 }
517 } else if c == '\\' {
518 in_escape = true;
519 } else if c == '"' || c == '\'' {
520 in_string = Some(c);
521 } else if c == ' ' {
522 if accum.is_empty() {
523 continue;
524 }
525 result.push(ShellBit::Quoted(std::mem::take(&mut accum)));
526 } else {
527 accum.push(c);
528 }
529 }
530 if let Some(string_char) = in_string {
531 return Err(ShellParseError::UnmatchedQuote(string_char));
532 }
533
534 if !accum.is_empty() {
535 result.push(ShellBit::Quoted(std::mem::take(&mut accum)));
536 }
537
538 Ok(result)
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 #[cfg(unix)]
546 #[test]
547 fn test_nice_path_buf_tmp_unix() {
548 let path = NicePathBuf::new(Path::new("/tmp/hello.world"));
549
550 assert_eq!("/tmp/hello.world", format!("{path}"));
551 assert_eq!("\"/tmp/hello.world\"", format!("{path:?}"));
552
553 let path = NicePathBuf::new(Path::new("//tmp//hello.world"));
554
555 assert_eq!("/tmp/hello.world", format!("{path}"));
556 assert_eq!("\"/tmp/hello.world\"", format!("{path:?}"));
557
558 let path = NicePathBuf::new(Path::new("//does-not-exist-anywhere/..//tmp//hello.world"));
559
560 assert_eq!("/tmp/hello.world", format!("{path}"));
561 assert_eq!("\"/tmp/hello.world\"", format!("{path:?}"));
562
563 let path = NicePathBuf::new(
564 Path::new("/tmp")
565 .canonicalize()
566 .unwrap()
567 .join("hello.world"),
568 );
569
570 assert_eq!("/tmp/hello.world", format!("{path}"));
571 assert_eq!("\"/tmp/hello.world\"", format!("{path:?}"));
572
573 let temp_dir = NiceTempDir::new();
575 let path = temp_dir.join("a/b/c/d");
576
577 let name = temp_dir.file_name().unwrap().to_string_lossy();
578
579 assert_eq!(format!("/tmp/{name}/a/b/c/d"), format!("{}", path));
580 assert_eq!(format!("\"/tmp/{name}/a/b/c/d\""), format!("{:?}", path));
581 }
582
583 #[cfg(windows)]
584 #[test]
585 fn test_nice_path_buf_tmp_windows() {
586 let tmp = std::env::temp_dir();
587 let tmp = tmp.join("hello.world");
588
589 let path = NicePathBuf::new(&tmp);
590
591 assert_eq!(r"%TEMP%\hello.world", format!("{}", path));
592 assert_eq!(r"<%TEMP%\hello.world>", format!("{:?}", path));
593
594 let path = NicePathBuf::new(
595 &std::env::temp_dir()
596 .canonicalize()
597 .unwrap()
598 .join("hello.world"),
599 );
600
601 assert_eq!(r"%TEMP%\hello.world", format!("{}", path));
602 assert_eq!(r"<%TEMP%\hello.world>", format!("{:?}", path));
603
604 let path = NicePathBuf::new(r#"C:\directory"#);
605
606 assert_eq!(r"C:\directory", format!("{}", path));
607 assert_eq!(r"<C:\directory>", format!("{:?}", path));
608 }
609
610 #[test]
611 fn test_shell_split() {
612 assert_eq!(format!("{:?}", shell_split("").unwrap()), r#"[]"#);
613 assert_eq!(format!("{:?}", shell_split("a").unwrap()), r#"["a"]"#);
614 assert_eq!(
615 format!("{:?}", shell_split("a b").unwrap()),
616 r#"["a", "b"]"#
617 );
618 assert_eq!(
619 format!("{:?}", shell_split("a b c").unwrap()),
620 r#"["a", "b", "c"]"#
621 );
622 assert_eq!(
623 format!("{:?}", shell_split("a 'b' c").unwrap()),
624 r#"["a", "b", "c"]"#
625 );
626 assert_eq!(
627 format!("{:?}", shell_split("a 'b c' d").unwrap()),
628 r#"["a", "b c", "d"]"#
629 );
630 assert_eq!(
631 format!("{:?}", shell_split(r#"a "b" c"#).unwrap()),
632 r#"["a", "b", "c"]"#
633 );
634 assert_eq!(
635 format!("{:?}", shell_split(r#"a "b c" d"#).unwrap()),
636 r#"["a", "b c", "d"]"#
637 );
638 assert_eq!(
639 format!("{:?}", shell_split(r#"a "b\"c" d"#).unwrap()),
640 r#"["a", "b\"c", "d"]"#
641 );
642 assert_eq!(
643 format!("{:?}", shell_split(r#"a "b\'c" d"#).unwrap()),
644 r#"["a", "b\\'c", "d"]"#
645 );
646 assert_eq!(
647 format!("{:?}", shell_split(r#"a "b\nc" d"#).unwrap()),
648 r#"["a", "b\nc", "d"]"#
649 );
650 assert_eq!(
651 format!("{:?}", shell_split(r#"a "a\\b" d"#).unwrap()),
652 r#"["a", "a\\\\b", "d"]"#
653 );
654 assert_eq!(
655 format!("{:?}", shell_split(r#"a 'a\\b' d"#).unwrap()),
656 r#"["a", "a\\\\b", "d"]"#
657 );
658 }
659
660 #[test]
661 fn test_shell_split_errors() {
662 assert_eq!(
663 shell_split("a 'b").unwrap_err(),
664 ShellParseError::UnmatchedQuote('\'')
665 );
666 assert_eq!(
667 shell_split("a \"b c").unwrap_err(),
668 ShellParseError::UnmatchedQuote('"')
669 );
670 assert_eq!(
671 shell_split("a '").unwrap_err(),
672 ShellParseError::UnmatchedQuote('\'')
673 );
674 }
675}