canonical_path/
lib.rs

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
16use 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`
26macro_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)]
171pub struct CanonicalPathBuf(PathBuf);
172
173impl CanonicalPathBuf {
174    /// Create a canonical path by first canonicalizing the given 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)]
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]
204    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`
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.
218    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.
223    pub fn into_path_buf(self) -> PathBuf {
224        self.0
225    }
226
227    /// Consumes the `CanonicalPathBuf`, yielding its internal `OsString` storage.
228    pub fn into_os_string(self) -> OsString {
229        self.0.into_os_string()
230    }
231
232    impl_path!();
233}
234
235impl AsRef<Path> for CanonicalPathBuf {
236    fn as_ref(&self) -> &Path {
237        self.as_path()
238    }
239}
240
241impl AsRef<CanonicalPath> for CanonicalPathBuf {
242    fn as_ref(&self) -> &CanonicalPath {
243        self.as_canonical_path()
244    }
245}
246
247impl AsRef<OsStr> for CanonicalPathBuf {
248    fn as_ref(&self) -> &OsStr {
249        self.as_os_str()
250    }
251}
252
253impl Borrow<CanonicalPath> for CanonicalPathBuf {
254    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)]
265pub struct CanonicalPath(Path);
266
267impl CanonicalPath {
268    /// Create a canonical path, returning error if the supplied path is not canonical
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`.
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`
306    pub fn to_canonical_path_buf(&self) -> CanonicalPathBuf {
307        CanonicalPathBuf(self.0.to_owned())
308    }
309
310    impl_path!();
311}
312
313impl AsRef<Path> for CanonicalPath {
314    fn as_ref(&self) -> &Path {
315        &self.0
316    }
317}
318
319impl ToOwned for CanonicalPath {
320    type Owned = CanonicalPathBuf;
321
322    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.
329pub 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)))]
336mod 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 {
366        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]
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]
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]
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]
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}
OSZAR »