1use crate::paths::{PathStyle, is_absolute};
2use anyhow::{Context as _, Result, anyhow};
3use serde::{Deserialize, Serialize};
4use std::{
5 borrow::{Borrow, Cow},
6 fmt,
7 ops::Deref,
8 path::{Path, PathBuf},
9 sync::Arc,
10};
11
12/// A file system path that is guaranteed to be relative and normalized.
13///
14/// This type can be used to represent paths in a uniform way, regardless of
15/// whether they refer to Windows or POSIX file systems, and regardless of
16/// the host platform.
17///
18/// Internally, paths are stored in POSIX ('/'-delimited) format, but they can
19/// be displayed in either POSIX or Windows format.
20///
21/// Relative paths are also guaranteed to be valid unicode.
22#[repr(transparent)]
23#[derive(PartialEq, Eq, Hash, Serialize)]
24pub struct RelPath(str);
25
26/// An owned representation of a file system path that is guaranteed to be
27/// relative and normalized.
28///
29/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
30#[derive(PartialEq, Eq, Clone, Ord, PartialOrd, Serialize)]
31pub struct RelPathBuf(String);
32
33impl RelPath {
34 /// Creates an empty [`RelPath`].
35 pub fn empty() -> &'static Self {
36 Self::new_unchecked("")
37 }
38
39 /// Converts a path with a given style into a [`RelPath`].
40 ///
41 /// Returns an error if the path is absolute, or is not valid unicode.
42 ///
43 /// This method will normalize the path by removing `.` components,
44 /// processing `..` components, and removing trailing separators. It does
45 /// not allocate unless it's necessary to reformat the path.
46 #[track_caller]
47 pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result<Cow<'a, Self>> {
48 let mut path = path.to_str().context("non utf-8 path")?;
49
50 let (prefixes, suffixes): (&[_], &[_]) = match path_style {
51 PathStyle::Posix => (&["./"], &['/']),
52 PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
53 };
54
55 while prefixes.iter().any(|prefix| path.starts_with(prefix)) {
56 path = &path[prefixes[0].len()..];
57 }
58 while let Some(prefix) = path.strip_suffix(suffixes)
59 && !prefix.is_empty()
60 {
61 path = prefix;
62 }
63
64 if is_absolute(&path, path_style) {
65 return Err(anyhow!("absolute path not allowed: {path:?}"));
66 }
67
68 let mut string = Cow::Borrowed(path);
69 if path_style == PathStyle::Windows && path.contains('\\') {
70 string = Cow::Owned(string.as_ref().replace('\\', "/"))
71 }
72
73 let mut result = match string {
74 Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)),
75 Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
76 };
77
78 if result
79 .components()
80 .any(|component| component == "" || component == "." || component == "..")
81 {
82 let mut normalized = RelPathBuf::new();
83 for component in result.components() {
84 match component {
85 "" => {}
86 "." => {}
87 ".." => {
88 if !normalized.pop() {
89 return Err(anyhow!("path is not relative: {result:?}"));
90 }
91 }
92 other => normalized.push(RelPath::new_unchecked(other)),
93 }
94 }
95 result = Cow::Owned(normalized)
96 }
97
98 Ok(result)
99 }
100
101 /// Converts a path that is already normalized and uses '/' separators
102 /// into a [`RelPath`] .
103 ///
104 /// Returns an error if the path is not already in the correct format.
105 #[track_caller]
106 pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
107 let path = path.as_ref();
108 match Self::new(path, PathStyle::Posix)? {
109 Cow::Borrowed(path) => Ok(path),
110 Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
111 }
112 }
113
114 fn new_unchecked(s: &str) -> &Self {
115 // Safety: `RelPath` is a transparent wrapper around `str`.
116 unsafe { &*(s as *const str as *const Self) }
117 }
118
119 pub fn is_empty(&self) -> bool {
120 self.0.is_empty()
121 }
122
123 pub fn components(&self) -> RelPathComponents<'_> {
124 RelPathComponents(&self.0)
125 }
126
127 pub fn ancestors(&self) -> RelPathAncestors<'_> {
128 RelPathAncestors(Some(&self.0))
129 }
130
131 pub fn file_name(&self) -> Option<&str> {
132 self.components().next_back()
133 }
134
135 pub fn file_stem(&self) -> Option<&str> {
136 Some(self.as_std_path().file_stem()?.to_str().unwrap())
137 }
138
139 pub fn extension(&self) -> Option<&str> {
140 Some(self.as_std_path().extension()?.to_str().unwrap())
141 }
142
143 pub fn parent(&self) -> Option<&Self> {
144 let mut components = self.components();
145 components.next_back()?;
146 Some(components.rest())
147 }
148
149 pub fn starts_with(&self, other: &Self) -> bool {
150 self.strip_prefix(other).is_ok()
151 }
152
153 pub fn ends_with(&self, other: &Self) -> bool {
154 if let Some(suffix) = self.0.strip_suffix(&other.0) {
155 if suffix.ends_with('/') {
156 return true;
157 } else if suffix.is_empty() {
158 return true;
159 }
160 }
161 false
162 }
163
164 pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self, StripPrefixError> {
165 if other.is_empty() {
166 return Ok(self);
167 }
168 if let Some(suffix) = self.0.strip_prefix(&other.0) {
169 if let Some(suffix) = suffix.strip_prefix('/') {
170 return Ok(Self::new_unchecked(suffix));
171 } else if suffix.is_empty() {
172 return Ok(Self::empty());
173 }
174 }
175 Err(StripPrefixError)
176 }
177
178 pub fn len(&self) -> usize {
179 self.0.matches('/').count() + 1
180 }
181
182 pub fn last_n_components(&self, count: usize) -> Option<&Self> {
183 let len = self.len();
184 if len >= count {
185 let mut components = self.components();
186 for _ in 0..(len - count) {
187 components.next()?;
188 }
189 Some(components.rest())
190 } else {
191 None
192 }
193 }
194
195 pub fn join(&self, other: &Self) -> Arc<Self> {
196 let result = if self.0.is_empty() {
197 Cow::Borrowed(&other.0)
198 } else if other.0.is_empty() {
199 Cow::Borrowed(&self.0)
200 } else {
201 Cow::Owned(format!("{}/{}", &self.0, &other.0))
202 };
203 Arc::from(Self::new_unchecked(result.as_ref()))
204 }
205
206 pub fn to_rel_path_buf(&self) -> RelPathBuf {
207 RelPathBuf(self.0.to_string())
208 }
209
210 pub fn into_arc(&self) -> Arc<Self> {
211 Arc::from(self)
212 }
213
214 /// Convert the path into the wire representation.
215 pub fn to_proto(&self) -> String {
216 self.as_unix_str().to_owned()
217 }
218
219 /// Load the path from its wire representation.
220 pub fn from_proto(path: &str) -> Result<Arc<Self>> {
221 Ok(Arc::from(Self::unix(path)?))
222 }
223
224 /// Convert the path into a string with the given path style.
225 ///
226 /// Whenever a path is presented to the user, it should be converted to
227 /// a string via this method.
228 pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
229 match style {
230 PathStyle::Posix => Cow::Borrowed(&self.0),
231 PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")),
232 PathStyle::Windows => Cow::Borrowed(&self.0),
233 }
234 }
235
236 /// Get the internal unix-style representation of the path.
237 ///
238 /// This should not be shown to the user.
239 pub fn as_unix_str(&self) -> &str {
240 &self.0
241 }
242
243 /// Interprets the path as a [`std::path::Path`], suitable for file system calls.
244 ///
245 /// This is guaranteed to be a valid path regardless of the host platform, because
246 /// the `/` is accepted as a path separator on windows.
247 ///
248 /// This should not be shown to the user.
249 pub fn as_std_path(&self) -> &Path {
250 Path::new(&self.0)
251 }
252}
253
254#[derive(Debug)]
255pub struct StripPrefixError;
256
257impl std::fmt::Display for StripPrefixError {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 f.write_str("prefix not found")
260 }
261}
262
263impl std::error::Error for StripPrefixError {}
264
265impl ToOwned for RelPath {
266 type Owned = RelPathBuf;
267
268 fn to_owned(&self) -> Self::Owned {
269 self.to_rel_path_buf()
270 }
271}
272
273impl Borrow<RelPath> for RelPathBuf {
274 fn borrow(&self) -> &RelPath {
275 self.as_rel_path()
276 }
277}
278
279impl PartialOrd for RelPath {
280 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
281 Some(self.cmp(other))
282 }
283}
284
285impl Ord for RelPath {
286 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
287 self.components().cmp(other.components())
288 }
289}
290
291impl fmt::Debug for RelPath {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 fmt::Debug::fmt(&self.0, f)
294 }
295}
296
297impl fmt::Debug for RelPathBuf {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 fmt::Debug::fmt(&self.0, f)
300 }
301}
302
303impl RelPathBuf {
304 pub fn new() -> Self {
305 Self(String::new())
306 }
307
308 pub fn pop(&mut self) -> bool {
309 if let Some(ix) = self.0.rfind('/') {
310 self.0.truncate(ix);
311 true
312 } else if !self.is_empty() {
313 self.0.clear();
314 true
315 } else {
316 false
317 }
318 }
319
320 pub fn push(&mut self, path: &RelPath) {
321 if !self.is_empty() {
322 self.0.push('/');
323 }
324 self.0.push_str(&path.0);
325 }
326
327 pub fn as_rel_path(&self) -> &RelPath {
328 RelPath::new_unchecked(self.0.as_str())
329 }
330
331 pub fn set_extension(&mut self, extension: &str) -> bool {
332 if let Some(filename) = self.file_name() {
333 let mut filename = PathBuf::from(filename);
334 filename.set_extension(extension);
335 self.pop();
336 self.0.push_str(filename.to_str().unwrap());
337 true
338 } else {
339 false
340 }
341 }
342}
343
344impl<'de> Deserialize<'de> for RelPathBuf {
345 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
346 where
347 D: serde::Deserializer<'de>,
348 {
349 let path = String::deserialize(deserializer)?;
350 let rel_path =
351 RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
352 Ok(rel_path.into_owned())
353 }
354}
355
356impl Into<Arc<RelPath>> for RelPathBuf {
357 fn into(self) -> Arc<RelPath> {
358 Arc::from(self.as_rel_path())
359 }
360}
361
362impl AsRef<Path> for RelPathBuf {
363 fn as_ref(&self) -> &Path {
364 self.as_std_path()
365 }
366}
367
368impl AsRef<Path> for RelPath {
369 fn as_ref(&self) -> &Path {
370 self.as_std_path()
371 }
372}
373
374impl AsRef<RelPath> for RelPathBuf {
375 fn as_ref(&self) -> &RelPath {
376 self.as_rel_path()
377 }
378}
379
380impl AsRef<RelPath> for RelPath {
381 fn as_ref(&self) -> &RelPath {
382 self
383 }
384}
385
386impl Deref for RelPathBuf {
387 type Target = RelPath;
388
389 fn deref(&self) -> &Self::Target {
390 self.as_ref()
391 }
392}
393
394impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
395 fn from(value: &'a RelPath) -> Self {
396 Self::Borrowed(value)
397 }
398}
399
400impl From<&RelPath> for Arc<RelPath> {
401 fn from(rel_path: &RelPath) -> Self {
402 let bytes: Arc<str> = Arc::from(&rel_path.0);
403 unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
404 }
405}
406
407#[cfg(any(test, feature = "test-support"))]
408#[track_caller]
409pub fn rel_path(path: &str) -> &RelPath {
410 RelPath::unix(path).unwrap()
411}
412
413#[cfg(any(test, feature = "test-support"))]
414#[track_caller]
415pub fn rel_path_buf(path: &str) -> RelPathBuf {
416 RelPath::unix(path).unwrap().to_rel_path_buf()
417}
418
419impl PartialEq<str> for RelPath {
420 fn eq(&self, other: &str) -> bool {
421 self.0 == *other
422 }
423}
424
425pub trait PathExt {
426 fn to_rel_path_buf(&self) -> Result<RelPathBuf>;
427}
428
429impl<T: AsRef<Path> + ?Sized> PathExt for T {
430 fn to_rel_path_buf(&self) -> Result<RelPathBuf> {
431 Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
432 }
433}
434
435#[derive(Default)]
436pub struct RelPathComponents<'a>(&'a str);
437
438pub struct RelPathAncestors<'a>(Option<&'a str>);
439
440const SEPARATOR: char = '/';
441
442impl<'a> RelPathComponents<'a> {
443 pub fn rest(&self) -> &'a RelPath {
444 RelPath::new_unchecked(self.0)
445 }
446}
447
448impl<'a> Iterator for RelPathComponents<'a> {
449 type Item = &'a str;
450
451 fn next(&mut self) -> Option<Self::Item> {
452 if let Some(sep_ix) = self.0.find(SEPARATOR) {
453 let (head, tail) = self.0.split_at(sep_ix);
454 self.0 = &tail[1..];
455 Some(head)
456 } else if self.0.is_empty() {
457 None
458 } else {
459 let result = self.0;
460 self.0 = "";
461 Some(result)
462 }
463 }
464}
465
466impl<'a> Iterator for RelPathAncestors<'a> {
467 type Item = &'a RelPath;
468
469 fn next(&mut self) -> Option<Self::Item> {
470 let result = self.0?;
471 if let Some(sep_ix) = result.rfind(SEPARATOR) {
472 self.0 = Some(&result[..sep_ix]);
473 } else if !result.is_empty() {
474 self.0 = Some("");
475 } else {
476 self.0 = None;
477 }
478 Some(RelPath::new_unchecked(result))
479 }
480}
481
482impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
483 fn next_back(&mut self) -> Option<Self::Item> {
484 if let Some(sep_ix) = self.0.rfind(SEPARATOR) {
485 let (head, tail) = self.0.split_at(sep_ix);
486 self.0 = head;
487 Some(&tail[1..])
488 } else if self.0.is_empty() {
489 None
490 } else {
491 let result = self.0;
492 self.0 = "";
493 Some(result)
494 }
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use itertools::Itertools;
502 use pretty_assertions::assert_matches;
503
504 #[test]
505 fn test_rel_path_new() {
506 assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err());
507 assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err());
508 assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err());
509
510 let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
511 assert_eq!(path, rel_path("foo").into());
512 assert_matches!(path, Cow::Borrowed(_));
513
514 let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
515 assert_eq!(path, rel_path("foo").into());
516 assert_matches!(path, Cow::Borrowed(_));
517
518 assert_eq!(
519 RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
520 .unwrap()
521 .as_ref(),
522 rel_path("foo/baz/quux")
523 );
524
525 let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap();
526 assert_eq!(path.as_ref(), rel_path("foo/bar"));
527 assert_matches!(path, Cow::Borrowed(_));
528
529 let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
530 assert_eq!(path, rel_path("foo").into());
531 assert_matches!(path, Cow::Borrowed(_));
532
533 let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
534 assert_eq!(path, rel_path("foo").into());
535 assert_matches!(path, Cow::Borrowed(_));
536
537 let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap();
538 assert_eq!(path.as_ref(), rel_path("foo/bar"));
539 assert_matches!(path, Cow::Owned(_));
540
541 let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
542 assert_eq!(path.as_ref(), rel_path("foo/bar"));
543 assert_matches!(path, Cow::Borrowed(_));
544
545 let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
546 assert_eq!(path.as_ref(), rel_path("foo/bar"));
547 assert_matches!(path, Cow::Owned(_));
548 }
549
550 #[test]
551 fn test_rel_path_components() {
552 let path = rel_path("foo/bar/baz");
553 assert_eq!(
554 path.components().collect::<Vec<_>>(),
555 vec!["foo", "bar", "baz"]
556 );
557 assert_eq!(
558 path.components().rev().collect::<Vec<_>>(),
559 vec!["baz", "bar", "foo"]
560 );
561
562 let path = rel_path("");
563 let mut components = path.components();
564 assert_eq!(components.next(), None);
565 }
566
567 #[test]
568 fn test_rel_path_ancestors() {
569 let path = rel_path("foo/bar/baz");
570 let mut ancestors = path.ancestors();
571 assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
572 assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
573 assert_eq!(ancestors.next(), Some(rel_path("foo")));
574 assert_eq!(ancestors.next(), Some(rel_path("")));
575 assert_eq!(ancestors.next(), None);
576
577 let path = rel_path("foo");
578 let mut ancestors = path.ancestors();
579 assert_eq!(ancestors.next(), Some(rel_path("foo")));
580 assert_eq!(ancestors.next(), Some(RelPath::empty()));
581 assert_eq!(ancestors.next(), None);
582
583 let path = RelPath::empty();
584 let mut ancestors = path.ancestors();
585 assert_eq!(ancestors.next(), Some(RelPath::empty()));
586 assert_eq!(ancestors.next(), None);
587 }
588
589 #[test]
590 fn test_rel_path_parent() {
591 assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar")));
592 assert_eq!(rel_path("foo").parent(), Some(RelPath::empty()));
593 assert_eq!(rel_path("").parent(), None);
594 }
595
596 #[test]
597 fn test_rel_path_partial_ord_is_compatible_with_std() {
598 let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
599 for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
600 assert_eq!(
601 Path::new(lhs).cmp(Path::new(rhs)),
602 RelPath::unix(lhs)
603 .unwrap()
604 .cmp(&RelPath::unix(rhs).unwrap())
605 );
606 }
607 }
608
609 #[test]
610 fn test_strip_prefix() {
611 let parent = rel_path("");
612 let child = rel_path(".foo");
613
614 assert!(child.starts_with(parent));
615 assert_eq!(child.strip_prefix(parent).unwrap(), child);
616 }
617
618 #[test]
619 fn test_rel_path_constructors_absolute_path() {
620 assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
621 assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
622 assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err());
623 assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
624 assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
625 assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
626 }
627
628 #[test]
629 fn test_pop() {
630 let mut path = rel_path("a/b").to_rel_path_buf();
631 path.pop();
632 assert_eq!(path.as_rel_path().as_unix_str(), "a");
633 path.pop();
634 assert_eq!(path.as_rel_path().as_unix_str(), "");
635 path.pop();
636 assert_eq!(path.as_rel_path().as_unix_str(), "");
637 }
638}