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}
253