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