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 #[cfg(windows)]
288 write!(f, ".\\{}", path.display())?;
289 #[cfg(not(windows))]
290 write!(f, "./{}", path.display())?;
291 }
292 return Ok(());
293 }
294
295 if !cfg!(unix) && !cfg!(windows) {
297 if debug {
298 write_debug_path(f, path)?;
299 } else {
300 write!(f, "{}", path.display())?;
301 }
302 return Ok(());
303 }
304
305 if let Ok(path) = canon_path.strip_prefix(tmp) {
307 if cfg!(unix) {
308 let path = Path::new("/tmp").join(path);
309 if debug {
310 write_debug_path(f, &path)?;
311 } else {
312 write!(f, "{}", path.display())?;
313 }
314 } else if cfg!(windows) {
315 let path = Path::new("%TEMP%").join(path);
316 if debug {
317 write_debug_path(f, &path)?;
318 } else {
319 write!(f, "{}", path.display())?;
320 }
321 }
322 return Ok(());
323 }
324
325 if debug {
327 if cfg!(windows)
329 && let Some(Component::Prefix(prefix)) = canon_path.components().next()
330 {
331 if let Prefix::VerbatimDisk(_) = prefix.kind() {
333 return f.write_str(&format!("<{}>", canon_path.display()).replace(r"\\?\", ""));
334 }
335 }
336
337 write_debug_path(f, &canon_path)?;
338 return Ok(());
339 }
340
341 if let Some(home) = home
343 && let Ok(path) = canon_path.strip_prefix(home)
344 {
345 if cfg!(unix) {
346 write!(f, "~/{}", path.display())?;
347 } else if cfg!(windows) {
348 write!(f, "%USERPROFILE%\\{}", path.display())?;
349 }
350 return Ok(());
351 }
352
353 if cfg!(windows)
355 && let Some(Component::Prefix(prefix)) = canon_path.components().next()
356 && let Prefix::VerbatimDisk(_) = prefix.kind()
357 {
358 return write!(
359 f,
360 "{}",
361 canon_path.display().to_string().replace(r"\\?\", "")
362 );
363 }
364
365 write!(f, "{}", canon_path.display())
366}
367
368fn write_debug_path(f: &mut std::fmt::Formatter<'_>, path: &Path) -> std::fmt::Result {
369 if cfg!(windows) {
370 write!(f, "<{}>", path.display())
371 } else {
372 write!(f, "{path:?}")
373 }
374}
375
376#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, derive_more::Error, derive_more::Display)]
377pub enum ShellParseError {
378 #[display("unmatched quote ({_0})")]
379 UnmatchedQuote(#[error(not(source))] char),
380 #[display("invalid hex escape ({_0})")]
381 InvalidHexEscape(#[error(not(source))] char),
382}
383
384#[derive(derive_more::Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
386pub enum ShellBit {
387 #[debug("{_0:?}")]
390 Literal(String),
391 #[debug("{_0:?}")]
394 Quoted(String),
395}
396
397impl PartialEq<str> for ShellBit {
398 fn eq(&self, other: &str) -> bool {
399 match self {
400 ShellBit::Literal(s) => s == other,
401 ShellBit::Quoted(s) => s == other,
402 }
403 }
404}
405
406impl PartialEq<&'_ str> for ShellBit {
407 fn eq(&self, other: &&str) -> bool {
408 match self {
409 ShellBit::Literal(s) => s == other,
410 ShellBit::Quoted(s) => s == other,
411 }
412 }
413}
414
415impl std::fmt::Display for ShellBit {
416 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417 match self {
418 ShellBit::Literal(s) => f.write_str(s),
419 ShellBit::Quoted(s) => f.write_str(s),
420 }
421 }
422}
423
424impl Serialize for ShellBit {
425 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
426 where
427 S: serde::Serializer,
428 {
429 match self {
430 ShellBit::Literal(s) => serializer.serialize_str(s),
431 ShellBit::Quoted(s) => serializer.serialize_str(s),
432 }
433 }
434}
435
436pub fn shell_split(input: &str) -> Result<Vec<ShellBit>, ShellParseError> {
438 let mut result = Vec::new();
439 let mut in_string = None;
440 let mut in_escape = false;
441 let mut in_hex_escape = 0;
442 let mut hex_accum = 0;
443 let mut accum = String::new();
444
445 for c in input.chars() {
446 match in_hex_escape {
447 2 => {
448 in_hex_escape = 1;
449 if c.is_ascii_hexdigit() {
450 hex_accum = c.to_digit(16).unwrap();
451 continue;
452 } else {
453 return Err(ShellParseError::InvalidHexEscape(c));
454 }
455 }
456 1 => {
457 in_hex_escape = 0;
458 if c.is_ascii_hexdigit() {
459 hex_accum = hex_accum * 16 + c.to_digit(16).unwrap();
460 accum.push(char::from_u32(hex_accum).unwrap());
461 continue;
462 } else {
463 return Err(ShellParseError::InvalidHexEscape(c));
464 }
465 }
466 _ => {}
467 }
468
469 if in_escape {
470 in_escape = false;
471 match c {
472 'a' => accum.push('\x07'),
474 'b' => accum.push('\x08'),
476 'f' => accum.push('\x0c'),
478 'n' => accum.push('\n'),
480 'r' => accum.push('\r'),
482 't' => accum.push('\t'),
484 'v' => accum.push('\x0b'),
486 'e' => accum.push('\x1b'),
488 '0' => accum.push('\0'),
490
491 '"' => accum.push('"'),
492 'x' => in_hex_escape = 2,
493 _ => {
494 accum.push('\\');
495 accum.push(c);
496 }
497 }
498 continue;
499 }
500
501 if let Some(string_char) = in_string {
502 if string_char == '\'' {
503 if c == string_char {
504 in_string = None;
505 result.push(ShellBit::Literal(std::mem::take(&mut accum)));
506 } else {
507 accum.push(c);
508 }
509 } else if c == '\\' {
510 in_escape = true;
511 } else if c == string_char {
512 in_string = None;
513 if c == '"' {
514 result.push(ShellBit::Quoted(std::mem::take(&mut accum)));
515 }
516 } else {
517 accum.push(c);
518 }
519 } else if c == '\\' {
520 in_escape = true;
521 } else if c == '"' || c == '\'' {
522 in_string = Some(c);
523 } else if c == ' ' {
524 if accum.is_empty() {
525 continue;
526 }
527 result.push(ShellBit::Quoted(std::mem::take(&mut accum)));
528 } else {
529 accum.push(c);
530 }
531 }
532 if let Some(string_char) = in_string {
533 return Err(ShellParseError::UnmatchedQuote(string_char));
534 }
535
536 if !accum.is_empty() {
537 result.push(ShellBit::Quoted(std::mem::take(&mut accum)));
538 }
539
540 Ok(result)
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[cfg(unix)]
548 #[test]
549 fn test_nice_path_buf_tmp_unix() {
550 let path = NicePathBuf::new(Path::new("/tmp/hello.world"));
551
552 assert_eq!("/tmp/hello.world", format!("{path}"));
553 assert_eq!("\"/tmp/hello.world\"", format!("{path:?}"));
554
555 let path = NicePathBuf::new(Path::new("//tmp//hello.world"));
556
557 assert_eq!("/tmp/hello.world", format!("{path}"));
558 assert_eq!("\"/tmp/hello.world\"", format!("{path:?}"));
559
560 let path = NicePathBuf::new(Path::new("//does-not-exist-anywhere/..//tmp//hello.world"));
561
562 assert_eq!("/tmp/hello.world", format!("{path}"));
563 assert_eq!("\"/tmp/hello.world\"", format!("{path:?}"));
564
565 let path = NicePathBuf::new(
566 Path::new("/tmp")
567 .canonicalize()
568 .unwrap()
569 .join("hello.world"),
570 );
571
572 assert_eq!("/tmp/hello.world", format!("{path}"));
573 assert_eq!("\"/tmp/hello.world\"", format!("{path:?}"));
574
575 let temp_dir = NiceTempDir::new();
577 let path = temp_dir.join("a/b/c/d");
578
579 let name = temp_dir.file_name().unwrap().to_string_lossy();
580
581 assert_eq!(format!("/tmp/{name}/a/b/c/d"), format!("{}", path));
582 assert_eq!(format!("\"/tmp/{name}/a/b/c/d\""), format!("{:?}", path));
583 }
584
585 #[cfg(windows)]
586 #[test]
587 fn test_nice_path_buf_tmp_windows() {
588 let tmp = std::env::temp_dir();
589 let tmp = tmp.join("hello.world");
590
591 let path = NicePathBuf::new(&tmp);
592
593 assert_eq!(r"%TEMP%\hello.world", format!("{}", path));
594 assert_eq!(r"<%TEMP%\hello.world>", format!("{:?}", path));
595
596 let path = NicePathBuf::new(
597 &std::env::temp_dir()
598 .canonicalize()
599 .unwrap()
600 .join("hello.world"),
601 );
602
603 assert_eq!(r"%TEMP%\hello.world", format!("{}", path));
604 assert_eq!(r"<%TEMP%\hello.world>", format!("{:?}", path));
605
606 let path = NicePathBuf::new(r#"C:\directory"#);
607
608 assert_eq!(r"C:\directory", format!("{}", path));
609 assert_eq!(r"<C:\directory>", format!("{:?}", path));
610 }
611
612 #[test]
613 fn test_shell_split() {
614 assert_eq!(format!("{:?}", shell_split("").unwrap()), r#"[]"#);
615 assert_eq!(format!("{:?}", shell_split("a").unwrap()), r#"["a"]"#);
616 assert_eq!(
617 format!("{:?}", shell_split("a b").unwrap()),
618 r#"["a", "b"]"#
619 );
620 assert_eq!(
621 format!("{:?}", shell_split("a b c").unwrap()),
622 r#"["a", "b", "c"]"#
623 );
624 assert_eq!(
625 format!("{:?}", shell_split("a 'b' c").unwrap()),
626 r#"["a", "b", "c"]"#
627 );
628 assert_eq!(
629 format!("{:?}", shell_split("a 'b c' d").unwrap()),
630 r#"["a", "b c", "d"]"#
631 );
632 assert_eq!(
633 format!("{:?}", shell_split(r#"a "b" c"#).unwrap()),
634 r#"["a", "b", "c"]"#
635 );
636 assert_eq!(
637 format!("{:?}", shell_split(r#"a "b c" d"#).unwrap()),
638 r#"["a", "b c", "d"]"#
639 );
640 assert_eq!(
641 format!("{:?}", shell_split(r#"a "b\"c" d"#).unwrap()),
642 r#"["a", "b\"c", "d"]"#
643 );
644 assert_eq!(
645 format!("{:?}", shell_split(r#"a "b\'c" d"#).unwrap()),
646 r#"["a", "b\\'c", "d"]"#
647 );
648 assert_eq!(
649 format!("{:?}", shell_split(r#"a "b\nc" d"#).unwrap()),
650 r#"["a", "b\nc", "d"]"#
651 );
652 assert_eq!(
653 format!("{:?}", shell_split(r#"a "a\\b" d"#).unwrap()),
654 r#"["a", "a\\\\b", "d"]"#
655 );
656 assert_eq!(
657 format!("{:?}", shell_split(r#"a 'a\\b' d"#).unwrap()),
658 r#"["a", "a\\\\b", "d"]"#
659 );
660 }
661
662 #[test]
663 fn test_shell_split_errors() {
664 assert_eq!(
665 shell_split("a 'b").unwrap_err(),
666 ShellParseError::UnmatchedQuote('\'')
667 );
668 assert_eq!(
669 shell_split("a \"b c").unwrap_err(),
670 ShellParseError::UnmatchedQuote('"')
671 );
672 assert_eq!(
673 shell_split("a '").unwrap_err(),
674 ShellParseError::UnmatchedQuote('\'')
675 );
676 }
677}