pathdiff/
lib.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// Adapted from rustc's path_relative_from
12// https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158
13
14use std::path::*;
15
16/// Construct a relative path from a provided base directory path to the provided path.
17///
18/// ```rust
19/// use pathdiff::diff_paths;
20/// use std::path::*;
21///
22/// let baz = "/foo/bar/baz";
23/// let bar = "/foo/bar";
24/// let quux = "/foo/bar/quux";
25/// assert_eq!(diff_paths(bar, baz), Some("../".into()));
26/// assert_eq!(diff_paths(baz, bar), Some("baz".into()));
27/// assert_eq!(diff_paths(quux, baz), Some("../quux".into()));
28/// assert_eq!(diff_paths(baz, quux), Some("../baz".into()));
29/// assert_eq!(diff_paths(bar, quux), Some("../".into()));
30///
31/// assert_eq!(diff_paths(&baz, &bar.to_string()), Some("baz".into()));
32/// assert_eq!(diff_paths(Path::new(baz), Path::new(bar).to_path_buf()), Some("baz".into()));
33/// ```
34pub fn diff_paths<P, B>(path: P, base: B) -> Option<PathBuf>
35where
36    P: AsRef<Path>,
37    B: AsRef<Path>,
38{
39    let path = path.as_ref();
40    let base = base.as_ref();
41
42    if path.is_absolute() != base.is_absolute() {
43        if path.is_absolute() {
44            Some(PathBuf::from(path))
45        } else {
46            None
47        }
48    } else {
49        let mut ita = path.components();
50        let mut itb = base.components();
51        let mut comps: Vec<Component> = vec![];
52        loop {
53            match (ita.next(), itb.next()) {
54                (None, None) => break,
55                (Some(a), None) => {
56                    comps.push(a);
57                    comps.extend(ita.by_ref());
58                    break;
59                }
60                (None, _) => comps.push(Component::ParentDir),
61                (Some(a), Some(b)) if comps.is_empty() && a == b => (),
62                (Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
63                (Some(_), Some(b)) if b == Component::ParentDir => return None,
64                (Some(a), Some(_)) => {
65                    comps.push(Component::ParentDir);
66                    for _ in itb {
67                        comps.push(Component::ParentDir);
68                    }
69                    comps.push(a);
70                    comps.extend(ita.by_ref());
71                    break;
72                }
73            }
74        }
75        Some(comps.iter().map(|c| c.as_os_str()).collect())
76    }
77}
78
79#[cfg(feature = "camino")]
80mod utf8_paths {
81    use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
82
83    /// Construct a relative UTF-8 path from a provided base directory path to the provided path.
84    ///
85    /// ```rust
86    /// # extern crate camino;
87    /// use camino::*;
88    /// use pathdiff::diff_utf8_paths;
89    ///
90    /// let baz = "/foo/bar/baz";
91    /// let bar = "/foo/bar";
92    /// let quux = "/foo/bar/quux";
93    /// assert_eq!(diff_utf8_paths(bar, baz), Some("../".into()));
94    /// assert_eq!(diff_utf8_paths(baz, bar), Some("baz".into()));
95    /// assert_eq!(diff_utf8_paths(quux, baz), Some("../quux".into()));
96    /// assert_eq!(diff_utf8_paths(baz, quux), Some("../baz".into()));
97    /// assert_eq!(diff_utf8_paths(bar, quux), Some("../".into()));
98    ///
99    /// assert_eq!(diff_utf8_paths(&baz, &bar.to_string()), Some("baz".into()));
100    /// assert_eq!(diff_utf8_paths(Utf8Path::new(baz), Utf8Path::new(bar).to_path_buf()), Some("baz".into()));
101    /// ```
102    #[cfg_attr(docsrs, doc(cfg(feature = "camino")))]
103    pub fn diff_utf8_paths<P, B>(path: P, base: B) -> Option<Utf8PathBuf>
104    where
105        P: AsRef<Utf8Path>,
106        B: AsRef<Utf8Path>,
107    {
108        let path = path.as_ref();
109        let base = base.as_ref();
110
111        if path.is_absolute() != base.is_absolute() {
112            if path.is_absolute() {
113                Some(Utf8PathBuf::from(path))
114            } else {
115                None
116            }
117        } else {
118            let mut ita = path.components();
119            let mut itb = base.components();
120            let mut comps: Vec<Utf8Component> = vec![];
121            loop {
122                match (ita.next(), itb.next()) {
123                    (None, None) => break,
124                    (Some(a), None) => {
125                        comps.push(a);
126                        comps.extend(ita.by_ref());
127                        break;
128                    }
129                    (None, _) => comps.push(Utf8Component::ParentDir),
130                    (Some(a), Some(b)) if comps.is_empty() && a == b => (),
131                    (Some(a), Some(b)) if b == Utf8Component::CurDir => comps.push(a),
132                    (Some(_), Some(b)) if b == Utf8Component::ParentDir => return None,
133                    (Some(a), Some(_)) => {
134                        comps.push(Utf8Component::ParentDir);
135                        for _ in itb {
136                            comps.push(Utf8Component::ParentDir);
137                        }
138                        comps.push(a);
139                        comps.extend(ita.by_ref());
140                        break;
141                    }
142                }
143            }
144            Some(comps.iter().map(|c| c.as_str()).collect())
145        }
146    }
147}
148
149#[cfg(feature = "camino")]
150pub use crate::utf8_paths::*;
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_absolute() {
158        assert_diff_paths("/foo", "/bar", Some("../foo"));
159        assert_diff_paths("/foo", "bar", Some("/foo"));
160        assert_diff_paths("foo", "/bar", None);
161        assert_diff_paths("foo", "bar", Some("../foo"));
162    }
163
164    #[test]
165    fn test_identity() {
166        assert_diff_paths(".", ".", Some(""));
167        assert_diff_paths("../foo", "../foo", Some(""));
168        assert_diff_paths("./foo", "./foo", Some(""));
169        assert_diff_paths("/foo", "/foo", Some(""));
170        assert_diff_paths("foo", "foo", Some(""));
171
172        assert_diff_paths("../foo/bar/baz", "../foo/bar/baz", Some("".into()));
173        assert_diff_paths("foo/bar/baz", "foo/bar/baz", Some(""));
174    }
175
176    #[test]
177    fn test_subset() {
178        assert_diff_paths("foo", "fo", Some("../foo"));
179        assert_diff_paths("fo", "foo", Some("../fo"));
180    }
181
182    #[test]
183    fn test_empty() {
184        assert_diff_paths("", "", Some(""));
185        assert_diff_paths("foo", "", Some("foo"));
186        assert_diff_paths("", "foo", Some(".."));
187    }
188
189    #[test]
190    fn test_relative() {
191        assert_diff_paths("../foo", "../bar", Some("../foo"));
192        assert_diff_paths("../foo", "../foo/bar/baz", Some("../.."));
193        assert_diff_paths("../foo/bar/baz", "../foo", Some("bar/baz"));
194
195        assert_diff_paths("foo/bar/baz", "foo", Some("bar/baz"));
196        assert_diff_paths("foo/bar/baz", "foo/bar", Some("baz"));
197        assert_diff_paths("foo/bar/baz", "foo/bar/baz", Some(""));
198        assert_diff_paths("foo/bar/baz", "foo/bar/baz/", Some(""));
199
200        assert_diff_paths("foo/bar/baz/", "foo", Some("bar/baz"));
201        assert_diff_paths("foo/bar/baz/", "foo/bar", Some("baz"));
202        assert_diff_paths("foo/bar/baz/", "foo/bar/baz", Some(""));
203        assert_diff_paths("foo/bar/baz/", "foo/bar/baz/", Some(""));
204
205        assert_diff_paths("foo/bar/baz", "foo/", Some("bar/baz"));
206        assert_diff_paths("foo/bar/baz", "foo/bar/", Some("baz"));
207        assert_diff_paths("foo/bar/baz", "foo/bar/baz", Some(""));
208    }
209
210    #[test]
211    fn test_current_directory() {
212        assert_diff_paths(".", "foo", Some("../."));
213        assert_diff_paths("foo", ".", Some("foo"));
214        assert_diff_paths("/foo", "/.", Some("foo"));
215    }
216
217    fn assert_diff_paths(path: &str, base: &str, expected: Option<&str>) {
218        assert_eq!(diff_paths(path, base), expected.map(|s| s.into()));
219        #[cfg(feature = "camino")]
220        assert_eq!(diff_utf8_paths(path, base), expected.map(|s| s.into()));
221    }
222}