omnipath/
posix.rs

1#![cfg(any(doc, all(unix, feature = "std")))]
2use std::env;
3use std::io;
4#[cfg(not(doc))]
5use std::os::unix::ffi::OsStrExt;
6use std::path::Component;
7use std::path::{Path, PathBuf};
8
9pub trait PosixPathExt: Sealed {
10    /// [Unix only] Make a POSIX path absolute without changing its semantics.
11    ///
12    /// Unlike canonicalize the path does not need to exist. Symlinks and `..`
13    /// components will not be resolved.
14    ///
15    /// # Example
16    ///
17    /// ```
18    /// #[cfg(unix)]
19    /// {
20    ///     use omnipath::posix::PosixPathExt;
21    ///     use std::path::Path;
22    ///     use std::env::current_dir;
23    ///
24    ///     let path = Path::new(r"path/to/..//./file");
25    ///     assert_eq!(
26    ///         path.posix_absolute().unwrap(),
27    ///         current_dir().unwrap().join("path/to/../file")
28    ///     )
29    /// }
30    /// ```
31    fn posix_absolute(&self) -> io::Result<PathBuf>;
32
33    /// [Unix only] Make a POSIX path lexically absolute.
34    ///
35    /// Unlike `canonicalize` the path does not need to exist. Symlinks will not be resolved.
36    /// Unlike [`posix_absolute`][PosixPathExt::posix_absolute] this resolves `..` components by popping the
37    /// parent component. This means that it may resolve to a different path
38    /// than would be resolved by passing the path directly to the OS.
39    ///
40    /// Usually this is not the preferred behaviour.
41    ///
42    /// # Example
43    ///
44    /// ```
45    /// #[cfg(unix)]
46    /// {
47    ///     use omnipath::posix::PosixPathExt;
48    ///     use std::path::Path;
49    ///     use std::env::current_dir;
50    ///
51    ///     let path = Path::new(r"path/to/..//./file");
52    ///     assert_eq!(
53    ///         path.posix_lexically_absolute().unwrap(),
54    ///         current_dir().unwrap().join("path/file")
55    ///     )
56    /// }
57    /// ```
58    fn posix_lexically_absolute(&self) -> io::Result<PathBuf>;
59
60    /// [Unix only] Make a POSIX path absolute relative to a provided working
61    /// directory without changing its semantics.
62    ///
63    /// See [`PosixPathExt::posix_absolute`] for a version of this function that
64    /// is relative to [`std::env::current_dir()`] instead.
65    ///
66    /// # Example
67    ///
68    /// ```
69    /// #[cfg(unix)]
70    /// {
71    ///     use omnipath::posix::PosixPathExt;
72    ///     use std::path::Path;
73    ///     let working_dir = Path::new("/tmp");
74    ///     let path = Path::new("path/to/..//./file");
75    ///
76    ///     assert_eq!(
77    ///         &path.posix_absolute_from(working_dir).unwrap(),
78    ///         Path::new("/tmp/path/to/../file"),
79    ///     )
80    /// }
81    /// ```
82    ///
83    /// ```
84    /// #[cfg(unix)]
85    /// {
86    ///     use omnipath::posix::PosixPathExt;
87    ///     use std::path::Path;
88    ///     use std::env::current_dir;
89    ///     let root = Path::new("/tmp/foo//.././bar");
90    ///     let path = Path::new(r"path/to/..//./file");
91    ///     assert_eq!(
92    ///         &path.posix_absolute_from(root).unwrap(),
93    ///         Path::new("/tmp/foo/../bar/path/to/../file"),
94    ///     );
95    /// }
96    /// ```
97    fn posix_absolute_from(&self, path: &Path) -> io::Result<PathBuf>;
98
99    /// [Unix only] Make a POSIX path lexically absolute relative to a provided
100    /// current working directory.
101    ///
102    /// Unlike `canonicalize` the path does not need to exist. Symlinks will not be resolved.
103    /// Unlike [`posix_absolute`][PosixPathExt::posix_absolute] this resolves `..` components by popping the
104    /// parent component. This means that it may resolve to a different path
105    /// than would be resolved by passing the path directly to the OS.
106    ///
107    /// Usually this is not the preferred behaviour.
108    ///
109    /// See [`PosixPathExt::posix_lexically_absolute`] for a version of this function that
110    /// is relative to [`std::env::current_dir()`] instead.
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// #[cfg(unix)]
116    /// {
117    ///     use omnipath::posix::PosixPathExt;
118    ///     use std::path::Path;
119    ///     use std::env::current_dir;
120    ///     let root = Path::new("/tmp");
121    ///     let path = Path::new(r"path/to/..//./file");
122    ///     assert_eq!(
123    ///         &path.posix_lexically_absolute_from(root).unwrap(),
124    ///         Path::new("/tmp/path/file")
125    ///     );
126    /// }
127    /// ```
128    ///
129    /// ```
130    /// #[cfg(unix)]
131    /// {
132    ///     use omnipath::posix::PosixPathExt;
133    ///     use std::path::Path;
134    ///     use std::env::current_dir;
135    ///     let root = Path::new("/tmp/foo//.././bar");
136    ///     let path = Path::new(r"path/to/..//./file");
137    ///     assert_eq!(
138    ///         &path.posix_lexically_absolute_from(root).unwrap(),
139    ///         Path::new("/tmp/bar/path/file")
140    ///     );
141    /// }
142    /// ```
143    fn posix_lexically_absolute_from(&self, cwd: &Path) -> io::Result<PathBuf>;
144}
145
146impl PosixPathExt for Path {
147    fn posix_absolute(&self) -> io::Result<PathBuf> {
148        posix_absolute_from(self, env::current_dir)
149    }
150
151    fn posix_lexically_absolute(&self) -> io::Result<PathBuf> {
152        posix_lexically_absolute_from(self, env::current_dir)
153    }
154
155    fn posix_absolute_from(&self, cwd: &Path) -> io::Result<PathBuf> {
156        if !cwd.is_absolute() {
157            return Err(cwd_error());
158        }
159        posix_absolute_from(self, || posix_absolute_from(cwd, || unreachable!()))
160    }
161
162    fn posix_lexically_absolute_from(&self, cwd: &Path) -> io::Result<PathBuf> {
163        if !cwd.is_absolute() {
164            return Err(cwd_error());
165        }
166        posix_lexically_absolute_from(self, || {
167            posix_lexically_absolute_from(cwd, || unreachable!())
168        })
169    }
170}
171
172fn cwd_error() -> io::Error {
173    io::Error::new(
174        io::ErrorKind::InvalidInput,
175        "expected an absolute path as the current working directory",
176    )
177}
178
179mod private {
180    pub trait Sealed {}
181    impl Sealed for std::path::Path {}
182}
183use private::Sealed;
184
185fn posix_lexically_absolute_from<F>(path: &Path, get_cwd: F) -> io::Result<PathBuf>
186where
187    F: FnOnce() -> io::Result<PathBuf>,
188{
189    // This is mostly a wrapper around collecting `Path::components`, with
190    // exceptions made where this conflicts with the POSIX specification.
191    // See 4.13 Pathname Resolution, IEEE Std 1003.1-2017
192    // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13
193
194    // Get the components, skipping the redundant leading "." component if it exists.
195    let mut components = path.strip_prefix(".").unwrap_or(path).components();
196    let path_os = path.as_os_str().as_bytes();
197
198    let mut normalized = if path.is_absolute() {
199        // "If a pathname begins with two successive <slash> characters, the
200        // first component following the leading <slash> characters may be
201        // interpreted in an implementation-defined manner, although more than
202        // two leading <slash> characters shall be treated as a single <slash>
203        // character."
204        if path_os.starts_with(b"//") && !path_os.starts_with(b"///") {
205            components.next();
206            PathBuf::from("//")
207        } else {
208            PathBuf::new()
209        }
210    } else {
211        get_cwd()?
212    };
213    components.for_each(|component| {
214        if component == Component::ParentDir {
215            normalized.pop();
216        } else {
217            normalized.push(component);
218        }
219    });
220
221    // "Interfaces using pathname resolution may specify additional constraints
222    // when a pathname that does not name an existing directory contains at
223    // least one non- <slash> character and contains one or more trailing
224    // <slash> characters".
225    // A trailing <slash> is also meaningful if "a symbolic link is
226    // encountered during pathname resolution".
227    if path_os.ends_with(b"/") {
228        normalized.push("");
229    }
230
231    Ok(normalized)
232}
233
234fn posix_absolute_from<F>(path: &Path, get_cwd: F) -> io::Result<PathBuf>
235where
236    F: FnOnce() -> io::Result<PathBuf>,
237{
238    // This is mostly a wrapper around collecting `Path::components`, with
239    // exceptions made where this conflicts with the POSIX specification.
240    // See 4.13 Pathname Resolution, IEEE Std 1003.1-2017
241    // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13
242
243    // Get the components, skipping the redundant leading "." component if it exists.
244    let mut components = path.strip_prefix(".").unwrap_or(path).components();
245    let path_os = path.as_os_str().as_bytes();
246
247    let mut normalized = if path.is_absolute() {
248        // "If a pathname begins with two successive <slash> characters, the
249        // first component following the leading <slash> characters may be
250        // interpreted in an implementation-defined manner, although more than
251        // two leading <slash> characters shall be treated as a single <slash>
252        // character."
253        if path_os.starts_with(b"//") && !path_os.starts_with(b"///") {
254            components.next();
255            PathBuf::from("//")
256        } else {
257            PathBuf::new()
258        }
259    } else {
260        get_cwd()?
261    };
262    normalized.extend(components);
263
264    // "Interfaces using pathname resolution may specify additional constraints
265    // when a pathname that does not name an existing directory contains at
266    // least one non- <slash> character and contains one or more trailing
267    // <slash> characters".
268    // A trailing <slash> is also meaningful if "a symbolic link is
269    // encountered during pathname resolution".
270    if path_os.ends_with(b"/") {
271        normalized.push("");
272    }
273
274    Ok(normalized)
275}