1 //! Path newtypes which are always guaranteed to be canonical
2 //!
3 //! In the same way a `str` "guarantees" a `&[u8]` contains only valid UTF-8 data,
4 //! `CanonicalPath` and `CanonicalPathBuf` guarantee that the paths they represent
5 //! are canonical, or at least, were canonical at the time they were created.
6
7 #![deny(
8 warnings,
9 missing_docs,
10 trivial_numeric_casts,
11 unused_import_braces,
12 unused_qualifications
13 )]
14 #![doc(html_root_url = "https://docs.rs/canonical-path/2.0.2")]
15
16 use std::{
17 borrow::Borrow,
18 env,
19 ffi::{OsStr, OsString},
20 fs::{Metadata, ReadDir},
21 io::{Error, ErrorKind, Result},
22 path::{Components, Display, Iter, Path, PathBuf},
23 };
24
25 /// Common methods of `CanonicalPath` and `CanonicalPathBuf`
26 macro_rules! impl_path {
27 () => {
28 /// Return a `Path` reference.
29 #[inline]
30 pub fn as_path(&self) -> &Path {
31 self.0.as_ref()
32 }
33
34 /// Return an `OsStr` reference.
35 #[inline]
36 pub fn as_os_str(&self) -> &OsStr {
37 self.0.as_os_str()
38 }
39
40 /// Yields a `&str` slice if the path is valid unicode.
41 #[inline]
42 pub fn to_str(&self) -> Option<&str> {
43 self.0.to_str()
44 }
45
46 /// Return a canonical parent path of this path, or `io::Error` if the
47 /// path is the root directory or another canonicalization error occurs.
48 pub fn parent(&self) -> Result<CanonicalPathBuf> {
49 CanonicalPathBuf::new(&self.0
50 .parent()
51 .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "can't get parent of '/'"))?)
52 }
53
54 /// Returns the final component of the path, if there is one.
55 #[inline]
56 pub fn file_name(&self) -> Option<&OsStr> {
57 self.0.file_name()
58 }
59
60 /// Determines whether base is a prefix of self.
61 #[inline]
62 pub fn starts_with<P: AsRef<Path>>(&self, base: P) -> bool {
63 self.0.starts_with(base)
64 }
65
66 /// Determines whether child is a suffix of self.
67 #[inline]
68 pub fn ends_with<P: AsRef<Path>>(&self, child: P) -> bool {
69 self.0.ends_with(child)
70 }
71
72 /// Extracts the stem (non-extension) portion of `self.file_name`.
73 #[inline]
74 pub fn file_stem(&self) -> Option<&OsStr> {
75 self.0.file_stem()
76 }
77
78 /// Extracts the extension of `self.file_name`, if possible.
79 #[inline]
80 pub fn extension(&self) -> Option<&OsStr> {
81 self.0.extension()
82 }
83
84 /// Creates an owned `CanonicalPathBuf` like self but with the given file name.
85 #[inline]
86 pub fn with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<CanonicalPathBuf> {
87 CanonicalPathBuf::new(&self.0.with_file_name(file_name))
88 }
89
90 /// Creates an owned `CanonicalPathBuf` like self but with the given extension.
91 #[inline]
92 pub fn with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<CanonicalPathBuf> {
93 CanonicalPathBuf::new(&self.0.with_extension(extension))
94 }
95
96 /// Produces an iterator over the `Component`s of a path
97 #[inline]
98 pub fn components(&self) -> Components {
99 self.0.components()
100 }
101
102 /// Produces an iterator over the path's components viewed as
103 /// `OsStr` slices.
104 #[inline]
105 pub fn iter(&self) -> Iter {
106 self.0.iter()
107 }
108
109 /// Returns an object that implements `Display` for safely printing
110 /// paths that may contain non-Unicode data.
111 #[inline]
112 pub fn display(&self) -> Display {
113 self.0.display()
114 }
115
116 /// Queries the file system to get information about a file, directory, etc.
117 ///
118 /// Unlike the `std` version of this method, it will not follow symlinks,
119 /// since as a canonical path we should be symlink-free.
120 #[inline]
121 pub fn metadata(&self) -> Result<Metadata> {
122 // Counterintuitively this is the version of this method which
123 // does not traverse symlinks
124 self.0.symlink_metadata()
125 }
126
127 /// Join a path onto a canonical path, returning a `CanonicalPathBuf`.
128 #[inline]
129 pub fn join<P: AsRef<Path>>(&self, path: P) -> Result<CanonicalPathBuf> {
130 CanonicalPathBuf::new(&self.0.join(path))
131 }
132
133 /// Returns an iterator over the entries within a directory.
134 ///
135 /// The iterator will yield instances of io::Result<DirEntry>. New
136 /// errors may be encountered after an iterator is initially
137 /// constructed.
138 ///
139 /// This is an alias to fs::read_dir.
140 #[inline]
141 pub fn read_dir(&self) -> Result<ReadDir> {
142 self.0.read_dir()
143 }
144
145 /// Does this path exist?
146 #[inline]
147 pub fn exists(&self) -> bool {
148 self.0.exists()
149 }
150
151 /// Is this path a file?
152 #[inline]
153 pub fn is_file(&self) -> bool {
154 self.0.is_file()
155 }
156
157 /// Is this path a directory?
158 #[inline]
159 pub fn is_dir(&self) -> bool {
160 self.0.is_file()
161 }
162 }
163 }
164
165 /// An owned path on the filesystem which is guaranteed to be canonical.
166 ///
167 /// More specifically: it is at least guaranteed to be canonical at
168 /// the time it is created. There are potential TOCTTOU problems if the
169 /// underlying filesystem structure changes after path canonicalization.
170 #[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
171 pub struct CanonicalPathBuf(PathBuf);
172
173 impl CanonicalPathBuf {
174 /// Create a canonical path by first canonicalizing the given path.
canonicalize<P>(path: P) -> Result<Self> where P: AsRef<Path>,175 pub fn canonicalize<P>(path: P) -> Result<Self>
176 where
177 P: AsRef<Path>,
178 {
179 Ok(CanonicalPathBuf(path.as_ref().canonicalize()?))
180 }
181
182 /// Create a canonical path, returning error if the supplied path is not canonical.
183 // TODO: rename this to `from_path` or `try_new` to satisfy clippy? (breaking API change)
184 #[allow(clippy::new_ret_no_self)]
new<P>(path: P) -> Result<Self> where P: AsRef<Path>,185 pub fn new<P>(path: P) -> Result<Self>
186 where
187 P: AsRef<Path>,
188 {
189 let p = path.as_ref();
190 let canonical_path_buf = Self::canonicalize(p)?;
191
192 if canonical_path_buf.as_path() != p {
193 return Err(Error::new(
194 ErrorKind::InvalidInput,
195 format!("non-canonical input path: {}", p.display()),
196 ));
197 }
198
199 Ok(canonical_path_buf)
200 }
201
202 /// Return a `CanonicalPath` reference.
203 #[inline]
as_canonical_path(&self) -> &CanonicalPath204 pub fn as_canonical_path(&self) -> &CanonicalPath {
205 unsafe { CanonicalPath::from_path_unchecked(&self.0) }
206 }
207
208 /// Updates `self`'s filename ala the same method on `PathBuf`
set_file_name<S: AsRef<OsStr>>(&mut self, file_name: S)209 pub fn set_file_name<S: AsRef<OsStr>>(&mut self, file_name: S) {
210 self.0.set_file_name(file_name);
211 }
212
213 /// Updates `self.extension` to extension.
214 ///
215 /// Returns `false` and does nothing if `self.file_name` is `None`,
216 /// returns `true` and updates the extension otherwise.
217 /// If `self.extension` is `None`, the extension is added; otherwise it is replaced.
set_extension<S: AsRef<OsStr>>(&mut self, extension: S) -> bool218 pub fn set_extension<S: AsRef<OsStr>>(&mut self, extension: S) -> bool {
219 self.0.set_extension(extension)
220 }
221
222 /// Consumes the `CanonicalPathBuf`, yielding its internal `PathBuf` storage.
into_path_buf(self) -> PathBuf223 pub fn into_path_buf(self) -> PathBuf {
224 self.0
225 }
226
227 /// Consumes the `CanonicalPathBuf`, yielding its internal `OsString` storage.
into_os_string(self) -> OsString228 pub fn into_os_string(self) -> OsString {
229 self.0.into_os_string()
230 }
231
232 impl_path!();
233 }
234
235 impl AsRef<Path> for CanonicalPathBuf {
as_ref(&self) -> &Path236 fn as_ref(&self) -> &Path {
237 self.as_path()
238 }
239 }
240
241 impl AsRef<CanonicalPath> for CanonicalPathBuf {
as_ref(&self) -> &CanonicalPath242 fn as_ref(&self) -> &CanonicalPath {
243 self.as_canonical_path()
244 }
245 }
246
247 impl AsRef<OsStr> for CanonicalPathBuf {
as_ref(&self) -> &OsStr248 fn as_ref(&self) -> &OsStr {
249 self.as_os_str()
250 }
251 }
252
253 impl Borrow<CanonicalPath> for CanonicalPathBuf {
borrow(&self) -> &CanonicalPath254 fn borrow(&self) -> &CanonicalPath {
255 self.as_canonical_path()
256 }
257 }
258
259 /// A reference type for a canonical filesystem path
260 ///
261 /// More specifically: it is at least guaranteed to be canonical at
262 /// the time it is created. There are potential TOCTTOU problems if the
263 /// underlying filesystem structure changes after path canonicalization.
264 #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
265 pub struct CanonicalPath(Path);
266
267 impl CanonicalPath {
268 /// Create a canonical path, returning error if the supplied path is not canonical
new<P>(path: &P) -> Result<&Self> where P: AsRef<Path> + ?Sized,269 pub fn new<P>(path: &P) -> Result<&Self>
270 where
271 P: AsRef<Path> + ?Sized,
272 {
273 let p = path.as_ref();
274
275 // TODO: non-allocating check that `P` is canonical
276 //
277 // This seems tricky as realpath(3) is our only real way of checking
278 // that a path is canonical. It's also slightly terrifying in that,
279 // at least in glibc, it is over 200 lines long and full of complex
280 // logic and error handling:
281 //
282 // https://sourceware.org/git/?p=glibc.git;a=blob;f=stdlib/canonicalize.c;hb=HEAD
283 if p != p.canonicalize()? {
284 return Err(Error::new(
285 ErrorKind::InvalidInput,
286 format!("non-canonical input path: {}", p.display()),
287 ));
288 }
289
290 Ok(unsafe { Self::from_path_unchecked(p) })
291 }
292
293 /// Create a canonical path from a path, skipping the canonicalization check
294 ///
295 /// This uses the same unsafe reference conversion tricks as `std` to
296 /// convert from `AsRef<Path>` to `AsRef<CanonicalPath>`, i.e. `&CanonicalPath`
297 /// is a newtype for `&Path` in the same way `&Path` is a newtype for `&OsStr`.
from_path_unchecked<P>(path: &P) -> &Self where P: AsRef<Path> + ?Sized,298 pub unsafe fn from_path_unchecked<P>(path: &P) -> &Self
299 where
300 P: AsRef<Path> + ?Sized,
301 {
302 &*(path.as_ref() as *const Path as *const CanonicalPath)
303 }
304
305 /// Convert a canonical path reference into an owned `CanonicalPathBuf`
to_canonical_path_buf(&self) -> CanonicalPathBuf306 pub fn to_canonical_path_buf(&self) -> CanonicalPathBuf {
307 CanonicalPathBuf(self.0.to_owned())
308 }
309
310 impl_path!();
311 }
312
313 impl AsRef<Path> for CanonicalPath {
as_ref(&self) -> &Path314 fn as_ref(&self) -> &Path {
315 &self.0
316 }
317 }
318
319 impl ToOwned for CanonicalPath {
320 type Owned = CanonicalPathBuf;
321
to_owned(&self) -> CanonicalPathBuf322 fn to_owned(&self) -> CanonicalPathBuf {
323 self.to_canonical_path_buf()
324 }
325 }
326
327 /// Returns the full, canonicalized filesystem path of the current running
328 /// executable.
current_exe() -> Result<CanonicalPathBuf>329 pub fn current_exe() -> Result<CanonicalPathBuf> {
330 let p = env::current_exe()?;
331 Ok(CanonicalPathBuf::canonicalize(p)?)
332 }
333
334 // TODO: test on Windows
335 #[cfg(all(test, not(windows)))]
336 mod tests {
337 use std::fs::File;
338 use std::os::unix::fs;
339 use std::path::PathBuf;
340
341 use super::{CanonicalPath, CanonicalPathBuf};
342 use tempfile::TempDir;
343
344 // We create a test file with this name
345 const CANONICAL_FILENAME: &str = "canonical-file";
346
347 // We create a symlink to "canonical-file" with this name
348 const NON_CANONICAL_FILENAME: &str = "non-canonical-file";
349
350 /// A directory full of test fixtures
351 struct TestFixtureDir {
352 /// The temporary directory itself (i.e. root directory of our tests)
353 pub tempdir: TempDir,
354
355 /// Canonical path to the test directory
356 pub base_path: PathBuf,
357
358 /// Path to a canonical file in our test fixture directory
359 pub canonical_path: PathBuf,
360
361 /// Path to a symlink in our test fixture directory
362 pub symlink_path: PathBuf,
363 }
364
365 impl TestFixtureDir {
new() -> Self366 pub fn new() -> Self {
367 let tempdir = TempDir::new().unwrap();
368 let base_path = tempdir.path().canonicalize().unwrap();
369
370 let canonical_path = base_path.join(CANONICAL_FILENAME);
371 File::create(&canonical_path).unwrap();
372
373 let symlink_path = base_path.join(NON_CANONICAL_FILENAME);
374 fs::symlink(&canonical_path, &symlink_path).unwrap();
375
376 Self {
377 tempdir,
378 base_path,
379 canonical_path,
380 symlink_path,
381 }
382 }
383 }
384
385 #[test]
create_canonical_path()386 fn create_canonical_path() {
387 let test_fixtures = TestFixtureDir::new();
388 let canonical_path = CanonicalPath::new(&test_fixtures.canonical_path).unwrap();
389 assert_eq!(
390 canonical_path.as_path(),
391 test_fixtures.canonical_path.as_path()
392 );
393 }
394
395 #[test]
create_canonical_path_buf()396 fn create_canonical_path_buf() {
397 let test_fixtures = TestFixtureDir::new();
398 let canonical_path_buf = CanonicalPathBuf::new(&test_fixtures.canonical_path).unwrap();
399 assert_eq!(
400 canonical_path_buf.as_path(),
401 test_fixtures.canonical_path.as_path()
402 );
403 }
404
405 #[test]
reject_canonical_path_symlinks()406 fn reject_canonical_path_symlinks() {
407 let test_fixtures = TestFixtureDir::new();
408 let result = CanonicalPath::new(&test_fixtures.symlink_path);
409 assert!(result.is_err(), "symlinks aren't canonical paths!");
410 }
411
412 #[test]
reject_canonical_path_buf_symlinks()413 fn reject_canonical_path_buf_symlinks() {
414 let test_fixtures = TestFixtureDir::new();
415 let result = CanonicalPathBuf::new(&test_fixtures.symlink_path);
416 assert!(result.is_err(), "symlinks aren't canonical paths!");
417 }
418 }
419