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}