relative_path/
path_ext.rs

1// Copyright 2012-2015 The Rust Project Developers. See the COPYRIGHT
2// file at the top-level directory of this distribution and at
3// http://rust-lang.org/COPYRIGHT.
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11// Ported from the pathdiff crate, which adapted the original rustc's
12// path_relative_from
13// https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs
14// https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158
15
16use core::fmt;
17use std::error;
18use std::path::{Path, PathBuf};
19use std::prelude::v1::*;
20
21use crate::{Component, RelativePathBuf};
22
23// Prevent downstream implementations, so methods may be added without backwards
24// breaking changes.
25mod sealed {
26    use std::path::{Path, PathBuf};
27
28    pub trait Sealed {}
29
30    impl Sealed for Path {}
31    impl Sealed for PathBuf {}
32}
33
34/// An error raised when attempting to convert a path using
35/// [`PathExt::relative_to`].
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct RelativeToError {
38    kind: RelativeToErrorKind,
39}
40
41/// Error kind for [`RelativeToError`].
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[non_exhaustive]
44enum RelativeToErrorKind {
45    /// Non-utf8 component in path.
46    NonUtf8,
47    /// Mismatching path prefixes.
48    PrefixMismatch,
49    /// A provided path is ambiguous, in that there is no way to determine which
50    /// components should be added from one path to the other to traverse it.
51    ///
52    /// For example, `.` is ambiguous relative to `../..` because we don't know
53    /// the names of the components being traversed.
54    AmbiguousTraversal,
55    /// This is a catch-all error since we don't control the `std::path` API a
56    /// Components iterator might decide (intentionally or not) to produce
57    /// components which violates its own contract.
58    ///
59    /// In particular we rely on only relative components being produced after
60    /// the absolute prefix has been consumed.
61    IllegalComponent,
62}
63
64impl fmt::Display for RelativeToError {
65    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
66        match self.kind {
67            RelativeToErrorKind::NonUtf8 => "path contains non-utf8 component".fmt(fmt),
68            RelativeToErrorKind::PrefixMismatch => {
69                "paths contain different absolute prefixes".fmt(fmt)
70            }
71            RelativeToErrorKind::AmbiguousTraversal => {
72                "path traversal cannot be determined".fmt(fmt)
73            }
74            RelativeToErrorKind::IllegalComponent => "path contains illegal components".fmt(fmt),
75        }
76    }
77}
78
79impl error::Error for RelativeToError {}
80
81impl From<RelativeToErrorKind> for RelativeToError {
82    #[inline]
83    fn from(kind: RelativeToErrorKind) -> Self {
84        Self { kind }
85    }
86}
87
88/// Extension methods for [`Path`] and [`PathBuf`] to for building and
89/// interacting with [`RelativePath`].
90///
91/// [`RelativePath`]: crate::RelativePath
92pub trait PathExt: sealed::Sealed {
93    /// Build a relative path from the provided directory to `self`.
94    ///
95    /// Producing a relative path like this is a logical operation and does not
96    /// guarantee that the constructed path corresponds to what the filesystem
97    /// would do. On Linux for example symbolic links could mean that the
98    /// logical path doesn't correspond to the filesystem path.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use std::path::Path;
104    /// use relative_path::{RelativePath, PathExt};
105    ///
106    /// let baz = Path::new("/foo/bar/baz");
107    /// let bar = Path::new("/foo/bar");
108    /// let qux = Path::new("/foo/bar/qux");
109    ///
110    /// assert_eq!(bar.relative_to(baz)?, RelativePath::new("../"));
111    /// assert_eq!(baz.relative_to(bar)?, RelativePath::new("baz"));
112    /// assert_eq!(qux.relative_to(baz)?, RelativePath::new("../qux"));
113    /// assert_eq!(baz.relative_to(qux)?, RelativePath::new("../baz"));
114    /// assert_eq!(bar.relative_to(qux)?, RelativePath::new("../"));
115    /// # Ok::<_, relative_path::RelativeToError>(())
116    /// ```
117    ///
118    /// # Errors
119    ///
120    /// Errors in case the provided path contains components which cannot be
121    /// converted into a relative path as needed, such as non-utf8 data.
122    fn relative_to<P>(&self, root: P) -> Result<RelativePathBuf, RelativeToError>
123    where
124        P: AsRef<Path>;
125}
126
127impl PathExt for Path {
128    fn relative_to<P>(&self, root: P) -> Result<RelativePathBuf, RelativeToError>
129    where
130        P: AsRef<Path>,
131    {
132        use std::path::Component::{CurDir, Normal, ParentDir, Prefix, RootDir};
133
134        // Helper function to convert from a std::path::Component to a
135        // relative_path::Component.
136        fn std_to_c(c: std::path::Component<'_>) -> Result<Component<'_>, RelativeToError> {
137            Ok(match c {
138                CurDir => Component::CurDir,
139                ParentDir => Component::ParentDir,
140                Normal(n) => Component::Normal(n.to_str().ok_or(RelativeToErrorKind::NonUtf8)?),
141                _ => return Err(RelativeToErrorKind::IllegalComponent.into()),
142            })
143        }
144
145        let root = root.as_ref();
146        let mut a_it = self.components();
147        let mut b_it = root.components();
148
149        // Ensure that the two paths are both either relative, or have the same
150        // prefix. Strips any common prefix the two paths do have. Prefixes are
151        // platform dependent, but different prefixes would for example indicate
152        // paths for different drives on Windows.
153        let (a_head, b_head) = loop {
154            match (a_it.next(), b_it.next()) {
155                (Some(RootDir), Some(RootDir)) => (),
156                (Some(Prefix(a)), Some(Prefix(b))) if a == b => (),
157                (Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => {
158                    return Err(RelativeToErrorKind::PrefixMismatch.into());
159                }
160                (None, None) => break (None, None),
161                (a, b) if a != b => break (a, b),
162                _ => (),
163            }
164        };
165
166        let mut a_it = a_head.into_iter().chain(a_it);
167        let mut b_it = b_head.into_iter().chain(b_it);
168        let mut buf = RelativePathBuf::new();
169
170        loop {
171            let a = if let Some(a) = a_it.next() {
172                a
173            } else {
174                for _ in b_it {
175                    buf.push(Component::ParentDir);
176                }
177
178                break;
179            };
180
181            match b_it.next() {
182                Some(CurDir) => buf.push(std_to_c(a)?),
183                Some(ParentDir) => {
184                    return Err(RelativeToErrorKind::AmbiguousTraversal.into());
185                }
186                root => {
187                    if root.is_some() {
188                        buf.push(Component::ParentDir);
189                    }
190
191                    for comp in b_it {
192                        match comp {
193                            ParentDir => {
194                                if !buf.pop() {
195                                    return Err(RelativeToErrorKind::AmbiguousTraversal.into());
196                                }
197                            }
198                            CurDir => (),
199                            _ => buf.push(Component::ParentDir),
200                        }
201                    }
202
203                    buf.push(std_to_c(a)?);
204
205                    for c in a_it {
206                        buf.push(std_to_c(c)?);
207                    }
208
209                    break;
210                }
211            }
212        }
213
214        Ok(buf)
215    }
216}
217
218impl PathExt for PathBuf {
219    #[inline]
220    fn relative_to<P>(&self, root: P) -> Result<RelativePathBuf, RelativeToError>
221    where
222        P: AsRef<Path>,
223    {
224        self.as_path().relative_to(root)
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use std::path::Path;
231
232    use super::{PathExt, RelativeToErrorKind};
233    use crate::{RelativePathBuf, RelativeToError};
234
235    macro_rules! assert_relative_to {
236        ($path:expr, $base:expr, Ok($expected:expr) $(,)?) => {
237            assert_eq!(
238                Path::new($path).relative_to($base),
239                Ok(RelativePathBuf::from($expected))
240            );
241        };
242
243        ($path:expr, $base:expr, Err($expected:ident) $(,)?) => {
244            assert_eq!(
245                Path::new($path).relative_to($base),
246                Err(RelativeToError::from(RelativeToErrorKind::$expected))
247            );
248        };
249    }
250
251    #[cfg(windows)]
252    macro_rules! abs {
253        ($path:expr) => {
254            Path::new(concat!("C:\\", $path))
255        };
256    }
257
258    #[cfg(not(windows))]
259    macro_rules! abs {
260        ($path:expr) => {
261            Path::new(concat!("/", $path))
262        };
263    }
264
265    #[test]
266    #[cfg(windows)]
267    fn test_different_prefixes() {
268        assert_relative_to!("C:\\repo", "D:\\repo", Err(PrefixMismatch),);
269        assert_relative_to!("C:\\repo", "C:\\repo", Ok(""));
270        assert_relative_to!(
271            "\\\\server\\share\\repo",
272            "\\\\server2\\share\\repo",
273            Err(PrefixMismatch),
274        );
275    }
276
277    #[test]
278    fn test_absolute() {
279        assert_relative_to!(abs!("foo"), abs!("bar"), Ok("../foo"));
280        assert_relative_to!("foo", "bar", Ok("../foo"));
281        assert_relative_to!(abs!("foo"), "bar", Err(PrefixMismatch));
282        assert_relative_to!("foo", abs!("bar"), Err(PrefixMismatch));
283    }
284
285    #[test]
286    fn test_identity() {
287        assert_relative_to!(".", ".", Ok(""));
288        assert_relative_to!("../foo", "../foo", Ok(""));
289        assert_relative_to!("./foo", "./foo", Ok(""));
290        assert_relative_to!("/foo", "/foo", Ok(""));
291        assert_relative_to!("foo", "foo", Ok(""));
292
293        assert_relative_to!("../foo/bar/baz", "../foo/bar/baz", Ok(""));
294        assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok(""));
295    }
296
297    #[test]
298    fn test_subset() {
299        assert_relative_to!("foo", "fo", Ok("../foo"));
300        assert_relative_to!("fo", "foo", Ok("../fo"));
301    }
302
303    #[test]
304    fn test_empty() {
305        assert_relative_to!("", "", Ok(""));
306        assert_relative_to!("foo", "", Ok("foo"));
307        assert_relative_to!("", "foo", Ok(".."));
308    }
309
310    #[test]
311    fn test_relative() {
312        assert_relative_to!("../foo", "../bar", Ok("../foo"));
313        assert_relative_to!("../foo", "../foo/bar/baz", Ok("../.."));
314        assert_relative_to!("../foo/bar/baz", "../foo", Ok("bar/baz"));
315
316        assert_relative_to!("foo/bar/baz", "foo", Ok("bar/baz"));
317        assert_relative_to!("foo/bar/baz", "foo/bar", Ok("baz"));
318        assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok(""));
319        assert_relative_to!("foo/bar/baz", "foo/bar/baz/", Ok(""));
320
321        assert_relative_to!("foo/bar/baz/", "foo", Ok("bar/baz"));
322        assert_relative_to!("foo/bar/baz/", "foo/bar", Ok("baz"));
323        assert_relative_to!("foo/bar/baz/", "foo/bar/baz", Ok(""));
324        assert_relative_to!("foo/bar/baz/", "foo/bar/baz/", Ok(""));
325
326        assert_relative_to!("foo/bar/baz", "foo/", Ok("bar/baz"));
327        assert_relative_to!("foo/bar/baz", "foo/bar/", Ok("baz"));
328        assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok(""));
329    }
330
331    #[test]
332    fn test_current_directory() {
333        assert_relative_to!(".", "foo", Ok("../."));
334        assert_relative_to!("foo", ".", Ok("foo"));
335        assert_relative_to!("/foo", "/.", Ok("foo"));
336    }
337
338    #[test]
339    fn assert_does_not_skip_parents() {
340        assert_relative_to!("some/path", "some/foo/baz/path", Ok("../../../path"));
341        assert_relative_to!("some/path", "some/foo/bar/../baz/path", Ok("../../../path"));
342    }
343
344    #[test]
345    fn test_ambiguous_paths() {
346        // Parent directory name is unknown, so trying to make current directory
347        // relative to it is impossible.
348        assert_relative_to!(".", "../..", Err(AmbiguousTraversal));
349        assert_relative_to!(".", "a/../..", Err(AmbiguousTraversal));
350        // Common prefixes are ok.
351        assert_relative_to!("../a/..", "../a/../b", Ok(".."));
352        assert_relative_to!("../a/../b", "../a/..", Ok("b"));
353    }
354}