1use std::borrow::Cow;
2use std::path::{Component, Path, PathBuf};
3use std::sync::LazyLock;
4
5use either::Either;
6use path_slash::PathExt;
7
8#[allow(clippy::exit, clippy::print_stderr)]
10pub static CWD: LazyLock<PathBuf> = LazyLock::new(|| {
11 std::env::current_dir().unwrap_or_else(|_e| {
12 eprintln!("Current directory does not exist");
13 std::process::exit(1);
14 })
15});
16
17pub trait Simplified {
18 fn simplified(&self) -> &Path;
22
23 fn simplified_display(&self) -> impl std::fmt::Display;
28
29 fn simple_canonicalize(&self) -> std::io::Result<PathBuf>;
33
34 fn user_display(&self) -> impl std::fmt::Display;
38
39 fn user_display_from(&self, base: impl AsRef<Path>) -> impl std::fmt::Display;
44
45 fn portable_display(&self) -> impl std::fmt::Display;
49}
50
51impl<T: AsRef<Path>> Simplified for T {
52 fn simplified(&self) -> &Path {
53 dunce::simplified(self.as_ref())
54 }
55
56 fn simplified_display(&self) -> impl std::fmt::Display {
57 dunce::simplified(self.as_ref()).display()
58 }
59
60 fn simple_canonicalize(&self) -> std::io::Result<PathBuf> {
61 dunce::canonicalize(self.as_ref())
62 }
63
64 fn user_display(&self) -> impl std::fmt::Display {
65 let path = dunce::simplified(self.as_ref());
66
67 if CWD.ancestors().nth(1).is_none() {
69 return path.display();
70 }
71
72 let path = path.strip_prefix(CWD.simplified()).unwrap_or(path);
75
76 if path.as_os_str() == "" {
77 return Path::new(".").display();
79 }
80
81 path.display()
82 }
83
84 fn user_display_from(&self, base: impl AsRef<Path>) -> impl std::fmt::Display {
85 let path = dunce::simplified(self.as_ref());
86
87 if CWD.ancestors().nth(1).is_none() {
89 return path.display();
90 }
91
92 let path = path
95 .strip_prefix(base.as_ref())
96 .unwrap_or_else(|_| path.strip_prefix(CWD.simplified()).unwrap_or(path));
97
98 if path.as_os_str() == "" {
99 return Path::new(".").display();
101 }
102
103 path.display()
104 }
105
106 fn portable_display(&self) -> impl std::fmt::Display {
107 let path = dunce::simplified(self.as_ref());
108
109 let path = path.strip_prefix(CWD.simplified()).unwrap_or(path);
112
113 path.to_slash()
115 .map(Either::Left)
116 .unwrap_or_else(|| Either::Right(path.display()))
117 }
118}
119
120pub trait PythonExt {
121 fn escape_for_python(&self) -> String;
123}
124
125impl<T: AsRef<Path>> PythonExt for T {
126 fn escape_for_python(&self) -> String {
127 self.as_ref()
128 .to_string_lossy()
129 .replace('\\', "\\\\")
130 .replace('"', "\\\"")
131 }
132}
133
134pub fn normalize_url_path(path: &str) -> Cow<'_, str> {
141 let path = percent_encoding::percent_decode_str(path)
143 .decode_utf8()
144 .unwrap_or(Cow::Borrowed(path));
145
146 if cfg!(windows) {
148 Cow::Owned(
149 path.strip_prefix('/')
150 .unwrap_or(&path)
151 .replace('/', std::path::MAIN_SEPARATOR_STR),
152 )
153 } else {
154 path
155 }
156}
157
158pub fn normalize_absolute_path(path: &Path) -> Result<PathBuf, std::io::Error> {
175 let mut components = path.components().peekable();
176 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
177 components.next();
178 PathBuf::from(c.as_os_str())
179 } else {
180 PathBuf::new()
181 };
182
183 for component in components {
184 match component {
185 Component::Prefix(..) => unreachable!(),
186 Component::RootDir => {
187 ret.push(component.as_os_str());
188 }
189 Component::CurDir => {}
190 Component::ParentDir => {
191 if !ret.pop() {
192 return Err(std::io::Error::new(
193 std::io::ErrorKind::InvalidInput,
194 format!(
195 "cannot normalize a relative path beyond the base directory: {}",
196 path.display()
197 ),
198 ));
199 }
200 }
201 Component::Normal(c) => {
202 ret.push(c);
203 }
204 }
205 }
206 Ok(ret)
207}
208
209pub fn normalize_path(path: &Path) -> Cow<'_, Path> {
211 if path.components().all(|component| match component {
213 Component::Prefix(_) | Component::RootDir | Component::Normal(_) => true,
214 Component::ParentDir | Component::CurDir => false,
215 }) {
216 Cow::Borrowed(path)
217 } else {
218 Cow::Owned(normalized(path))
219 }
220}
221
222pub fn normalize_path_buf(path: PathBuf) -> PathBuf {
224 if path.components().all(|component| match component {
226 Component::Prefix(_) | Component::RootDir | Component::Normal(_) => true,
227 Component::ParentDir | Component::CurDir => false,
228 }) {
229 path
230 } else {
231 normalized(&path)
232 }
233}
234
235fn normalized(path: &Path) -> PathBuf {
253 let mut normalized = PathBuf::new();
254 for component in path.components() {
255 match component {
256 Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
257 normalized.push(component);
259 }
260 Component::ParentDir => {
261 match normalized.components().next_back() {
262 None | Some(Component::ParentDir | Component::RootDir) => {
263 normalized.push(component);
265 }
266 Some(Component::Normal(_) | Component::Prefix(_) | Component::CurDir) => {
267 normalized.pop();
269 }
270 }
271 }
272 Component::CurDir => {
273 }
275 }
276 }
277 normalized
278}
279
280pub fn relative_to(
289 path: impl AsRef<Path>,
290 base: impl AsRef<Path>,
291) -> Result<PathBuf, std::io::Error> {
292 let path = normalize_path(path.as_ref());
294 let base = normalize_path(base.as_ref());
295
296 let (stripped, common_prefix) = base
298 .ancestors()
299 .find_map(|ancestor| {
300 dunce::simplified(&path)
302 .strip_prefix(dunce::simplified(ancestor))
303 .ok()
304 .map(|stripped| (stripped, ancestor))
305 })
306 .ok_or_else(|| {
307 std::io::Error::other(format!(
308 "Trivial strip failed: {} vs. {}",
309 path.simplified_display(),
310 base.simplified_display()
311 ))
312 })?;
313
314 let levels_up = base.components().count() - common_prefix.components().count();
316 let up = std::iter::repeat_n("..", levels_up).collect::<PathBuf>();
317
318 Ok(up.join(stripped))
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct PortablePath<'a>(&'a Path);
327
328#[derive(Debug, Clone, PartialEq, Eq)]
329pub struct PortablePathBuf(Box<Path>);
330
331#[cfg(feature = "schemars")]
332impl schemars::JsonSchema for PortablePathBuf {
333 fn schema_name() -> Cow<'static, str> {
334 Cow::Borrowed("PortablePathBuf")
335 }
336
337 fn json_schema(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
338 PathBuf::json_schema(_gen)
339 }
340}
341
342impl AsRef<Path> for PortablePath<'_> {
343 fn as_ref(&self) -> &Path {
344 self.0
345 }
346}
347
348impl<'a, T> From<&'a T> for PortablePath<'a>
349where
350 T: AsRef<Path> + ?Sized,
351{
352 fn from(path: &'a T) -> Self {
353 PortablePath(path.as_ref())
354 }
355}
356
357impl std::fmt::Display for PortablePath<'_> {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 let path = self.0.to_slash_lossy();
360 if path.is_empty() {
361 write!(f, ".")
362 } else {
363 write!(f, "{path}")
364 }
365 }
366}
367
368impl std::fmt::Display for PortablePathBuf {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 let path = self.0.to_slash_lossy();
371 if path.is_empty() {
372 write!(f, ".")
373 } else {
374 write!(f, "{path}")
375 }
376 }
377}
378
379impl From<&str> for PortablePathBuf {
380 fn from(path: &str) -> Self {
381 if path == "." {
382 Self(PathBuf::new().into_boxed_path())
383 } else {
384 Self(PathBuf::from(path).into_boxed_path())
385 }
386 }
387}
388
389impl From<PortablePathBuf> for Box<Path> {
390 fn from(portable: PortablePathBuf) -> Self {
391 portable.0
392 }
393}
394
395impl From<Box<Path>> for PortablePathBuf {
396 fn from(path: Box<Path>) -> Self {
397 Self(path)
398 }
399}
400
401impl<'a> From<&'a Path> for PortablePathBuf {
402 fn from(path: &'a Path) -> Self {
403 Box::<Path>::from(path).into()
404 }
405}
406
407#[cfg(feature = "serde")]
408impl serde::Serialize for PortablePathBuf {
409 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
410 where
411 S: serde::ser::Serializer,
412 {
413 self.to_string().serialize(serializer)
414 }
415}
416
417#[cfg(feature = "serde")]
418impl serde::Serialize for PortablePath<'_> {
419 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
420 where
421 S: serde::ser::Serializer,
422 {
423 self.to_string().serialize(serializer)
424 }
425}
426
427#[cfg(feature = "serde")]
428impl<'de> serde::de::Deserialize<'de> for PortablePathBuf {
429 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
430 where
431 D: serde::de::Deserializer<'de>,
432 {
433 let s = String::deserialize(deserializer)?;
434 if s == "." {
435 Ok(Self(PathBuf::new().into_boxed_path()))
436 } else {
437 Ok(Self(PathBuf::from(s).into_boxed_path()))
438 }
439 }
440}
441
442impl AsRef<Path> for PortablePathBuf {
443 fn as_ref(&self) -> &Path {
444 &self.0
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn test_normalize_url() {
454 if cfg!(windows) {
455 assert_eq!(
456 normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"),
457 "C:\\Users\\ferris\\wheel-0.42.0.tar.gz"
458 );
459 } else {
460 assert_eq!(
461 normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"),
462 "/C:/Users/ferris/wheel-0.42.0.tar.gz"
463 );
464 }
465
466 if cfg!(windows) {
467 assert_eq!(
468 normalize_url_path("./ferris/wheel-0.42.0.tar.gz"),
469 ".\\ferris\\wheel-0.42.0.tar.gz"
470 );
471 } else {
472 assert_eq!(
473 normalize_url_path("./ferris/wheel-0.42.0.tar.gz"),
474 "./ferris/wheel-0.42.0.tar.gz"
475 );
476 }
477
478 if cfg!(windows) {
479 assert_eq!(
480 normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"),
481 ".\\wheel cache\\wheel-0.42.0.tar.gz"
482 );
483 } else {
484 assert_eq!(
485 normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"),
486 "./wheel cache/wheel-0.42.0.tar.gz"
487 );
488 }
489 }
490
491 #[test]
492 fn test_normalize_path() {
493 let path = Path::new("/a/b/../c/./d");
494 let normalized = normalize_absolute_path(path).unwrap();
495 assert_eq!(normalized, Path::new("/a/c/d"));
496
497 let path = Path::new("/a/../c/./d");
498 let normalized = normalize_absolute_path(path).unwrap();
499 assert_eq!(normalized, Path::new("/c/d"));
500
501 let path = Path::new("/a/../../c/./d");
503 let err = normalize_absolute_path(path).unwrap_err();
504 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
505 }
506
507 #[test]
508 fn test_relative_to() {
509 assert_eq!(
510 relative_to(
511 Path::new("/home/ferris/carcinization/lib/python/site-packages/foo/__init__.py"),
512 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
513 )
514 .unwrap(),
515 Path::new("foo/__init__.py")
516 );
517 assert_eq!(
518 relative_to(
519 Path::new("/home/ferris/carcinization/lib/marker.txt"),
520 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
521 )
522 .unwrap(),
523 Path::new("../../marker.txt")
524 );
525 assert_eq!(
526 relative_to(
527 Path::new("/home/ferris/carcinization/bin/foo_launcher"),
528 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
529 )
530 .unwrap(),
531 Path::new("../../../bin/foo_launcher")
532 );
533 }
534
535 #[test]
536 fn test_normalize_relative() {
537 let cases = [
538 (
539 "../../workspace-git-path-dep-test/packages/c/../../packages/d",
540 "../../workspace-git-path-dep-test/packages/d",
541 ),
542 (
543 "workspace-git-path-dep-test/packages/c/../../packages/d",
544 "workspace-git-path-dep-test/packages/d",
545 ),
546 ("./a/../../b", "../b"),
547 ("/usr/../../foo", "/../foo"),
548 ];
549 for (input, expected) in cases {
550 assert_eq!(normalize_path(Path::new(input)), Path::new(expected));
551 }
552 }
553}