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#[repr(transparent)]
13#[derive(PartialEq, Eq, Hash, Serialize)]
14pub struct RelPath(str);
15
16#[derive(Clone, Serialize, Deserialize)]
17pub struct RelPathBuf(String);
18
19impl RelPath {
20 pub fn empty() -> &'static Self {
21 unsafe { Self::new_unchecked("") }
22 }
23
24 #[track_caller]
25 pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result<Cow<'a, Self>> {
26 let mut path = path.to_str().context("non utf-8 path")?;
27
28 let (prefixes, suffixes): (&[_], &[_]) = match path_style {
29 PathStyle::Posix => (&["./"], &['/']),
30 PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
31 };
32
33 while prefixes.iter().any(|prefix| path.starts_with(prefix)) {
34 path = &path[prefixes[0].len()..];
35 }
36 while let Some(prefix) = path.strip_suffix(suffixes)
37 && !prefix.is_empty()
38 {
39 path = prefix;
40 }
41
42 if is_absolute(&path, path_style) {
43 return Err(anyhow!("absolute path not allowed: {path:?}"));
44 }
45
46 let mut string = Cow::Borrowed(path);
47 if path_style == PathStyle::Windows && path.contains('\\') {
48 string = Cow::Owned(string.as_ref().replace('\\', "/"))
49 }
50
51 let mut result = match string {
52 Cow::Borrowed(string) => Cow::Borrowed(unsafe { Self::new_unchecked(string) }),
53 Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
54 };
55
56 if result
57 .components()
58 .any(|component| component == "" || component == "." || component == "..")
59 {
60 let mut normalized = RelPathBuf::new();
61 for component in result.components() {
62 match component {
63 "" => {}
64 "." => {}
65 ".." => {
66 if !normalized.pop() {
67 return Err(anyhow!("path is not relative: {result:?}"));
68 }
69 }
70 other => normalized.push(&RelPath::unix(other)?),
71 }
72 }
73 result = Cow::Owned(normalized)
74 }
75
76 Ok(result)
77 }
78
79 #[track_caller]
80 pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
81 let path = path.as_ref();
82 match Self::new(path, PathStyle::Posix)? {
83 Cow::Borrowed(path) => Ok(path),
84 Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
85 }
86 }
87
88 unsafe fn new_unchecked(s: &str) -> &Self {
89 unsafe { &*(s as *const str as *const Self) }
90 }
91
92 pub fn is_empty(&self) -> bool {
93 self.0.is_empty()
94 }
95
96 pub fn components(&self) -> RelPathComponents<'_> {
97 RelPathComponents(&self.0)
98 }
99
100 pub fn ancestors(&self) -> RelPathAncestors<'_> {
101 RelPathAncestors(Some(&self.0))
102 }
103
104 pub fn file_name(&self) -> Option<&str> {
105 self.components().next_back()
106 }
107
108 pub fn file_stem(&self) -> Option<&str> {
109 Some(self.as_std_path().file_stem()?.to_str().unwrap())
110 }
111
112 pub fn extension(&self) -> Option<&str> {
113 Some(self.as_std_path().extension()?.to_str().unwrap())
114 }
115
116 pub fn parent(&self) -> Option<&Self> {
117 let mut components = self.components();
118 components.next_back()?;
119 Some(components.rest())
120 }
121
122 pub fn starts_with(&self, other: &Self) -> bool {
123 self.strip_prefix(other).is_ok()
124 }
125
126 pub fn ends_with(&self, other: &Self) -> bool {
127 if let Some(suffix) = self.0.strip_suffix(&other.0) {
128 if suffix.ends_with('/') {
129 return true;
130 } else if suffix.is_empty() {
131 return true;
132 }
133 }
134 false
135 }
136
137 pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self> {
138 if other.is_empty() {
139 return Ok(self);
140 }
141 if let Some(suffix) = self.0.strip_prefix(&other.0) {
142 if let Some(suffix) = suffix.strip_prefix('/') {
143 return Ok(unsafe { Self::new_unchecked(suffix) });
144 } else if suffix.is_empty() {
145 return Ok(Self::empty());
146 }
147 }
148 Err(anyhow!("failed to strip prefix: {other:?} from {self:?}"))
149 }
150
151 pub fn len(&self) -> usize {
152 self.0.matches('/').count() + 1
153 }
154
155 pub fn last_n_components(&self, count: usize) -> Option<&Self> {
156 let len = self.len();
157 if len >= count {
158 let mut components = self.components();
159 for _ in 0..(len - count) {
160 components.next()?;
161 }
162 Some(components.rest())
163 } else {
164 None
165 }
166 }
167
168 pub fn join(&self, other: &Self) -> Arc<Self> {
169 let result = if self.0.is_empty() {
170 Cow::Borrowed(&other.0)
171 } else if other.0.is_empty() {
172 Cow::Borrowed(&self.0)
173 } else {
174 Cow::Owned(format!("{}/{}", &self.0, &other.0))
175 };
176 Arc::from(unsafe { Self::new_unchecked(result.as_ref()) })
177 }
178
179 pub fn to_proto(&self) -> String {
180 self.as_unix_str().to_owned()
181 }
182
183 pub fn to_rel_path_buf(&self) -> RelPathBuf {
184 RelPathBuf(self.0.to_string())
185 }
186
187 pub fn from_proto(path: &str) -> Result<Arc<Self>> {
188 Ok(Arc::from(Self::unix(path)?))
189 }
190
191 pub fn as_unix_str(&self) -> &str {
192 &self.0
193 }
194
195 pub fn into_arc(&self) -> Arc<Self> {
196 Arc::from(self)
197 }
198
199 pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
200 match style {
201 PathStyle::Posix => Cow::Borrowed(&self.0),
202 PathStyle::Windows => Cow::Owned(self.0.replace('/', "\\")),
203 }
204 }
205
206 pub fn as_std_path(&self) -> &Path {
207 Path::new(&self.0)
208 }
209}
210
211impl ToOwned for RelPath {
212 type Owned = RelPathBuf;
213
214 fn to_owned(&self) -> Self::Owned {
215 self.to_rel_path_buf()
216 }
217}
218
219impl Borrow<RelPath> for RelPathBuf {
220 fn borrow(&self) -> &RelPath {
221 self.as_rel_path()
222 }
223}
224
225impl PartialOrd for RelPath {
226 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
227 Some(self.cmp(other))
228 }
229}
230
231impl Ord for RelPath {
232 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
233 self.components().cmp(other.components())
234 }
235}
236
237impl fmt::Debug for RelPath {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 fmt::Debug::fmt(&self.0, f)
240 }
241}
242
243impl fmt::Debug for RelPathBuf {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 fmt::Debug::fmt(&self.0, f)
246 }
247}
248
249impl RelPathBuf {
250 pub fn new() -> Self {
251 Self(String::new())
252 }
253
254 pub fn pop(&mut self) -> bool {
255 if let Some(ix) = self.0.rfind('/') {
256 self.0.truncate(ix);
257 true
258 } else if !self.is_empty() {
259 self.0.clear();
260 true
261 } else {
262 false
263 }
264 }
265
266 pub fn push(&mut self, path: &RelPath) {
267 if !self.is_empty() {
268 self.0.push('/');
269 }
270 self.0.push_str(&path.0);
271 }
272
273 pub fn as_rel_path(&self) -> &RelPath {
274 unsafe { RelPath::new_unchecked(self.0.as_str()) }
275 }
276
277 pub fn set_extension(&mut self, extension: &str) -> bool {
278 if let Some(filename) = self.file_name() {
279 let mut filename = PathBuf::from(filename);
280 filename.set_extension(extension);
281 self.pop();
282 self.0.push_str(filename.to_str().unwrap());
283 true
284 } else {
285 false
286 }
287 }
288}
289
290impl Into<Arc<RelPath>> for RelPathBuf {
291 fn into(self) -> Arc<RelPath> {
292 Arc::from(self.as_rel_path())
293 }
294}
295
296impl AsRef<RelPath> for RelPathBuf {
297 fn as_ref(&self) -> &RelPath {
298 self.as_rel_path()
299 }
300}
301
302impl Deref for RelPathBuf {
303 type Target = RelPath;
304
305 fn deref(&self) -> &Self::Target {
306 self.as_ref()
307 }
308}
309
310impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
311 fn from(value: &'a RelPath) -> Self {
312 Self::Borrowed(value)
313 }
314}
315
316impl From<&RelPath> for Arc<RelPath> {
317 fn from(rel_path: &RelPath) -> Self {
318 let bytes: Arc<str> = Arc::from(&rel_path.0);
319 unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
320 }
321}
322
323#[cfg(any(test, feature = "test-support"))]
324#[track_caller]
325pub fn rel_path(path: &str) -> &RelPath {
326 RelPath::unix(path).unwrap()
327}
328
329impl PartialEq<str> for RelPath {
330 fn eq(&self, other: &str) -> bool {
331 self.0 == *other
332 }
333}
334
335pub struct RelPathComponents<'a>(&'a str);
336
337pub struct RelPathAncestors<'a>(Option<&'a str>);
338
339const SEPARATOR: char = '/';
340
341impl<'a> RelPathComponents<'a> {
342 pub fn rest(&self) -> &'a RelPath {
343 unsafe { RelPath::new_unchecked(self.0) }
344 }
345}
346
347impl<'a> Iterator for RelPathComponents<'a> {
348 type Item = &'a str;
349
350 fn next(&mut self) -> Option<Self::Item> {
351 if let Some(sep_ix) = self.0.find(SEPARATOR) {
352 let (head, tail) = self.0.split_at(sep_ix);
353 self.0 = &tail[1..];
354 Some(head)
355 } else if self.0.is_empty() {
356 None
357 } else {
358 let result = self.0;
359 self.0 = "";
360 Some(result)
361 }
362 }
363}
364
365impl<'a> Iterator for RelPathAncestors<'a> {
366 type Item = &'a RelPath;
367
368 fn next(&mut self) -> Option<Self::Item> {
369 let result = self.0?;
370 if let Some(sep_ix) = result.rfind(SEPARATOR) {
371 self.0 = Some(&result[..sep_ix]);
372 } else if !result.is_empty() {
373 self.0 = Some("");
374 } else {
375 self.0 = None;
376 }
377 Some(unsafe { RelPath::new_unchecked(result) })
378 }
379}
380
381impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
382 fn next_back(&mut self) -> Option<Self::Item> {
383 if let Some(sep_ix) = self.0.rfind(SEPARATOR) {
384 let (head, tail) = self.0.split_at(sep_ix);
385 self.0 = head;
386 Some(&tail[1..])
387 } else if self.0.is_empty() {
388 None
389 } else {
390 let result = self.0;
391 self.0 = "";
392 Some(result)
393 }
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use itertools::Itertools;
401 use pretty_assertions::assert_matches;
402
403 #[test]
404 fn test_rel_path_new() {
405 assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err());
406 assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err());
407 assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err());
408
409 let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
410 assert_eq!(path, rel_path("foo").into());
411 assert_matches!(path, Cow::Borrowed(_));
412
413 let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
414 assert_eq!(path, rel_path("foo").into());
415 assert_matches!(path, Cow::Borrowed(_));
416
417 assert_eq!(
418 RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
419 .unwrap()
420 .as_ref(),
421 rel_path("foo/baz/quux")
422 );
423
424 let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap();
425 assert_eq!(path.as_ref(), rel_path("foo/bar"));
426 assert_matches!(path, Cow::Borrowed(_));
427
428 let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
429 assert_eq!(path, rel_path("foo").into());
430 assert_matches!(path, Cow::Borrowed(_));
431
432 let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
433 assert_eq!(path, rel_path("foo").into());
434 assert_matches!(path, Cow::Borrowed(_));
435
436 let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap();
437 assert_eq!(path.as_ref(), rel_path("foo/bar"));
438 assert_matches!(path, Cow::Owned(_));
439
440 let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
441 assert_eq!(path.as_ref(), rel_path("foo/bar"));
442 assert_matches!(path, Cow::Borrowed(_));
443
444 let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
445 assert_eq!(path.as_ref(), rel_path("foo/bar"));
446 assert_matches!(path, Cow::Owned(_));
447 }
448
449 #[test]
450 fn test_rel_path_components() {
451 let path = rel_path("foo/bar/baz");
452 assert_eq!(
453 path.components().collect::<Vec<_>>(),
454 vec!["foo", "bar", "baz"]
455 );
456 assert_eq!(
457 path.components().rev().collect::<Vec<_>>(),
458 vec!["baz", "bar", "foo"]
459 );
460
461 let path = rel_path("");
462 let mut components = path.components();
463 assert_eq!(components.next(), None);
464 }
465
466 #[test]
467 fn test_rel_path_ancestors() {
468 let path = rel_path("foo/bar/baz");
469 let mut ancestors = path.ancestors();
470 assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
471 assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
472 assert_eq!(ancestors.next(), Some(rel_path("foo")));
473 assert_eq!(ancestors.next(), Some(rel_path("")));
474 assert_eq!(ancestors.next(), None);
475
476 let path = rel_path("foo");
477 let mut ancestors = path.ancestors();
478 assert_eq!(ancestors.next(), Some(rel_path("foo")));
479 assert_eq!(ancestors.next(), Some(RelPath::empty()));
480 assert_eq!(ancestors.next(), None);
481
482 let path = RelPath::empty();
483 let mut ancestors = path.ancestors();
484 assert_eq!(ancestors.next(), Some(RelPath::empty()));
485 assert_eq!(ancestors.next(), None);
486 }
487
488 #[test]
489 fn test_rel_path_parent() {
490 assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar")));
491 assert_eq!(rel_path("foo").parent(), Some(RelPath::empty()));
492 assert_eq!(rel_path("").parent(), None);
493 }
494
495 #[test]
496 fn test_rel_path_partial_ord_is_compatible_with_std() {
497 let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
498 for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
499 assert_eq!(
500 Path::new(lhs).cmp(Path::new(rhs)),
501 RelPath::unix(lhs)
502 .unwrap()
503 .cmp(&RelPath::unix(rhs).unwrap())
504 );
505 }
506 }
507
508 #[test]
509 fn test_strip_prefix() {
510 let parent = rel_path("");
511 let child = rel_path(".foo");
512
513 assert!(child.starts_with(parent));
514 assert_eq!(child.strip_prefix(parent).unwrap(), child);
515 }
516
517 #[test]
518 fn test_rel_path_constructors_absolute_path() {
519 assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
520 assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
521 assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err());
522 assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
523 assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
524 assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
525 }
526
527 #[test]
528 fn test_pop() {
529 let mut path = rel_path("a/b").to_rel_path_buf();
530 path.pop();
531 assert_eq!(path.as_rel_path().as_unix_str(), "a");
532 path.pop();
533 assert_eq!(path.as_rel_path().as_unix_str(), "");
534 path.pop();
535 assert_eq!(path.as_rel_path().as_unix_str(), "");
536 }
537}