joat_path/
absolute_path.rs

1// Copyright (c) 2020-3 Richard Cook
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21//
22use crate::path_clean::clean;
23use std::io::{Error, ErrorKind, Result};
24use std::path::{Path, PathBuf};
25
26/// Normalize a target path to an absolute path relative to a base
27/// directory (typically the current working directory) without
28/// accessing the file system
29///
30/// # Arguments
31///
32/// * `base_dir` - Base directory (must be absolute), typically the current working directory
33/// * `path` - Path
34pub fn absolute_path<B: AsRef<Path>, P: AsRef<Path>>(base_dir: B, path: P) -> Result<PathBuf> {
35    fn normalize(path: &Path) -> Result<PathBuf> {
36        path.to_str()
37            .ok_or_else(|| {
38                Error::new(
39                    ErrorKind::Other,
40                    format!("Path {} cannot be converted to string", path.display()),
41                )
42            })
43            .map(clean)
44            .map(PathBuf::from)
45    }
46
47    if !base_dir.as_ref().is_absolute() {
48        return Err(Error::new(
49            ErrorKind::InvalidInput,
50            format!(
51                "Base directory {} is not absolute",
52                base_dir.as_ref().display()
53            ),
54        ));
55    }
56
57    normalize(&match path.as_ref().components().count() {
58        0 => base_dir.as_ref().to_path_buf(),
59        _ => base_dir.as_ref().join(path),
60    })
61}
62
63#[cfg(test)]
64mod tests {
65    use asserts::{check_absolute_path, check_absolute_path_fails};
66    use helpers::{abs, rel};
67
68    #[test]
69    fn fails_if_base_dir_not_absolute() {
70        check_absolute_path_fails(abs("aa/bb/cc"), rel(""));
71    }
72
73    #[test]
74    fn path_empty() {
75        check_absolute_path(abs("/aa/bb/cc"), rel(""), "/aa/bb/cc", 3);
76    }
77
78    #[test]
79    fn base_dir_unnormalized_path_empty() {
80        check_absolute_path(abs("/aa/../bb/cc"), rel(""), "/bb/cc", 2);
81    }
82
83    #[test]
84    fn path_single_component_relative() {
85        check_absolute_path(abs("/aa/bb/cc"), rel("dd"), "/aa/bb/cc/dd", 4);
86    }
87
88    #[test]
89    fn path_single_component_absolute() {
90        check_absolute_path(abs("/aa/bb/cc"), abs("/dd"), "/dd", 1);
91    }
92
93    #[test]
94    fn path_multiple_components_relative() {
95        check_absolute_path(abs("/aa/bb/cc"), rel("dd/ee"), "/aa/bb/cc/dd/ee", 5);
96    }
97
98    #[test]
99    fn path_multiple_components_absolute() {
100        check_absolute_path(abs("/aa/bb/cc"), abs("/dd/ee"), "/dd/ee", 2);
101    }
102
103    #[test]
104    fn path_multiple_components_unnormalized() {
105        check_absolute_path(abs("/aa/bb/cc"), rel("dd/../ee"), "/aa/bb/cc/ee", 4);
106    }
107
108    #[test]
109    fn both_unnormalized() {
110        check_absolute_path(abs("/aa/bb/../cc"), rel("dd/../ee"), "/aa/cc/ee", 3);
111    }
112
113    mod asserts {
114        use crate::absolute_path;
115
116        use super::helpers::{abs, TestPath};
117        use super::platform_helpers::{from_test_path, path_component_count, OTHER_SEPARATOR};
118
119        pub fn check_absolute_path(
120            base_dir: TestPath,
121            path: TestPath,
122            expected_path_str: &str,
123            expected_component_count: usize,
124        ) {
125            let p = absolute_path(from_test_path(base_dir), from_test_path(path)).unwrap();
126            assert!(p.is_absolute());
127            assert_eq!(p, from_test_path(abs(expected_path_str)));
128            assert_eq!(
129                p.to_str().unwrap(),
130                from_test_path(abs(expected_path_str)).to_str().unwrap()
131            );
132            assert_eq!(path_component_count(&p).unwrap(), expected_component_count);
133            assert!(!p.to_str().unwrap().contains(OTHER_SEPARATOR));
134        }
135
136        pub fn check_absolute_path_fails(p0: TestPath, p1: TestPath) {
137            assert!(absolute_path(from_test_path(p0), from_test_path(p1)).is_err());
138        }
139    }
140
141    mod helpers {
142        use self::TestPath::*;
143
144        pub enum TestPath {
145            Abs(String),
146            Rel(String),
147        }
148
149        pub fn abs(s: &str) -> TestPath {
150            Abs(String::from(s))
151        }
152
153        pub fn rel(s: &str) -> TestPath {
154            Rel(String::from(s))
155        }
156    }
157
158    #[cfg(target_os = "windows")]
159    mod platform_helpers {
160        use std::path::Component::*;
161        use std::path::Prefix::*;
162        use std::path::{Path, PathBuf};
163
164        use super::helpers::TestPath::{self, *};
165
166        pub const OTHER_SEPARATOR: char = '/';
167
168        pub fn from_test_path(test_path: TestPath) -> PathBuf {
169            let raw = match test_path {
170                Abs(s) => format!(
171                    "Z:{}",
172                    s.replace('/', &std::path::MAIN_SEPARATOR.to_string())
173                ),
174                Rel(s) => s.replace('/', &std::path::MAIN_SEPARATOR.to_string()),
175            };
176            PathBuf::from(raw)
177        }
178
179        pub fn path_component_count<P: AsRef<Path>>(path: P) -> Option<usize> {
180            let mut iter = path.as_ref().components();
181
182            match iter.next() {
183                Some(Prefix(prefix_component)) => match prefix_component.kind() {
184                    Disk(90) => {}
185                    _ => return None,
186                },
187                _ => return None,
188            };
189
190            match iter.next() {
191                Some(RootDir) => {}
192                _ => return None,
193            };
194
195            let mut n = 0;
196            loop {
197                match iter.next() {
198                    Some(Normal(_)) => n += 1,
199                    Some(_) => return None,
200                    None => return Some(n),
201                }
202            }
203        }
204    }
205
206    #[cfg(not(target_os = "windows"))]
207    mod platform_helpers {
208        use std::path::Component::*;
209        use std::path::{Path, PathBuf};
210
211        use super::helpers::TestPath::{self, *};
212
213        pub const OTHER_SEPARATOR: char = '\\';
214
215        pub fn from_test_path(test_path: TestPath) -> PathBuf {
216            let raw = match test_path {
217                Abs(s) | Rel(s) => s,
218            };
219            PathBuf::from(raw)
220        }
221
222        pub fn path_component_count<P: AsRef<Path>>(path: P) -> Option<usize> {
223            let mut iter = path.as_ref().components();
224
225            match iter.next() {
226                Some(RootDir) => {}
227                _ => return None,
228            };
229
230            let mut n = 0;
231            loop {
232                match iter.next() {
233                    Some(Normal(_)) => n += 1,
234                    Some(_) => return None,
235                    None => return Some(n),
236                }
237            }
238        }
239    }
240}