> {
236 | let mut output = Vec::new();
237 |
238 | for c in pattern.components() {
239 | output.push(match c {
240 | relative_path::Component::CurDir => continue,
241 | relative_path::Component::ParentDir => Component::ParentDir,
242 | relative_path::Component::Normal("**") => Component::StarStar,
243 | relative_path::Component::Normal(normal) => {
244 | let fragment = Fragment::parse(normal);
245 |
246 | if let Some(normal) = fragment.as_literal() {
247 | Component::Normal(normal)
248 | } else {
249 | Component::Fragment(fragment)
250 | }
251 | }
252 | });
253 | }
254 |
255 | output
256 | }
257 |
258 | #[derive(Debug, Clone, Copy)]
259 | enum Part<'a> {
260 | Star,
261 | Literal(&'a str),
262 | }
263 |
264 | /// A match fragment.
265 | #[derive(Debug, Clone)]
266 | pub(crate) struct Fragment<'a> {
267 | parts: Box<[Part<'a>]>,
268 | }
269 |
270 | impl<'a> Fragment<'a> {
271 | pub(crate) fn parse(string: &'a str) -> Fragment<'a> {
272 | let mut literal = true;
273 | let mut parts = Vec::new();
274 | let mut start = None;
275 |
276 | for (n, c) in string.char_indices() {
277 | if c == '*' {
278 | if let Some(s) = start.take() {
279 | parts.push(Part::Literal(&string[s..n]));
280 | }
281 |
282 | if mem::take(&mut literal) {
283 | parts.push(Part::Star);
284 | }
285 | } else {
286 | if start.is_none() {
287 | start = Some(n);
288 | }
289 |
290 | literal = true;
291 | }
292 | }
293 |
294 | if let Some(s) = start {
295 | parts.push(Part::Literal(&string[s..]));
296 | }
297 |
298 | Fragment {
299 | parts: parts.into(),
300 | }
301 | }
302 |
303 | /// Test if the given string matches the current fragment.
304 | pub(crate) fn is_match(&self, string: &str) -> bool {
305 | let mut backtrack = VecDeque::new();
306 | backtrack.push_back((self.parts.as_ref(), string));
307 |
308 | while let Some((mut parts, mut string)) = backtrack.pop_front() {
309 | while let Some(part) = parts.first() {
310 | match part {
311 | Part::Star => {
312 | // Peek the next literal component. If we have a
313 | // trailing wildcard (which this constitutes) then it
314 | // is by definition a match.
315 | let Some(Part::Literal(peek)) = parts.get(1) else {
316 | return true;
317 | };
318 |
319 | let Some(peek) = peek.chars().next() else {
320 | return true;
321 | };
322 |
323 | while let Some(c) = string.chars().next() {
324 | if c == peek {
325 | backtrack.push_front((
326 | parts,
327 | string.get(c.len_utf8()..).unwrap_or_default(),
328 | ));
329 | break;
330 | }
331 |
332 | string = string.get(c.len_utf8()..).unwrap_or_default();
333 | }
334 | }
335 | Part::Literal(literal) => {
336 | // The literal component must be an exact prefix of the
337 | // current string.
338 | let Some(remainder) = string.strip_prefix(literal) else {
339 | return false;
340 | };
341 |
342 | string = remainder;
343 | }
344 | }
345 |
346 | parts = parts.get(1..).unwrap_or_default();
347 | }
348 |
349 | if string.is_empty() {
350 | return true;
351 | }
352 | }
353 |
354 | false
355 | }
356 |
357 | /// Treat the fragment as a single normal component.
358 | fn as_literal(&self) -> Option<&'a str> {
359 | if let [Part::Literal(one)] = self.parts.as_ref() {
360 | Some(one)
361 | } else {
362 | None
363 | }
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/relative-path-utils/src/glob/tests.rs:
--------------------------------------------------------------------------------
1 | use super::Fragment;
2 |
3 | #[test]
4 | fn test_fragment() {
5 | let fragment = Fragment::parse("abc*def.*");
6 | assert!(fragment.is_match("abc_xyz_def.exe"));
7 | assert!(fragment.is_match("abc_xyz_def."));
8 | assert!(!fragment.is_match("ab_xyz_def.exe"));
9 | assert!(!fragment.is_match("abcdef"));
10 | assert!(!fragment.is_match("abc_xyz_def"));
11 |
12 | let fragment = Fragment::parse("*def");
13 | assert!(fragment.is_match("abcdef"));
14 | assert!(!fragment.is_match("abcdeftrailing"));
15 |
16 | let fragment = Fragment::parse("abc*");
17 | assert!(fragment.is_match("abcdef"));
18 | assert!(!fragment.is_match("leadingabc"));
19 | }
20 |
--------------------------------------------------------------------------------
/relative-path-utils/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! [
](https://github.com/udoprog/relative-path)
2 | //! [
](https://crates.io/crates/relative-path-utils)
3 | //! [
](https://docs.rs/relative-path-utils)
4 | //!
5 | //! Utilities for working with relative paths.
6 | //!
7 | //! This crate contains:
8 | //! * [`Root`] the `root` feature - A root directory that can be used to open
9 | //! files relative to it.
10 | //! * [`Glob`] the `root` feature - A glob pattern that can be used to match
11 | //! files relative to a [`Root`].
12 | //!
13 | //! [`Root`]: https://docs.rs/relative-path-utils/latest/relative_path_utils/struct.Root.html
14 | //! [`Glob`]: https://docs.rs/relative-path-utils/latest/relative_path_utils/struct.Glob.html
15 |
16 | #![deny(missing_docs)]
17 | #![no_std]
18 | #![cfg_attr(relative_path_docsrs, feature(doc_cfg))]
19 |
20 | #[cfg(feature = "alloc")]
21 | extern crate alloc;
22 |
23 | #[cfg(feature = "std")]
24 | extern crate std;
25 |
26 | #[cfg(feature = "root")]
27 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "root")))]
28 | #[doc(inline)]
29 | pub use self::root::{DirEntry, Metadata, OpenOptions, ReadDir, Root};
30 | #[cfg(feature = "root")]
31 | mod root;
32 |
33 | #[cfg(feature = "root")]
34 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "root")))]
35 | #[doc(inline)]
36 | pub use self::glob::Glob;
37 | #[cfg(feature = "root")]
38 | mod glob;
39 |
--------------------------------------------------------------------------------
/relative-path-utils/src/root/mod.rs:
--------------------------------------------------------------------------------
1 | // Some documentation copied from the Rust project under the MIT license.
2 | //
3 | // See https://github.com/rust-lang/rust
4 |
5 | use alloc::string::String;
6 | use alloc::vec::Vec;
7 |
8 | use std::ffi::OsString;
9 | use std::fs::File;
10 | use std::io::{self, Read, Write};
11 | use std::path::Path;
12 |
13 | use crate::Glob;
14 | use relative_path::RelativePath;
15 |
16 | #[cfg_attr(windows, path = "windows.rs")]
17 | #[cfg_attr(unix, path = "unix.rs")]
18 | mod imp;
19 |
20 | #[cfg(not(any(windows, unix)))]
21 | compile_error!("root is only supported on cfg(windows) and cfg(unix)");
22 |
23 | /// An open root directory from which relative paths can be opened.
24 | ///
25 | /// In contrast to using APIs such as [`RelativePath::to_path`], this does not
26 | /// require allocations to open a path.
27 | ///
28 | /// This is achieved by keeping an open handle to the directory and using
29 | /// platform-specific APIs to open a relative path, such as [`openat`] on `unix`.
30 | ///
31 | /// [`openat`]: https://linux.die.net/man/2/openat
32 | pub struct Root {
33 | inner: imp::Root,
34 | }
35 |
36 | impl Root {
37 | /// Open the given directory that can be used as a root for opening and
38 | /// manipulating relative paths.
39 | ///
40 | /// # Errors
41 | ///
42 | /// Errors if the underlying I/O operation fails.
43 | ///
44 | /// # Examples
45 | ///
46 | /// ```no_run
47 | /// use relative_path_utils::Root;
48 | ///
49 | /// let root = Root::new(".")?;
50 | /// # Ok::<_, std::io::Error>(())
51 | /// ```
52 | pub fn new(path: P) -> io::Result
53 | where
54 | P: AsRef,
55 | {
56 | Ok(Self {
57 | inner: imp::Root::new(path.as_ref())?,
58 | })
59 | }
60 |
61 | /// Construct an open options associated with this root.
62 | ///
63 | /// # Examples
64 | ///
65 | /// ```no_run
66 | /// use relative_path_utils::Root;
67 | ///
68 | /// let root = Root::new(".")?;
69 | ///
70 | /// let file = root.open_options().read(true).open("foo.txt");
71 | /// # Ok::<_, std::io::Error>(())
72 | /// ```
73 | pub fn open_options(&self) -> OpenOptions {
74 | OpenOptions {
75 | root: &self.inner,
76 | options: imp::OpenOptions::new(),
77 | }
78 | }
79 |
80 | /// Opens a file in write-only mode.
81 | ///
82 | /// This function will create a file if it does not exist, and will truncate
83 | /// it if it does.
84 | ///
85 | /// Depending on the platform, this function may fail if the full directory
86 | /// path does not exist. See the [`OpenOptions::open`] function for more
87 | /// details.
88 | ///
89 | /// See also [`Root::write()`] for a simple function to create a file with a
90 | /// given data.
91 | ///
92 | /// # Errors
93 | ///
94 | /// Errors if the underlying I/O operation fails.
95 | ///
96 | /// # Examples
97 | ///
98 | /// ```no_run
99 | /// use std::io::Write;
100 | ///
101 | /// use relative_path_utils::Root;
102 | ///
103 | /// let root = Root::new(".")?;
104 | ///
105 | /// let mut f = root.create("foo.txt")?;
106 | /// f.write_all(&1234_u32.to_be_bytes())?;
107 | /// # Ok::<_, std::io::Error>(())
108 | /// ```
109 | pub fn create(&self, path: P) -> io::Result
110 | where
111 | P: AsRef,
112 | {
113 | self.open_options().write(true).create(true).open(path)
114 | }
115 |
116 | /// Attempts to open a file in read-only mode.
117 | ///
118 | /// See the [`OpenOptions::open`] method for more details.
119 | ///
120 | /// If you only need to read the entire file contents, consider
121 | /// [`std::fs::read()`][Root::read] or
122 | /// [`std::fs::read_to_string()`][Root::read_to_string] instead.
123 | ///
124 | /// # Errors
125 | ///
126 | /// This function will return an error if `path` does not already exist.
127 | /// Other errors may also be returned according to [`OpenOptions::open`].
128 | ///
129 | /// # Examples
130 | ///
131 | /// ```no_run
132 | /// use std::io::Read;
133 | ///
134 | /// use relative_path_utils::Root;
135 | ///
136 | /// let root = Root::new(".")?;
137 | ///
138 | /// let mut f = root.open("foo.txt")?;
139 | /// let mut data = vec![];
140 | /// f.read_to_end(&mut data)?;
141 | /// # Ok::<_, std::io::Error>(())
142 | /// ```
143 | pub fn open(&self, path: P) -> io::Result
144 | where
145 | P: AsRef,
146 | {
147 | self.open_options().read(true).open(path)
148 | }
149 |
150 | /// Read the entire contents of a file into a bytes vector.
151 | ///
152 | /// This is a convenience function for using [`File::open`] and
153 | /// [`read_to_end`] with fewer imports and without an intermediate variable.
154 | ///
155 | /// [`read_to_end`]: Read::read_to_end
156 | ///
157 | /// # Errors
158 | ///
159 | /// This function will return an error if `path` does not already exist.
160 | /// Other errors may also be returned according to [`OpenOptions::open`].
161 | ///
162 | /// While reading from the file, this function handles
163 | /// [`io::ErrorKind::Interrupted`] with automatic retries. See [`io::Read`]
164 | /// documentation for details.
165 | ///
166 | /// # Examples
167 | ///
168 | /// ```no_run
169 | /// use std::net::SocketAddr;
170 | ///
171 | /// use relative_path_utils::Root;
172 | ///
173 | /// let root = Root::new(".")?;
174 | /// let foo: SocketAddr = String::from_utf8_lossy(&root.read("address.txt")?).parse()?;
175 | /// # Ok::<_, Box>(())
176 | /// ```
177 | pub fn read(&self, path: P) -> io::Result>
178 | where
179 | P: AsRef,
180 | {
181 | fn inner(this: &Root, path: &RelativePath) -> io::Result> {
182 | let mut file = this.open(path)?;
183 | let size = file
184 | .metadata()
185 | .map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX))
186 | .ok();
187 | let mut bytes = Vec::with_capacity(size.unwrap_or(0));
188 | file.read_to_end(&mut bytes)?;
189 | Ok(bytes)
190 | }
191 |
192 | inner(self, path.as_ref())
193 | }
194 |
195 | /// Read the entire contents of a file into a string.
196 | ///
197 | /// This is a convenience function for using [`File::open`] and
198 | /// [`read_to_string`] with fewer imports and without an intermediate
199 | /// variable.
200 | ///
201 | /// [`read_to_string`]: Read::read_to_string
202 | ///
203 | /// # Errors
204 | ///
205 | /// This function will return an error if `path` does not already exist.
206 | /// Other errors may also be returned according to [`OpenOptions::open`].
207 | ///
208 | /// If the contents of the file are not valid UTF-8, then an error will also
209 | /// be returned.
210 | ///
211 | /// # Examples
212 | ///
213 | /// ```no_run
214 | /// use std::net::SocketAddr;
215 | ///
216 | /// use relative_path_utils::Root;
217 | ///
218 | /// let root = Root::new(".")?;
219 | /// let foo: SocketAddr = root.read_to_string("address.txt")?.parse()?;
220 | /// # Ok::<_, Box>(())
221 | /// ```
222 | pub fn read_to_string(&self, path: P) -> io::Result
223 | where
224 | P: AsRef,
225 | {
226 | fn inner(this: &Root, path: &RelativePath) -> io::Result {
227 | let mut file = this.open(path)?;
228 | let size = file
229 | .metadata()
230 | .map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX))
231 | .ok();
232 | let mut string = String::with_capacity(size.unwrap_or(0));
233 | file.read_to_string(&mut string)?;
234 | Ok(string)
235 | }
236 |
237 | inner(self, path.as_ref())
238 | }
239 |
240 | /// Write a slice as the entire contents of a file.
241 | ///
242 | /// This function will create a file if it does not exist,
243 | /// and will entirely replace its contents if it does.
244 | ///
245 | /// Depending on the platform, this function may fail if the
246 | /// full directory path does not exist.
247 | ///
248 | /// This is a convenience function for using [`File::create`] and
249 | /// [`write_all`] with fewer imports.
250 | ///
251 | /// [`write_all`]: Write::write_all
252 | ///
253 | /// # Errors
254 | ///
255 | /// Fails if an underlying I/O operation fails.
256 | ///
257 | /// # Examples
258 | ///
259 | /// ```no_run
260 | /// use relative_path_utils::Root;
261 | ///
262 | /// let root = Root::new(".")?;
263 | ///
264 | /// root.write("foo.txt", b"Lorem ipsum")?;
265 | /// root.write("bar.txt", "dolor sit")?;
266 | /// # Ok::<_, std::io::Error>(())
267 | /// ```
268 | pub fn write(&self, path: P, contents: C) -> io::Result<()>
269 | where
270 | P: AsRef,
271 | C: AsRef<[u8]>,
272 | {
273 | self.create(path)?.write_all(contents.as_ref())
274 | }
275 |
276 | /// Given a path, query the file system to get information about a file,
277 | /// directory, etc.
278 | ///
279 | /// This function will traverse symbolic links to query information about
280 | /// the destination file.
281 | ///
282 | /// # Platform-specific behavior
283 | ///
284 | /// This function currently corresponds to the `stat` function on Unix and
285 | /// the `GetFileInformationByHandle` function on Windows. Note that, this
286 | /// [may change in the future][changes].
287 | ///
288 | /// [changes]: io#platform-specific-behavior
289 | ///
290 | /// # Errors
291 | ///
292 | /// This function will return an error in the following situations, but is
293 | /// not limited to just these cases:
294 | ///
295 | /// * The user lacks permissions to perform `metadata` call on `path`.
296 | /// * `path` does not exist.
297 | ///
298 | /// # Examples
299 | ///
300 | /// ```rust,no_run
301 | /// use relative_path_utils::Root;
302 | ///
303 | /// let root = Root::new(".")?;
304 | /// let attr = root.metadata("file/path.txt")?;
305 | /// # Ok::<_, std::io::Error>(())
306 | /// ```
307 | pub fn metadata(&self, path: P) -> io::Result
308 | where
309 | P: AsRef,
310 | {
311 | Ok(Metadata {
312 | inner: self.inner.metadata(path.as_ref())?,
313 | })
314 | }
315 |
316 | /// Returns `true` if the path exists on disk and is pointing at a
317 | /// directory.
318 | ///
319 | /// This function will traverse symbolic links to query information about
320 | /// the destination file.
321 | ///
322 | /// If you cannot access the metadata of the file, e.g. because of a
323 | /// permission error or broken symbolic links, this will return `false`.
324 | ///
325 | /// # Examples
326 | ///
327 | /// ```no_run
328 | /// use relative_path_utils::Root;
329 | ///
330 | /// let root = Root::new(".")?;
331 | ///
332 | /// assert_eq!(root.is_dir("./is_a_directory/"), true);
333 | /// assert_eq!(root.is_dir("a_file.txt"), false);
334 | /// # Ok::<_, std::io::Error>(())
335 | /// ```
336 | ///
337 | /// # See Also
338 | ///
339 | /// This is a convenience function that coerces errors to false. If you want
340 | /// to check errors, call [`Root::metadata`] and handle its [`Result`]. Then
341 | /// call [`Metadata::is_dir`] if it was [`Ok`].
342 | pub fn is_dir(&self, path: P) -> bool
343 | where
344 | P: AsRef,
345 | {
346 | self.metadata(path).map(|m| m.is_dir()).unwrap_or(false)
347 | }
348 |
349 | /// Returns an iterator over the entries within a directory.
350 | ///
351 | /// The iterator will yield instances of
352 | /// [`io::Result`]<[`DirEntry`]>
. New errors may be encountered
353 | /// after an iterator is initially constructed. Entries for the current and
354 | /// parent directories (typically `.` and `..`) are skipped.
355 | ///
356 | /// # Platform-specific behavior
357 | ///
358 | /// This function currently corresponds to the `opendir` function on Unix
359 | /// and the `FindFirstFile` function on Windows. Advancing the iterator
360 | /// currently corresponds to `readdir` on Unix and `FindNextFile` on Windows.
361 | /// Note that, this [may change in the future][changes].
362 | ///
363 | /// [changes]: io#platform-specific-behavior
364 | ///
365 | /// The order in which this iterator returns entries is platform and filesystem
366 | /// dependent.
367 | ///
368 | /// # Errors
369 | ///
370 | /// This function will return an error in the following situations, but is not
371 | /// limited to just these cases:
372 | ///
373 | /// * The provided `path` doesn't exist.
374 | /// * The process lacks permissions to view the contents.
375 | /// * The `path` points at a non-directory file.
376 | ///
377 | /// # Examples
378 | ///
379 | /// ```
380 | /// use std::io;
381 | ///
382 | /// use relative_path_utils::{Root, DirEntry};
383 | /// use relative_path::RelativePath;
384 | ///
385 | /// // one possible implementation of walking a directory only visiting files
386 | /// fn visit_dirs(root: &Root, dir: &RelativePath, cb: &dyn Fn(&DirEntry)) -> io::Result<()> {
387 | /// if root.is_dir(dir) {
388 | /// for entry in root.read_dir(dir)? {
389 | /// let entry = entry?;
390 | /// let file_name = entry.file_name();
391 | /// let path = dir.join(file_name.to_string_lossy().as_ref());
392 | ///
393 | /// if root.is_dir(&path) {
394 | /// visit_dirs(root, &path, cb)?;
395 | /// } else {
396 | /// cb(&entry);
397 | /// }
398 | /// }
399 | /// }
400 | ///
401 | /// Ok(())
402 | /// }
403 | /// ```
404 | pub fn read_dir(&self, path: P) -> io::Result
405 | where
406 | P: AsRef,
407 | {
408 | self.inner
409 | .read_dir(path.as_ref())
410 | .map(|inner| ReadDir { inner })
411 | }
412 |
413 | /// Parse a glob over the specified path.
414 | ///
415 | /// To perform the globbing, use [`Glob::matcher`].
416 | ///
417 | /// # Examples
418 | ///
419 | /// ```no_run
420 | /// use relative_path_utils::Root;
421 | ///
422 | /// let root = Root::new("src")?;
423 | ///
424 | /// let glob = root.glob("**/*.rs");
425 | ///
426 | /// let mut results = Vec::new();
427 | ///
428 | /// for e in glob.matcher() {
429 | /// results.push(e?);
430 | /// }
431 | ///
432 | /// results.sort();
433 | /// assert_eq!(results, vec!["lib.rs", "main.rs"]);
434 | /// # Ok::<_, Box>(())
435 | /// ```
436 | pub fn glob<'a, P>(&'a self, path: &'a P) -> Glob<'a>
437 | where
438 | P: ?Sized + AsRef,
439 | {
440 | Glob::new(self, path.as_ref())
441 | }
442 | }
443 |
444 | /// Options and flags which can be used to configure how a file is opened.
445 | ///
446 | /// This builder exposes the ability to configure how a [`File`] is opened and
447 | /// what operations are permitted on the open file. The [`File::open`] and
448 | /// [`File::create`] methods are aliases for commonly used options using this
449 | /// builder.
450 | ///
451 | /// Generally speaking, when using `OpenOptions`, you'll first call
452 | /// [`Root::open_options`], then chain calls to methods to set each option, then
453 | /// call [`OpenOptions::open`], passing the path of the file you're trying to
454 | /// open. This will give you a [`io::Result`] with a [`File`] inside that you
455 | /// can further operate on.
456 | ///
457 | /// # Examples
458 | ///
459 | /// Opening a file to read:
460 | ///
461 | /// ```no_run
462 | /// use relative_path_utils::Root;
463 | ///
464 | /// let root = Root::new(".")?;
465 | ///
466 | /// let file = root.open_options().read(true).open("foo.txt");
467 | /// # Ok::<_, std::io::Error>(())
468 | /// ```
469 | ///
470 | /// Opening a file for both reading and writing, as well as creating it if it
471 | /// doesn't exist:
472 | ///
473 | /// ```no_run
474 | /// use relative_path_utils::Root;
475 | ///
476 | /// let root = Root::new(".")?;
477 | ///
478 | /// let file = root
479 | /// .open_options()
480 | /// .read(true)
481 | /// .write(true)
482 | /// .create(true)
483 | /// .open("foo.txt")?;
484 | /// # Ok::<_, std::io::Error>(())
485 | /// ```
486 | #[derive(Clone, Debug)]
487 | #[must_use]
488 | pub struct OpenOptions<'a> {
489 | root: &'a imp::Root,
490 | options: imp::OpenOptions,
491 | }
492 |
493 | impl OpenOptions<'_> {
494 | /// Sets the option for read access.
495 | ///
496 | /// This option, when true, will indicate that the file should be
497 | /// `read`-able if opened.
498 | ///
499 | /// # Examples
500 | ///
501 | /// ```no_run
502 | /// use relative_path_utils::Root;
503 | ///
504 | /// let root = Root::new(".")?;
505 | ///
506 | /// let file = root.open_options().read(true).open("foo.txt");
507 | /// # Ok::<_, std::io::Error>(())
508 | /// ```
509 | pub fn read(&mut self, read: bool) -> &mut Self {
510 | self.options.read(read);
511 | self
512 | }
513 |
514 | /// Sets the option for write access.
515 | ///
516 | /// This option, when true, will indicate that the file should be
517 | /// `write`-able if opened.
518 | ///
519 | /// If the file already exists, any write calls on it will overwrite its
520 | /// contents, without truncating it.
521 | ///
522 | /// # Examples
523 | ///
524 | /// ```no_run
525 | /// use relative_path_utils::Root;
526 | ///
527 | /// let root = Root::new(".")?;
528 | ///
529 | /// let file = root.open_options().write(true).open("foo.txt");
530 | /// # Ok::<_, std::io::Error>(())
531 | /// ```
532 | pub fn write(&mut self, write: bool) -> &mut Self {
533 | self.options.write(write);
534 | self
535 | }
536 |
537 | /// Sets the option for the append mode.
538 | ///
539 | /// This option, when true, means that writes will append to a file instead
540 | /// of overwriting previous contents. Note that setting
541 | /// `.write(true).append(true)` has the same effect as setting only
542 | /// `.append(true)`.
543 | ///
544 | /// For most filesystems, the operating system guarantees that all writes
545 | /// are atomic: no writes get mangled because another process writes at the
546 | /// same time.
547 | ///
548 | /// One maybe obvious note when using append-mode: make sure that all data
549 | /// that belongs together is written to the file in one operation. This can
550 | /// be done by concatenating strings before passing them to [`write()`], or
551 | /// using a buffered writer (with a buffer of adequate size), and calling
552 | /// [`flush()`] when the message is complete.
553 | ///
554 | /// If a file is opened with both read and append access, beware that after
555 | /// opening, and after every write, the position for reading may be set at
556 | /// the end of the file. So, before writing, save the current position
557 | /// (using [`seek`]\([`SeekFrom`]::[`Current`]\(0))
), and
558 | /// restore it before the next read.
559 | ///
560 | /// ## Note
561 | ///
562 | /// This function doesn't create the file if it doesn't exist. Use the
563 | /// [`OpenOptions::create`] method to do so.
564 | ///
565 | /// [`write()`]: Write::write "io::Write::write"
566 | /// [`flush()`]: Write::flush "io::Write::flush"
567 | /// [`seek`]: std::io::Seek::seek "io::Seek::seek"
568 | /// [`SeekFrom`]: std::io::SeekFrom
569 | /// [`Current`]: std::io::SeekFrom::Current
570 | ///
571 | /// # Examples
572 | ///
573 | /// ```no_run
574 | /// use relative_path_utils::Root;
575 | ///
576 | /// let root = Root::new(".")?;
577 | ///
578 | /// let file = root.open_options().append(true).open("foo.txt");
579 | /// # Ok::<_, std::io::Error>(())
580 | /// ```
581 | pub fn append(&mut self, append: bool) -> &mut Self {
582 | self.options.append(append);
583 | self
584 | }
585 |
586 | /// Sets the option for truncating a previous file.
587 | ///
588 | /// If a file is successfully opened with this option set it will truncate
589 | /// the file to 0 length if it already exists.
590 | ///
591 | /// The file must be opened with write access for truncate to work.
592 | ///
593 | /// # Examples
594 | ///
595 | /// ```no_run
596 | /// use relative_path_utils::Root;
597 | ///
598 | /// let root = Root::new(".")?;
599 | ///
600 | /// let file = root.open_options().write(true).truncate(true).open("foo.txt");
601 | /// # Ok::<_, std::io::Error>(())
602 | /// ```
603 | pub fn truncate(&mut self, truncate: bool) -> &mut Self {
604 | self.options.truncate(truncate);
605 | self
606 | }
607 |
608 | /// Sets the option to create a new file, or open it if it already exists.
609 | ///
610 | /// In order for the file to be created, [`OpenOptions::write`] or
611 | /// [`OpenOptions::append`] access must be used.
612 | ///
613 | /// See also [`Root::write()`] for a simple function to create a file with a
614 | /// given data.
615 | ///
616 | /// # Examples
617 | ///
618 | /// ```no_run
619 | /// use relative_path_utils::Root;
620 | ///
621 | /// let root = Root::new(".")?;
622 | ///
623 | /// let file = root.open_options().write(true).create(true).open("foo.txt");
624 | /// # Ok::<_, std::io::Error>(())
625 | /// ```
626 | pub fn create(&mut self, create: bool) -> &mut Self {
627 | self.options.create(create);
628 | self
629 | }
630 |
631 | /// Sets the option to create a new file, failing if it already exists.
632 | ///
633 | /// No file is allowed to exist at the target location, also no (dangling) symlink. In this
634 | /// way, if the call succeeds, the file returned is guaranteed to be new.
635 | ///
636 | /// This option is useful because it is atomic. Otherwise between checking
637 | /// whether a file exists and creating a new one, the file may have been
638 | /// created by another process (a TOCTOU race condition / attack).
639 | ///
640 | /// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are
641 | /// ignored.
642 | ///
643 | /// The file must be opened with write or append access in order to create
644 | /// a new file.
645 | ///
646 | /// [`.create()`]: OpenOptions::create
647 | /// [`.truncate()`]: OpenOptions::truncate
648 | ///
649 | /// # Examples
650 | ///
651 | /// ```no_run
652 | /// use relative_path_utils::Root;
653 | ///
654 | /// let root = Root::new(".")?;
655 | ///
656 | /// let file = root.open_options().write(true).create_new(true).open("foo.txt");
657 | /// # Ok::<_, std::io::Error>(())
658 | /// ```
659 | pub fn create_new(&mut self, create_new: bool) -> &mut Self {
660 | self.options.create_new(create_new);
661 | self
662 | }
663 |
664 | /// Opens a file at `path` with the options specified by `self`.
665 | ///
666 | /// # Errors
667 | ///
668 | /// This function will return an error under a number of different
669 | /// circumstances. Some of these error conditions are listed here, together
670 | /// with their [`io::ErrorKind`]. The mapping to [`io::ErrorKind`]s is not
671 | /// part of the compatibility contract of the function.
672 | ///
673 | /// * [`NotFound`]: The specified file does not exist and neither `create`
674 | /// or `create_new` is set.
675 | /// * [`NotFound`]: One of the directory components of the file path does
676 | /// not exist.
677 | /// * [`PermissionDenied`]: The user lacks permission to get the specified
678 | /// access rights for the file.
679 | /// * [`PermissionDenied`]: The user lacks permission to open one of the
680 | /// directory components of the specified path.
681 | /// * [`AlreadyExists`]: `create_new` was specified and the file already
682 | /// exists.
683 | /// * [`InvalidInput`]: Invalid combinations of open options (truncate
684 | /// without write access, no access mode set, etc.).
685 | ///
686 | /// The following errors don't match any existing [`io::ErrorKind`] at the moment:
687 | /// * One of the directory components of the specified file path
688 | /// was not, in fact, a directory.
689 | /// * Filesystem-level errors: full disk, write permission
690 | /// requested on a read-only file system, exceeded disk quota, too many
691 | /// open files, too long filename, too many symbolic links in the
692 | /// specified path (Unix-like systems only), etc.
693 | ///
694 | /// # Examples
695 | ///
696 | /// ```no_run
697 | /// use relative_path_utils::Root;
698 | ///
699 | /// let root = Root::new(".")?;
700 | ///
701 | /// let file = root.open_options().read(true).open("foo.txt");
702 | /// # Ok::<_, std::io::Error>(())
703 | /// ```
704 | ///
705 | /// [`AlreadyExists`]: io::ErrorKind::AlreadyExists
706 | /// [`InvalidInput`]: io::ErrorKind::InvalidInput
707 | /// [`NotFound`]: io::ErrorKind::NotFound
708 | /// [`PermissionDenied`]: io::ErrorKind::PermissionDenied
709 | pub fn open(&self, path: P) -> io::Result
710 | where
711 | P: AsRef,
712 | {
713 | self.root.open_at(path.as_ref(), &self.options)
714 | }
715 | }
716 |
717 | /// Iterator over the entries in a directory.
718 | ///
719 | /// This iterator is returned from the [`Root::read_dir`] function and will
720 | /// yield instances of [`io::Result`]<[`DirEntry`]>
. Through a
721 | /// [`DirEntry`] information like the entry's path and possibly other metadata
722 | /// can be learned.
723 | ///
724 | /// The order in which this iterator returns entries is platform and filesystem
725 | /// dependent.
726 | ///
727 | /// # Errors
728 | ///
729 | /// This [`io::Result`] will be an [`Err`] if there's some sort of intermittent
730 | /// IO error during iteration.
731 | pub struct ReadDir {
732 | inner: imp::ReadDir,
733 | }
734 |
735 | impl Iterator for ReadDir {
736 | type Item = io::Result;
737 |
738 | #[inline]
739 | fn next(&mut self) -> Option {
740 | let inner = self.inner.next()?;
741 | Some(inner.map(|inner| DirEntry { inner }))
742 | }
743 | }
744 |
745 | /// Entries returned by the [`ReadDir`] iterator.
746 | ///
747 | /// An instance of `DirEntry` represents an entry inside of a directory on the
748 | /// filesystem. Each entry can be inspected via methods to learn about the full
749 | /// path or possibly other metadata through per-platform extension traits.
750 | ///
751 | /// # Platform-specific behavior
752 | ///
753 | /// On Unix, the `DirEntry` struct contains an internal reference to the open
754 | /// directory. Holding `DirEntry` objects will consume a file handle even after
755 | /// the `ReadDir` iterator is dropped.
756 | pub struct DirEntry {
757 | inner: imp::DirEntry,
758 | }
759 |
760 | impl DirEntry {
761 | /// Returns the file name of this directory entry without any
762 | /// leading path component(s).
763 | ///
764 | /// As an example,
765 | /// the output of the function will result in "foo" for all the following paths:
766 | /// - "./foo"
767 | /// - "/the/foo"
768 | /// - "../../foo"
769 | ///
770 | /// # Examples
771 | ///
772 | /// ```no_run
773 | /// use relative_path_utils::Root;
774 | ///
775 | /// let mut root = Root::new(".")?;
776 | ///
777 | /// for entry in root.read_dir("src")? {
778 | /// let entry = entry?;
779 | /// println!("{:?}", entry.file_name());
780 | /// }
781 | /// # Ok::<_, std::io::Error>(())
782 | /// ```
783 | #[must_use]
784 | pub fn file_name(&self) -> OsString {
785 | self.inner.file_name()
786 | }
787 | }
788 |
789 | /// Metadata information about a file.
790 | ///
791 | /// This structure is returned from the [`metadata`] function and represents
792 | /// known metadata about a file such as its permissions, size, modification
793 | /// times, etc.
794 | ///
795 | /// [`metadata`]: Root::metadata
796 | #[derive(Clone)]
797 | pub struct Metadata {
798 | inner: imp::Metadata,
799 | }
800 |
801 | impl Metadata {
802 | /// Returns `true` if this metadata is for a directory. The result is
803 | /// mutually exclusive to the result of [`Metadata::is_file`].
804 | ///
805 | /// # Examples
806 | ///
807 | /// ```no_run
808 | /// use relative_path_utils::Root;
809 | ///
810 | /// let root = Root::new(".")?;
811 | ///
812 | /// let metadata = root.metadata("foo.txt")?;
813 | /// assert!(!metadata.is_dir());
814 | /// # Ok::<_, std::io::Error>(())
815 | /// ```
816 | #[must_use]
817 | #[inline]
818 | pub fn is_dir(&self) -> bool {
819 | self.inner.is_dir()
820 | }
821 |
822 | /// Returns `true` if this metadata is for a regular file. The result is
823 | /// mutually exclusive to the result of [`Metadata::is_dir`].
824 | ///
825 | /// When the goal is simply to read from (or write to) the source, the most
826 | /// reliable way to test the source can be read (or written to) is to open
827 | /// it. Only using `is_file` can break workflows like `diff <( prog_a )` on
828 | /// a Unix-like system for example. See [`Root::open`] or
829 | /// [`OpenOptions::open`] for more information.
830 | ///
831 | /// # Examples
832 | ///
833 | /// ```no_run
834 | /// use relative_path_utils::Root;
835 | ///
836 | /// let root = Root::new(".")?;
837 | ///
838 | /// let metadata = root.metadata("foo.txt")?;
839 | /// assert!(metadata.is_file());
840 | /// # Ok::<_, std::io::Error>(())
841 | /// ```
842 | #[must_use]
843 | #[inline]
844 | pub fn is_file(&self) -> bool {
845 | self.inner.is_file()
846 | }
847 |
848 | /// Returns `true` if this metadata is for a symbolic link.
849 | ///
850 | /// # Examples
851 | ///
852 | /// ```no_run
853 | /// use relative_path_utils::Root;
854 | ///
855 | /// let root = Root::new(".")?;
856 | ///
857 | /// let metadata = root.metadata("foo.txt")?;
858 | /// assert!(metadata.is_symlink());
859 | /// # Ok::<_, std::io::Error>(())
860 | /// ```
861 | #[must_use]
862 | pub fn is_symlink(&self) -> bool {
863 | self.inner.is_symlink()
864 | }
865 | }
866 |
--------------------------------------------------------------------------------
/relative-path-utils/src/root/unix.rs:
--------------------------------------------------------------------------------
1 | use core::mem::MaybeUninit;
2 |
3 | use alloc::vec::Vec;
4 |
5 | use std::ffi::{CString, OsString};
6 | use std::fs::File;
7 | use std::io;
8 | use std::os::fd::OwnedFd;
9 | use std::os::fd::{AsFd, AsRawFd};
10 | use std::os::fd::{FromRawFd, IntoRawFd};
11 | use std::path::{Path, MAIN_SEPARATOR};
12 |
13 | #[cfg(not(any(
14 | all(target_os = "linux", not(target_env = "musl")),
15 | target_os = "emscripten",
16 | target_os = "l4re",
17 | target_os = "android",
18 | target_os = "hurd",
19 | )))]
20 | use libc::{fstat as fstat64, stat as stat64};
21 | #[cfg(any(
22 | all(target_os = "linux", not(target_env = "musl")),
23 | target_os = "emscripten",
24 | target_os = "l4re",
25 | target_os = "hurd"
26 | ))]
27 | use libc::{fstat64, stat64};
28 |
29 | use relative_path::{Component, RelativePath};
30 |
31 | #[derive(Debug)]
32 | pub(super) struct Root {
33 | handle: OwnedFd,
34 | }
35 |
36 | impl Root {
37 | pub(super) fn new(path: &Path) -> io::Result {
38 | let file = std::fs::OpenOptions::new().read(true).open(path)?;
39 |
40 | Ok(Root {
41 | handle: OwnedFd::from(file),
42 | })
43 | }
44 |
45 | pub(super) fn open_at(&self, path: &RelativePath, options: &OpenOptions) -> io::Result {
46 | Ok(File::from(self.open_at_inner(path, options)?))
47 | }
48 |
49 | fn open_at_inner(&self, path: &RelativePath, options: &OpenOptions) -> io::Result {
50 | let path = convert_to_c_string(path)?;
51 |
52 | let flags = libc::O_CLOEXEC | options.get_creation_mode()? | options.get_access_mode()?;
53 |
54 | unsafe {
55 | let fd = libc::openat(self.handle.as_raw_fd(), path.as_ptr(), flags);
56 |
57 | if fd == -1 {
58 | return Err(io::Error::last_os_error());
59 | }
60 |
61 | Ok(OwnedFd::from_raw_fd(fd))
62 | }
63 | }
64 |
65 | pub(super) fn metadata(&self, path: &RelativePath) -> io::Result {
66 | let owned;
67 |
68 | let fd = if is_current(path) {
69 | self.handle.as_fd()
70 | } else {
71 | let mut opts = OpenOptions::new();
72 | opts.read(true);
73 | owned = self.open_at_inner(path, &opts)?;
74 | owned.as_fd()
75 | };
76 |
77 | unsafe {
78 | let mut stat = MaybeUninit::zeroed();
79 |
80 | if fstat64(fd.as_raw_fd(), stat.as_mut_ptr()) == -1 {
81 | return Err(io::Error::last_os_error());
82 | }
83 |
84 | let stat = stat.assume_init();
85 |
86 | Ok(Metadata { stat })
87 | }
88 | }
89 |
90 | pub(super) fn read_dir(&self, path: &RelativePath) -> io::Result {
91 | let mut opts = OpenOptions::new();
92 | opts.read(true);
93 | let fd = self.open_at_inner(path, &opts)?;
94 |
95 | let dir = unsafe {
96 | let handle = libc::fdopendir(fd.as_raw_fd());
97 |
98 | if handle.is_null() {
99 | return Err(io::Error::last_os_error());
100 | }
101 |
102 | // fdopendir() takes ownership of the file descriptor, and it will
103 | // be closed when the `Dir` handle is dropped.
104 | _ = fd.into_raw_fd();
105 | Dir { handle }
106 | };
107 |
108 | Ok(ReadDir {
109 | dir,
110 | end_of_stream: false,
111 | })
112 | }
113 | }
114 |
115 | pub(super) struct ReadDir {
116 | dir: Dir,
117 | end_of_stream: bool,
118 | }
119 |
120 | impl Iterator for ReadDir {
121 | type Item = io::Result;
122 |
123 | #[inline]
124 | fn next(&mut self) -> Option {
125 | self.inner_next()
126 | }
127 | }
128 |
129 | #[cfg(any(
130 | target_os = "android",
131 | target_os = "linux",
132 | target_os = "solaris",
133 | target_os = "fuchsia",
134 | target_os = "redox",
135 | target_os = "illumos",
136 | target_os = "aix",
137 | target_os = "nto",
138 | target_os = "vita",
139 | target_os = "hurd",
140 | ))]
141 | mod read_dir {
142 | use std::ffi::c_char;
143 | use std::ffi::CStr;
144 | use std::ffi::OsString;
145 | use std::io;
146 | use std::mem::size_of;
147 | use std::os::unix::ffi::OsStringExt;
148 |
149 | use super::{DirEntry, ReadDir};
150 |
151 | impl ReadDir {
152 | pub(super) fn inner_next(&mut self) -> Option> {
153 | const D_NAME_OFFSET: usize = size_of::()
154 | + size_of::()
155 | + size_of::()
156 | + size_of::();
157 |
158 | if self.end_of_stream {
159 | return None;
160 | }
161 |
162 | unsafe {
163 | loop {
164 | // As of POSIX.1-2017, readdir() is not required to be thread safe; only
165 | // readdir_r() is. However, readdir_r() cannot correctly handle platforms
166 | // with unlimited or variable NAME_MAX. Many modern platforms guarantee
167 | // thread safety for readdir() as long an individual DIR* is not accessed
168 | // concurrently, which is sufficient for Rust.
169 | errno::set_errno(errno::Errno(0));
170 | let entry_ptr = libc::readdir64(self.dir.handle);
171 |
172 | if entry_ptr.is_null() {
173 | // We either encountered an error, or reached the end. Either way,
174 | // the next call to next() should return None.
175 | self.end_of_stream = true;
176 |
177 | // To distinguish between errors and end-of-directory, we had to clear
178 | // errno beforehand to check for an error now.
179 | return match errno::errno().0 {
180 | 0 => None,
181 | e => Some(Err(io::Error::from_raw_os_error(e))),
182 | };
183 | }
184 |
185 | // d_name is guaranteed to be null-terminated.
186 | let name =
187 | CStr::from_ptr(entry_ptr.cast::().add(D_NAME_OFFSET).cast::());
188 |
189 | let name_bytes = name.to_bytes();
190 |
191 | if name_bytes == b"." || name_bytes == b".." {
192 | continue;
193 | }
194 |
195 | return Some(Ok(DirEntry {
196 | file_name: OsString::from_vec(name_bytes.to_vec()),
197 | }));
198 | }
199 | }
200 | }
201 | }
202 | }
203 |
204 | #[cfg(not(any(
205 | target_os = "android",
206 | target_os = "linux",
207 | target_os = "solaris",
208 | target_os = "fuchsia",
209 | target_os = "redox",
210 | target_os = "illumos",
211 | target_os = "aix",
212 | target_os = "nto",
213 | target_os = "vita",
214 | target_os = "hurd",
215 | )))]
216 | mod read_dir {
217 | use std::ffi::OsString;
218 | use std::io;
219 | use std::mem::MaybeUninit;
220 | use std::os::unix::ffi::OsStringExt;
221 | use std::ptr;
222 | use std::slice;
223 |
224 | use super::{DirEntry, ReadDir};
225 |
226 | impl ReadDir {
227 | pub(super) fn inner_next(&mut self) -> Option> {
228 | if self.end_of_stream {
229 | return None;
230 | }
231 |
232 | unsafe {
233 | loop {
234 | let mut entry = MaybeUninit::zeroed();
235 | let mut entry_ptr = ptr::null_mut();
236 |
237 | let err = libc::readdir_r(self.dir.handle, entry.as_mut_ptr(), &mut entry_ptr);
238 |
239 | if err != 0 {
240 | if entry_ptr.is_null() {
241 | // We encountered an error (which will be returned in this iteration), but
242 | // we also reached the end of the directory stream. The `end_of_stream`
243 | // flag is enabled to make sure that we return `None` in the next iteration
244 | // (instead of looping forever)
245 | self.end_of_stream = true;
246 | }
247 |
248 | return Some(Err(io::Error::from_raw_os_error(err)));
249 | }
250 |
251 | if entry_ptr.is_null() {
252 | return None;
253 | }
254 |
255 | let entry = entry.assume_init();
256 |
257 | let name_bytes = slice::from_raw_parts(
258 | entry.d_name.as_ptr() as *const u8,
259 | entry.d_namlen as usize,
260 | );
261 |
262 | if name_bytes == b"." || name_bytes == b".." {
263 | continue;
264 | }
265 |
266 | return Some(Ok(DirEntry {
267 | file_name: OsString::from_vec(name_bytes.to_vec()),
268 | }));
269 | }
270 | }
271 | }
272 | }
273 | }
274 |
275 | pub(crate) struct DirEntry {
276 | file_name: OsString,
277 | }
278 |
279 | impl DirEntry {
280 | pub(crate) fn file_name(&self) -> OsString {
281 | self.file_name.clone()
282 | }
283 | }
284 |
285 | struct Dir {
286 | handle: *mut libc::DIR,
287 | }
288 |
289 | unsafe impl Send for Dir {}
290 | unsafe impl Sync for Dir {}
291 |
292 | impl Drop for Dir {
293 | #[inline]
294 | fn drop(&mut self) {
295 | _ = unsafe { libc::closedir(self.handle) };
296 | }
297 | }
298 |
299 | unsafe impl Send for OpenOptions {}
300 | unsafe impl Sync for OpenOptions {}
301 |
302 | #[derive(Clone, Debug)]
303 | #[allow(clippy::struct_excessive_bools)]
304 | pub(super) struct OpenOptions {
305 | // generic
306 | read: bool,
307 | write: bool,
308 | append: bool,
309 | truncate: bool,
310 | create: bool,
311 | create_new: bool,
312 | }
313 |
314 | impl OpenOptions {
315 | pub(super) fn new() -> OpenOptions {
316 | OpenOptions {
317 | // generic
318 | read: false,
319 | write: false,
320 | append: false,
321 | truncate: false,
322 | create: false,
323 | create_new: false,
324 | }
325 | }
326 |
327 | pub(super) fn read(&mut self, read: bool) {
328 | self.read = read;
329 | }
330 |
331 | pub(super) fn write(&mut self, write: bool) {
332 | self.write = write;
333 | }
334 |
335 | pub(super) fn append(&mut self, append: bool) {
336 | self.append = append;
337 | }
338 |
339 | pub(super) fn truncate(&mut self, truncate: bool) {
340 | self.truncate = truncate;
341 | }
342 |
343 | pub(super) fn create(&mut self, create: bool) {
344 | self.create = create;
345 | }
346 |
347 | pub(super) fn create_new(&mut self, create_new: bool) {
348 | self.create_new = create_new;
349 | }
350 |
351 | fn get_access_mode(&self) -> io::Result {
352 | match (self.read, self.write, self.append) {
353 | (true, false, false) => Ok(libc::O_RDONLY),
354 | (false, true, false) => Ok(libc::O_WRONLY),
355 | (true, true, false) => Ok(libc::O_RDWR),
356 | (false, _, true) => Ok(libc::O_WRONLY | libc::O_APPEND),
357 | (true, _, true) => Ok(libc::O_RDWR | libc::O_APPEND),
358 | (false, false, false) => Err(io::Error::from_raw_os_error(libc::EINVAL)),
359 | }
360 | }
361 |
362 | fn get_creation_mode(&self) -> io::Result {
363 | match (self.write, self.append) {
364 | (true, false) => {}
365 | (false, false) => {
366 | if self.truncate || self.create || self.create_new {
367 | return Err(io::Error::from_raw_os_error(libc::EINVAL));
368 | }
369 | }
370 | (_, true) => {
371 | if self.truncate && !self.create_new {
372 | return Err(io::Error::from_raw_os_error(libc::EINVAL));
373 | }
374 | }
375 | }
376 |
377 | Ok(match (self.create, self.truncate, self.create_new) {
378 | (false, false, false) => 0,
379 | (true, false, false) => libc::O_CREAT,
380 | (false, true, false) => libc::O_TRUNC,
381 | (true, true, false) => libc::O_CREAT | libc::O_TRUNC,
382 | (_, _, true) => libc::O_CREAT | libc::O_EXCL,
383 | })
384 | }
385 | }
386 |
387 | #[derive(Clone)]
388 | pub(super) struct Metadata {
389 | stat: stat64,
390 | }
391 |
392 | impl Metadata {
393 | #[inline]
394 | pub(super) fn is_dir(&self) -> bool {
395 | self.is(libc::S_IFDIR)
396 | }
397 |
398 | #[inline]
399 | pub(super) fn is_file(&self) -> bool {
400 | self.is(libc::S_IFREG)
401 | }
402 |
403 | #[inline]
404 | pub(super) fn is_symlink(&self) -> bool {
405 | self.is(libc::S_IFLNK)
406 | }
407 |
408 | #[inline]
409 | pub(super) fn is(&self, mode: libc::mode_t) -> bool {
410 | self.masked() == mode
411 | }
412 |
413 | #[inline]
414 | fn masked(&self) -> libc::mode_t {
415 | self.stat.st_mode & libc::S_IFMT
416 | }
417 | }
418 |
419 | fn is_current(path: &RelativePath) -> bool {
420 | path.components().all(|c| c == Component::CurDir)
421 | }
422 |
423 | fn convert_to_c_string(input: &RelativePath) -> io::Result {
424 | let mut path = Vec::new();
425 |
426 | for c in input.components() {
427 | match c {
428 | Component::CurDir => {}
429 | Component::ParentDir => {
430 | if path.is_empty() {
431 | return Err(io::Error::from_raw_os_error(libc::EINVAL));
432 | }
433 |
434 | let index = path
435 | .iter()
436 | .rposition(|&b| b == MAIN_SEPARATOR as u8)
437 | .unwrap_or(0);
438 | path.truncate(index);
439 | }
440 | Component::Normal(normal) => {
441 | if normal.as_bytes().contains(&b'\0') {
442 | return Err(io::Error::from_raw_os_error(libc::EINVAL));
443 | }
444 |
445 | if !path.is_empty() {
446 | path.push(b'/');
447 | }
448 |
449 | path.extend_from_slice(normal.as_bytes());
450 | }
451 | }
452 | }
453 |
454 | if path.is_empty() {
455 | path.push(b'.');
456 | }
457 |
458 | path.push(0);
459 |
460 | // SAFETY: We've checked safety requirements of CString above.
461 | let owned = unsafe { CString::from_vec_with_nul_unchecked(path) };
462 | Ok(owned)
463 | }
464 |
--------------------------------------------------------------------------------
/relative-path-utils/src/root/windows.rs:
--------------------------------------------------------------------------------
1 | use core::mem::{self, align_of, size_of, MaybeUninit};
2 | use core::ptr;
3 | use core::slice;
4 |
5 | use alloc::vec::Vec;
6 |
7 | use std::ffi::OsString;
8 | use std::fs::File;
9 | use std::io;
10 | use std::os::windows::ffi::OsStringExt;
11 | use std::os::windows::fs::OpenOptionsExt;
12 | use std::os::windows::io::{AsHandle, AsRawHandle, FromRawHandle, OwnedHandle, RawHandle};
13 | use std::path::Path;
14 | use std::path::MAIN_SEPARATOR;
15 |
16 | use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES;
17 | use windows_sys::Wdk::Storage::FileSystem as nt;
18 | use windows_sys::Wdk::System::SystemServices::SL_RESTART_SCAN;
19 | use windows_sys::Win32::Foundation::FALSE;
20 | use windows_sys::Win32::Foundation::{
21 | RtlNtStatusToDosError, ERROR_INVALID_PARAMETER, HANDLE, STATUS_NO_MORE_FILES, STATUS_PENDING,
22 | STATUS_SUCCESS, UNICODE_STRING,
23 | };
24 | use windows_sys::Win32::Storage::FileSystem as c;
25 | use windows_sys::Win32::System::IO::IO_STATUS_BLOCK;
26 |
27 | use relative_path::{Component, RelativePath};
28 |
29 | #[allow(clippy::cast_possible_wrap)]
30 | const EINVAL: i32 = ERROR_INVALID_PARAMETER as i32;
31 | const MAIN_SEPARATOR_U16: u16 = MAIN_SEPARATOR as u16;
32 |
33 | #[derive(Debug)]
34 | pub(super) struct Root {
35 | handle: OwnedHandle,
36 | }
37 |
38 | impl Root {
39 | pub(super) fn new(path: &Path) -> io::Result {
40 | let file = std::fs::OpenOptions::new()
41 | .read(true)
42 | .attributes(c::FILE_FLAG_BACKUP_SEMANTICS)
43 | .open(path)?;
44 |
45 | Ok(Root {
46 | handle: OwnedHandle::from(file),
47 | })
48 | }
49 |
50 | pub(super) fn open_at(&self, path: &RelativePath, options: &OpenOptions) -> io::Result {
51 | let handle = self.open_at_inner(path, options)?;
52 | Ok(File::from(handle))
53 | }
54 |
55 | pub(super) fn open_at_inner(
56 | &self,
57 | path: &RelativePath,
58 | options: &OpenOptions,
59 | ) -> io::Result {
60 | let path = encode_path_wide(path)?;
61 |
62 | // SAFETY: All the operations and parameters are correctly used.
63 | unsafe {
64 | let object_name = unicode_string_ref(&path)?;
65 |
66 | let attributes = OBJECT_ATTRIBUTES {
67 | Length: len_of::(),
68 | RootDirectory: self.handle.as_raw_handle() as HANDLE,
69 | ObjectName: &object_name,
70 | Attributes: 0,
71 | SecurityDescriptor: ptr::null(),
72 | SecurityQualityOfService: ptr::null(),
73 | };
74 |
75 | let mut status_block = IO_STATUS_BLOCK {
76 | Anonymous: windows_sys::Win32::System::IO::IO_STATUS_BLOCK_0 {
77 | Status: STATUS_PENDING,
78 | },
79 | Information: 0,
80 | };
81 |
82 | let mut handle = MaybeUninit::zeroed();
83 |
84 | let status = nt::NtCreateFile(
85 | handle.as_mut_ptr(),
86 | c::SYNCHRONIZE | options.get_access_mode()?,
87 | &attributes,
88 | &mut status_block,
89 | ptr::null_mut(),
90 | 0,
91 | options.share_mode,
92 | options.get_creation_mode()?,
93 | nt::FILE_SYNCHRONOUS_IO_ALERT | options.custom_create_options,
94 | ptr::null(),
95 | 0,
96 | );
97 |
98 | if status != STATUS_SUCCESS {
99 | return Err(nt_error(status));
100 | }
101 |
102 | Ok(OwnedHandle::from_raw_handle(
103 | handle.assume_init() as RawHandle
104 | ))
105 | }
106 | }
107 |
108 | pub(super) fn metadata(&self, path: &RelativePath) -> io::Result {
109 | let owned;
110 |
111 | let handle = if is_current(path) {
112 | self.handle.as_handle()
113 | } else {
114 | let mut opts = OpenOptions::new();
115 | opts.read(true);
116 | opts.access_mode = Some(c::FILE_READ_ATTRIBUTES);
117 | // No read or write permissions are necessary
118 | // opts.access_mode = Some(0);
119 | // opts.custom_create_options = nt::FILE_OPEN_FOR_BACKUP_INTENT;
120 | owned = self.open_at_inner(path, &opts)?;
121 | owned.as_handle()
122 | };
123 |
124 | unsafe {
125 | let mut info = MaybeUninit::zeroed();
126 |
127 | if c::GetFileInformationByHandle(handle.as_raw_handle() as isize, info.as_mut_ptr())
128 | == FALSE
129 | {
130 | return Err(io::Error::last_os_error());
131 | }
132 |
133 | let info = info.assume_init();
134 |
135 | let mut reparse_tag = 0;
136 |
137 | if info.dwFileAttributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0 {
138 | let mut attr_tag = MaybeUninit::::zeroed();
139 |
140 | let result = c::GetFileInformationByHandleEx(
141 | self.handle.as_raw_handle() as isize,
142 | c::FileAttributeTagInfo,
143 | attr_tag.as_mut_ptr().cast(),
144 | mem::size_of::()
145 | .try_into()
146 | .unwrap(),
147 | );
148 |
149 | if result == FALSE {
150 | return Err(io::Error::last_os_error());
151 | }
152 |
153 | let attr_tag = attr_tag.assume_init();
154 |
155 | if attr_tag.FileAttributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0 {
156 | reparse_tag = attr_tag.ReparseTag;
157 | }
158 | }
159 |
160 | Ok(Metadata {
161 | attributes: info.dwFileAttributes,
162 | reparse_tag,
163 | })
164 | }
165 | }
166 |
167 | pub(super) fn read_dir(&self, path: &RelativePath) -> io::Result {
168 | let handle = if is_current(path) {
169 | self.handle.try_clone()?
170 | } else {
171 | let mut opts = OpenOptions::new();
172 | opts.access_mode = Some(c::FILE_LIST_DIRECTORY);
173 | self.open_at_inner(path, &opts)?
174 | };
175 |
176 | Ok(ReadDir {
177 | handle,
178 | buffer: AlignedBuf::new(),
179 | at: None,
180 | first: true,
181 | })
182 | }
183 | }
184 |
185 | #[repr(C)]
186 | struct AlignedBuf {
187 | _align: [T; 0],
188 | buf: [u8; N],
189 | }
190 |
191 | impl AlignedBuf {
192 | fn new() -> Self {
193 | Self {
194 | _align: [],
195 | buf: [0; N],
196 | }
197 | }
198 |
199 | #[allow(clippy::cast_possible_truncation, clippy::unused_self)]
200 | fn len(&self) -> u32 {
201 | N as u32
202 | }
203 |
204 | unsafe fn as_ptr_at(&self, at: usize) -> *const T {
205 | assert!(at % align_of::() == 0);
206 | self.buf.as_ptr().add(at).cast()
207 | }
208 |
209 | fn as_mut_ptr(&mut self) -> *mut u8 {
210 | self.buf.as_mut_ptr()
211 | }
212 | }
213 |
214 | pub(super) struct ReadDir {
215 | handle: OwnedHandle,
216 | buffer: AlignedBuf,
217 | first: bool,
218 | at: Option,
219 | }
220 |
221 | impl Iterator for ReadDir {
222 | type Item = io::Result;
223 |
224 | #[inline]
225 | fn next(&mut self) -> Option {
226 | loop {
227 | while let Some(at) = self.at.take() {
228 | unsafe {
229 | let file_names = &*self
230 | .buffer
231 | .as_ptr_at(at)
232 | .cast::();
233 |
234 | let len = file_names.FileNameLength;
235 | let ptr = file_names.FileName.as_ptr();
236 | let name = slice::from_raw_parts(ptr, len as usize / 2);
237 |
238 | if file_names.NextEntryOffset != 0 {
239 | self.at = Some(at + file_names.NextEntryOffset as usize);
240 | }
241 |
242 | if let Some(entry) = DirEntry::new(name) {
243 | return Some(Ok(entry));
244 | }
245 | }
246 | }
247 |
248 | unsafe {
249 | let mut status_block = IO_STATUS_BLOCK {
250 | Anonymous: windows_sys::Win32::System::IO::IO_STATUS_BLOCK_0 {
251 | Status: STATUS_PENDING,
252 | },
253 | Information: 0,
254 | };
255 |
256 | let status = nt::NtQueryDirectoryFileEx(
257 | self.handle.as_raw_handle() as HANDLE,
258 | 0,
259 | None,
260 | ptr::null(),
261 | &mut status_block,
262 | self.buffer.as_mut_ptr().cast(),
263 | self.buffer.len(),
264 | 12, // FileNamesInformation
265 | if mem::take(&mut self.first) {
266 | SL_RESTART_SCAN
267 | } else {
268 | 0
269 | },
270 | ptr::null(),
271 | );
272 |
273 | if status == STATUS_NO_MORE_FILES {
274 | return None;
275 | }
276 |
277 | if status != STATUS_SUCCESS {
278 | return Some(Err(nt_error(status)));
279 | }
280 |
281 | self.at = Some(0);
282 | }
283 | }
284 | }
285 | }
286 |
287 | pub(super) struct DirEntry {
288 | file_name: OsString,
289 | }
290 |
291 | impl DirEntry {
292 | fn new(data: &[u16]) -> Option {
293 | match data {
294 | // check for '.' and '..'
295 | [46] | [46, 46] => return None,
296 | _ => {}
297 | }
298 |
299 | Some(DirEntry {
300 | file_name: OsString::from_wide(data),
301 | })
302 | }
303 |
304 | pub(super) fn file_name(&self) -> OsString {
305 | self.file_name.clone()
306 | }
307 | }
308 |
309 | fn is_current(path: &RelativePath) -> bool {
310 | path.components().all(|c| c == Component::CurDir)
311 | }
312 |
313 | fn encode_path_wide(input: &RelativePath) -> io::Result> {
314 | let mut path = Vec::with_capacity(input.as_str().len() * 2);
315 |
316 | for c in input.components() {
317 | match c {
318 | Component::CurDir => {}
319 | Component::ParentDir => {
320 | if path.is_empty() {
321 | return Err(io::Error::from_raw_os_error(EINVAL));
322 | }
323 |
324 | let index = path
325 | .iter()
326 | .rposition(|window| *window == MAIN_SEPARATOR_U16)
327 | .unwrap_or(0);
328 |
329 | path.truncate(index);
330 | }
331 | Component::Normal(normal) => {
332 | if !path.is_empty() {
333 | path.extend_from_slice(MAIN_SEPARATOR.encode_utf16(&mut [0; 2]));
334 | }
335 |
336 | path.extend(normal.encode_utf16());
337 | }
338 | }
339 | }
340 |
341 | if path.is_empty() {
342 | path.extend_from_slice('.'.encode_utf16(&mut [0; 2]));
343 | }
344 |
345 | Ok(path)
346 | }
347 |
348 | unsafe impl Send for OpenOptions {}
349 | unsafe impl Sync for OpenOptions {}
350 |
351 | #[derive(Clone, Debug)]
352 | #[allow(clippy::struct_excessive_bools)]
353 | pub(super) struct OpenOptions {
354 | // generic
355 | read: bool,
356 | write: bool,
357 | append: bool,
358 | truncate: bool,
359 | create: bool,
360 | create_new: bool,
361 | // system-specific
362 | custom_create_options: u32,
363 | share_mode: u32,
364 | access_mode: Option,
365 | }
366 |
367 | impl OpenOptions {
368 | pub(super) fn new() -> OpenOptions {
369 | OpenOptions {
370 | // generic
371 | read: false,
372 | write: false,
373 | append: false,
374 | truncate: false,
375 | create: false,
376 | create_new: false,
377 | // system-specific
378 | custom_create_options: 0,
379 | share_mode: c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE,
380 | access_mode: None,
381 | }
382 | }
383 |
384 | pub(super) fn read(&mut self, read: bool) {
385 | self.read = read;
386 | }
387 |
388 | pub(super) fn write(&mut self, write: bool) {
389 | self.write = write;
390 | }
391 |
392 | pub(super) fn append(&mut self, append: bool) {
393 | self.append = append;
394 | }
395 |
396 | pub(super) fn truncate(&mut self, truncate: bool) {
397 | self.truncate = truncate;
398 | }
399 |
400 | pub(super) fn create(&mut self, create: bool) {
401 | self.create = create;
402 | }
403 |
404 | pub(super) fn create_new(&mut self, create_new: bool) {
405 | self.create_new = create_new;
406 | }
407 |
408 | fn get_access_mode(&self) -> io::Result {
409 | // NtCreateFile does not support `GENERIC_READ`.
410 | const DEFAULT_READ: u32 =
411 | c::STANDARD_RIGHTS_READ | c::FILE_READ_DATA | c::FILE_READ_EA | c::FILE_READ_ATTRIBUTES;
412 |
413 | const DEFAULT_WRITE: u32 = c::STANDARD_RIGHTS_WRITE
414 | | c::FILE_WRITE_DATA
415 | | c::FILE_WRITE_EA
416 | | c::FILE_WRITE_ATTRIBUTES;
417 |
418 | let access_mode = match (self.read, self.write, self.append, self.access_mode) {
419 | (_, _, _, Some(access_mode)) => access_mode,
420 | (true, false, false, _) => DEFAULT_READ,
421 | (false, true, false, _) => DEFAULT_WRITE,
422 | (true, true, false, _) => DEFAULT_READ | DEFAULT_WRITE,
423 | (false, _, true, _) => c::FILE_GENERIC_WRITE & !c::FILE_WRITE_DATA,
424 | (true, _, true, _) => DEFAULT_READ | (c::FILE_GENERIC_WRITE & !c::FILE_WRITE_DATA),
425 | (false, false, false, _) => {
426 | return Err(io::Error::from_raw_os_error(EINVAL));
427 | }
428 | };
429 |
430 | Ok(access_mode)
431 | }
432 |
433 | fn get_creation_mode(&self) -> io::Result {
434 | match (self.write, self.append) {
435 | (true, false) => {}
436 | (false, false) => {
437 | if self.truncate || self.create || self.create_new {
438 | return Err(io::Error::from_raw_os_error(EINVAL));
439 | }
440 | }
441 | (_, true) => {
442 | if self.truncate && !self.create_new {
443 | return Err(io::Error::from_raw_os_error(EINVAL));
444 | }
445 | }
446 | }
447 |
448 | Ok(match (self.create, self.truncate, self.create_new) {
449 | (false, false, false) => nt::FILE_OPEN,
450 | (true, false, false) => nt::FILE_OPEN_IF,
451 | (false, true, false) => nt::FILE_OVERWRITE,
452 | (true, true, false) => nt::FILE_OVERWRITE_IF,
453 | (_, _, true) => nt::FILE_CREATE,
454 | })
455 | }
456 | }
457 |
458 | #[derive(Clone)]
459 | pub(super) struct Metadata {
460 | attributes: u32,
461 | reparse_tag: u32,
462 | }
463 |
464 | impl Metadata {
465 | #[inline]
466 | pub(super) fn is_dir(&self) -> bool {
467 | !self.is_symlink() && self.is_directory()
468 | }
469 |
470 | #[inline]
471 | pub(super) fn is_file(&self) -> bool {
472 | !self.is_symlink() && !self.is_directory()
473 | }
474 |
475 | #[inline]
476 | pub(super) fn is_symlink(&self) -> bool {
477 | self.is_reparse_point() && self.is_reparse_tag_name_surrogate()
478 | }
479 |
480 | #[inline]
481 | #[allow(unused)]
482 | pub(super) fn is_symlink_dir(&self) -> bool {
483 | self.is_symlink() && self.is_directory()
484 | }
485 |
486 | #[inline]
487 | #[allow(unused)]
488 | pub(super) fn is_symlink_file(&self) -> bool {
489 | self.is_symlink() && !self.is_directory()
490 | }
491 |
492 | #[inline]
493 | fn is_directory(&self) -> bool {
494 | self.attributes & c::FILE_ATTRIBUTE_DIRECTORY != 0
495 | }
496 |
497 | #[inline]
498 | fn is_reparse_point(&self) -> bool {
499 | self.attributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0
500 | }
501 |
502 | #[inline]
503 | fn is_reparse_tag_name_surrogate(&self) -> bool {
504 | self.reparse_tag & 0x2000_0000 != 0
505 | }
506 | }
507 |
508 | #[allow(clippy::cast_possible_truncation)]
509 | unsafe fn len_of() -> u32 {
510 | size_of::() as u32
511 | }
512 |
513 | #[allow(clippy::cast_possible_wrap)]
514 | fn nt_error(status: i32) -> io::Error {
515 | io::Error::from_raw_os_error(unsafe { RtlNtStatusToDosError(status) } as i32)
516 | }
517 |
518 | fn unicode_string_ref(path: &[u16]) -> io::Result {
519 | let Ok(len) = u16::try_from(mem::size_of_val(path)) else {
520 | return Err(io::Error::from_raw_os_error(EINVAL));
521 | };
522 |
523 | Ok(UNICODE_STRING {
524 | Length: len,
525 | MaximumLength: len,
526 | Buffer: path.as_ptr().cast_mut(),
527 | })
528 | }
529 |
--------------------------------------------------------------------------------
/relative-path-utils/tests/root.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 | use std::path::{Path, PathBuf};
3 |
4 | use anyhow::Result;
5 | use relative_path_utils::Root;
6 |
7 | const PATH: &str = env!("CARGO_TARGET_TMPDIR");
8 |
9 | fn make_path(path: &'static str) -> PathBuf {
10 | Path::new(PATH).join(path)
11 | }
12 |
13 | fn root(path: &'static str) -> Root {
14 | match Root::new(make_path(path)) {
15 | Ok(root) => root,
16 | Err(error) => panic!("Failed to open root: {path}: {error}"),
17 | }
18 | }
19 |
20 | fn files(list: &[(&'static str, Option<&'static str>)]) {
21 | for (item, content) in list {
22 | let path = make_path(item);
23 |
24 | if let Some(content) = content {
25 | if let Some(parent) = path.parent() {
26 | if let Err(error) = fs::create_dir_all(parent) {
27 | panic!("Failed to create directory {}: {}", parent.display(), error);
28 | }
29 | }
30 |
31 | if let Err(error) = fs::write(&path, content) {
32 | panic!("Failed to create file {}: {}", path.display(), error);
33 | }
34 | } else if let Err(error) = fs::create_dir_all(&path) {
35 | panic!("Failed to create directory {}: {}", path.display(), error);
36 | }
37 | }
38 | }
39 |
40 | #[test]
41 | fn relative_open() -> Result<()> {
42 | files(&[("relative_open/src/root/first", Some("first content"))]);
43 |
44 | let r1 = root("relative_open/src/root");
45 | assert_eq!(r1.read_to_string("first")?, "first content");
46 |
47 | let r2 = root("relative_open/src");
48 | assert_eq!(r2.read_to_string("root/first")?, "first content");
49 | Ok(())
50 | }
51 |
52 | #[test]
53 | fn read_dir() -> Result<()> {
54 | files(&[("read_dir/src/root/first", Some("first content"))]);
55 | files(&[("read_dir/src/root/second", Some("second content"))]);
56 |
57 | let r1 = root(".");
58 | let d = r1.read_dir("read_dir/src/root")?;
59 |
60 | let mut values = Vec::new();
61 |
62 | for e in d {
63 | let e = e?;
64 | values.push(e.file_name().to_string_lossy().into_owned());
65 | }
66 |
67 | values.sort();
68 |
69 | assert_eq!(values, vec!["first", "second"]);
70 | Ok(())
71 | }
72 |
73 | #[test]
74 | fn glob() -> Result<()> {
75 | files(&[("glob/src/root/first", Some("first content"))]);
76 | files(&[("glob/src/root/second", Some("second content"))]);
77 |
78 | let r1 = root("glob");
79 | let glob = r1.glob("**/root/*");
80 |
81 | let mut results = Vec::new();
82 |
83 | for e in glob.matcher() {
84 | results.push(e?);
85 | }
86 |
87 | results.sort();
88 | assert_eq!(results, vec!["src/root/first", "src/root/second"]);
89 | Ok(())
90 | }
91 |
92 | #[test]
93 | fn read_root_dir() -> Result<()> {
94 | files(&[("read_root_dir/first", Some("first content"))]);
95 |
96 | let r1 = root("read_root_dir");
97 |
98 | let mut values = Vec::new();
99 |
100 | for e in r1.read_dir("")? {
101 | let e = e?;
102 | values.push(e.file_name().to_string_lossy().into_owned());
103 | }
104 |
105 | for e in r1.read_dir("")? {
106 | let e = e?;
107 | values.push(e.file_name().to_string_lossy().into_owned());
108 | }
109 |
110 | assert_eq!(values, vec!["first", "first"]);
111 | Ok(())
112 | }
113 |
114 | #[test]
115 | fn test_parent_dir() -> Result<()> {
116 | files(&[("test_parent_dir/foo/bar/first", Some("first content"))]);
117 | files(&[("test_parent_dir/foo/second", Some("second content"))]);
118 |
119 | let r1 = root("test_parent_dir");
120 | assert_eq!(r1.read_to_string("foo/bar/../second")?, "second content");
121 | assert!(r1.read_to_string("../second").is_err());
122 | Ok(())
123 | }
124 |
--------------------------------------------------------------------------------
/relative-path/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "relative-path"
3 | version = "2.0.1"
4 | authors = ["John-John Tedro "]
5 | edition = "2021"
6 | rust-version = "1.66"
7 | description = "Portable, relative paths for Rust."
8 | documentation = "https://docs.rs/relative-path"
9 | readme = "README.md"
10 | homepage = "https://github.com/udoprog/relative-path"
11 | repository = "https://github.com/udoprog/relative-path"
12 | license = "MIT OR Apache-2.0"
13 | keywords = ["path"]
14 | categories = ["filesystem"]
15 |
16 | [lints.rust]
17 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(relative_path_docsrs)'] }
18 |
19 | [features]
20 | default = ["std", "alloc"]
21 | alloc = ["serde?/alloc"]
22 | std = []
23 | serde = ["dep:serde" ]
24 |
25 | [dependencies]
26 | serde = { version = "1.0.160", optional = true, default-features = false }
27 |
28 | [dev-dependencies]
29 | anyhow = "1.0.76"
30 | foldhash = "0.1.5"
31 | serde = { version = "1.0.160", features = ["derive"] }
32 |
33 | [package.metadata.docs.rs]
34 | all-features = true
35 | rustdoc-args = ["--cfg", "relative_path_docsrs"]
36 |
--------------------------------------------------------------------------------
/relative-path/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/relative-path/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/relative-path/README.md:
--------------------------------------------------------------------------------
1 | # relative-path
2 |
3 | [
](https://github.com/udoprog/relative-path)
4 | [
](https://crates.io/crates/relative-path)
5 | [
](https://docs.rs/relative-path)
6 | [
](https://github.com/udoprog/relative-path/actions?query=branch%3Amain)
7 |
8 | Portable relative UTF-8 paths for Rust.
9 |
10 | This crate provides a module analogous to [`std::path`], with the following
11 | characteristics:
12 |
13 | * The path separator is set to a fixed character (`/`), regardless of
14 | platform.
15 | * Relative paths cannot represent a path in the filesystem without first
16 | specifying *what they are relative to* using functions such as [`to_path`]
17 | and [`to_logical_path`].
18 | * Relative paths are always guaranteed to be valid UTF-8 strings.
19 |
20 | On top of this we support many operations that guarantee the same behavior
21 | across platforms.
22 |
23 | For more utilities to manipulate relative paths, see the
24 | [`relative-path-utils` crate].
25 |
26 |
27 |
28 | ## Usage
29 |
30 | Add `relative-path` to your `Cargo.toml`:
31 |
32 | ```toml
33 | relative-path = "2.0.1"
34 | ```
35 |
36 | Start using relative paths:
37 |
38 | ```rust
39 | use serde::{Serialize, Deserialize};
40 | use relative_path::RelativePath;
41 |
42 | #[derive(Serialize, Deserialize)]
43 | struct Manifest<'a> {
44 | #[serde(borrow)]
45 | source: &'a RelativePath,
46 | }
47 |
48 | ```
49 |
50 |
51 |
52 | ## Serde Support
53 |
54 | This library includes serde support that can be enabled with the `serde`
55 | feature.
56 |
57 |
58 |
59 | ## Why is `std::path` a portability hazard?
60 |
61 | Path representations differ across platforms.
62 |
63 | * Windows permits using drive volumes (multiple roots) as a prefix (e.g.
64 | `"c:\"`) and backslash (`\`) as a separator.
65 | * Unix references absolute paths from a single root and uses forward slash
66 | (`/`) as a separator.
67 |
68 | If we use `PathBuf`, Storing paths in a manifest would allow our application
69 | to build and run on one platform but potentially not others.
70 |
71 | Consider the following data model and corresponding toml for a manifest:
72 |
73 | ```rust
74 | use std::path::PathBuf;
75 |
76 | use serde::{Serialize, Deserialize};
77 |
78 | #[derive(Serialize, Deserialize)]
79 | struct Manifest {
80 | source: PathBuf,
81 | }
82 | ```
83 |
84 | ```toml
85 | source = "C:\\Users\\udoprog\\repo\\data\\source"
86 | ```
87 |
88 | This will run for you (assuming `source` exists). So you go ahead and check
89 | the manifest into git. The next day your Linux colleague calls you and
90 | wonders what they have ever done to wrong you?
91 |
92 | So what went wrong? Well two things. You forgot to make the `source`
93 | relative, so anyone at the company which has a different username than you
94 | won't be able to use it. So you go ahead and fix that:
95 |
96 | ```toml
97 | source = "data\\source"
98 | ```
99 |
100 | But there is still one problem! A backslash (`\`) is only a legal path
101 | separator on Windows. Luckily you learn that forward slashes are supported
102 | both on Windows *and* Linux. So you opt for:
103 |
104 | ```toml
105 | source = "data/source"
106 | ```
107 |
108 | Things are working now. So all is well... Right? Sure, but we can do better.
109 |
110 | This crate provides types that work with *portable relative paths* (hence
111 | the name). So by using [`RelativePath`] we can systematically help avoid
112 | portability issues like the one above. Avoiding issues at the source is
113 | preferably over spending 5 minutes of onboarding time on a theoretical
114 | problem, hoping that your new hires will remember what to do if they ever
115 | encounter it.
116 |
117 | Using [`RelativePathBuf`] we can fix our data model like this:
118 |
119 | ```rust
120 | use relative_path::RelativePathBuf;
121 | use serde::{Serialize, Deserialize};
122 |
123 | #[derive(Serialize, Deserialize)]
124 | pub struct Manifest {
125 | source: RelativePathBuf,
126 | }
127 | ```
128 |
129 | And where it's used:
130 |
131 | ```rust
132 | use std::fs;
133 | use std::env::current_dir;
134 |
135 | let manifest: Manifest = todo!();
136 |
137 | let root = current_dir()?;
138 | let source = manifest.source.to_path(&root);
139 | let content = fs::read(&source)?;
140 | ```
141 |
142 |
143 |
144 | ## Overview
145 |
146 | Conversion to a platform-specific [`Path`] happens through the [`to_path`]
147 | and [`to_logical_path`] functions. Where you are required to specify the
148 | path that prefixes the relative path. This can come from a function such as
149 | [`std::env::current_dir`].
150 |
151 | ```rust
152 | use std::env::current_dir;
153 | use std::path::Path;
154 |
155 | use relative_path::RelativePath;
156 |
157 | let root = current_dir()?;
158 |
159 | // to_path unconditionally concatenates a relative path with its base:
160 | let relative_path = RelativePath::new("../foo/./bar");
161 | let full_path = relative_path.to_path(&root);
162 | assert_eq!(full_path, root.join("..\\foo\\.\\bar"));
163 |
164 | // to_logical_path tries to apply the logical operations that the relative
165 | // path corresponds to:
166 | let relative_path = RelativePath::new("../foo/./bar");
167 | let full_path = relative_path.to_logical_path(&root);
168 |
169 | // Replicate the operation performed by `to_logical_path`.
170 | let mut parent = root.clone();
171 | parent.pop();
172 | assert_eq!(full_path, parent.join("foo\\bar"));
173 | ```
174 |
175 | When two relative paths are compared to each other, their exact component
176 | makeup determines equality.
177 |
178 | ```rust
179 | use relative_path::RelativePath;
180 |
181 | assert_ne!(
182 | RelativePath::new("foo/bar/../baz"),
183 | RelativePath::new("foo/baz")
184 | );
185 | ```
186 |
187 | Using platform-specific path separators to construct relative paths is not
188 | supported.
189 |
190 | Path separators from other platforms are simply treated as part of a
191 | component:
192 |
193 | ```rust
194 | use relative_path::RelativePath;
195 |
196 | assert_ne!(
197 | RelativePath::new("foo/bar"),
198 | RelativePath::new("foo\\bar")
199 | );
200 |
201 | assert_eq!(1, RelativePath::new("foo\\bar").components().count());
202 | assert_eq!(2, RelativePath::new("foo/bar").components().count());
203 | ```
204 |
205 | To see if two relative paths are equivalent you can use [`normalize`]:
206 |
207 | ```rust
208 | use relative_path::RelativePath;
209 |
210 | assert_eq!(
211 | RelativePath::new("foo/bar/../baz").normalize(),
212 | RelativePath::new("foo/baz").normalize(),
213 | );
214 | ```
215 |
216 |
217 |
218 | ## Additional portability notes
219 |
220 | While relative paths avoid the most egregious portability issue, that
221 | absolute paths will work equally unwell on all platforms. We cannot avoid
222 | all. This section tries to document additional portability hazards that we
223 | are aware of.
224 |
225 | [`RelativePath`], similarly to [`Path`], makes no guarantees that its
226 | constituent components make up legal file names. While components are
227 | strictly separated by slashes, we can still store things in them which may
228 | not be used as legal paths on all platforms.
229 |
230 | * A `NUL` character is not permitted on unix platforms - this is a
231 | terminator in C-based filesystem APIs. Slash (`/`) is also used as a path
232 | separator.
233 | * Windows has a number of [reserved characters and names][windows-reserved]
234 | (like `CON`, `PRN`, and `AUX`) which cannot legally be part of a
235 | filesystem component.
236 | * Windows paths are [case-insensitive by default][windows-case]. So,
237 | `Foo.txt` and `foo.txt` are the same files on windows. But they are
238 | considered different paths on most unix systems.
239 |
240 | A relative path that *accidentally* contains a platform-specific components
241 | will largely result in a nonsensical paths being generated in the hope that
242 | they will fail fast during development and testing.
243 |
244 | ```rust
245 | use relative_path::{RelativePath, PathExt};
246 | use std::path::Path;
247 |
248 | if cfg!(windows) {
249 | assert_eq!(
250 | Path::new("foo\\c:\\bar\\baz"),
251 | RelativePath::new("c:\\bar\\baz").to_path("foo")
252 | );
253 | }
254 |
255 | if cfg!(unix) {
256 | assert_eq!(
257 | Path::new("foo/bar/baz"),
258 | RelativePath::new("/bar/baz").to_path("foo")
259 | );
260 | }
261 |
262 | assert_eq!(
263 | Path::new("foo").relative_to("bar")?,
264 | RelativePath::new("../foo"),
265 | );
266 | ```
267 |
268 | [`None`]: https://doc.rust-lang.org/std/option/enum.Option.html
269 | [`normalize`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html#method.normalize
270 | [`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html
271 | [`RelativePath`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html
272 | [`RelativePathBuf`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePathBuf.html
273 | [`std::env::current_dir`]: https://doc.rust-lang.org/std/env/fn.current_dir.html
274 | [`std::path`]: https://doc.rust-lang.org/std/path/index.html
275 | [`to_logical_path`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html#method.to_logical_path
276 | [`to_path`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html#method.to_path
277 | [windows-reserved]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
278 | [windows-case]: https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity
279 | [`relative-path-utils` crate]: https://docs.rs/relative-path-utils
280 |
--------------------------------------------------------------------------------
/relative-path/src/path_ext.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2012-2015 The Rust Project Developers. See the COPYRIGHT
2 | // file at the top-level directory of this distribution and at
3 | // http://rust-lang.org/COPYRIGHT.
4 | //
5 | // Licensed under the Apache License, Version 2.0 or the MIT license
7 | // , at your
8 | // option. This file may not be copied, modified, or distributed
9 | // except according to those terms.
10 |
11 | // Ported from the pathdiff crate, which adapted the original rustc's
12 | // path_relative_from
13 | // https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs
14 | // https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158
15 |
16 | use core::fmt;
17 | use std::error;
18 | use std::path::{Path, PathBuf};
19 | use std::prelude::v1::*;
20 |
21 | use crate::{Component, RelativePathBuf};
22 |
23 | // Prevent downstream implementations, so methods may be added without backwards
24 | // breaking changes.
25 | mod sealed {
26 | use std::path::{Path, PathBuf};
27 |
28 | pub trait Sealed {}
29 |
30 | impl Sealed for Path {}
31 | impl Sealed for PathBuf {}
32 | }
33 |
34 | /// An error raised when attempting to convert a path using
35 | /// [`PathExt::relative_to`].
36 | #[derive(Debug, Clone, PartialEq, Eq)]
37 | pub struct RelativeToError {
38 | kind: RelativeToErrorKind,
39 | }
40 |
41 | /// Error kind for [`RelativeToError`].
42 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
43 | #[non_exhaustive]
44 | enum RelativeToErrorKind {
45 | /// Non-utf8 component in path.
46 | NonUtf8,
47 | /// Mismatching path prefixes.
48 | PrefixMismatch,
49 | /// A provided path is ambiguous, in that there is no way to determine which
50 | /// components should be added from one path to the other to traverse it.
51 | ///
52 | /// For example, `.` is ambiguous relative to `../..` because we don't know
53 | /// the names of the components being traversed.
54 | AmbiguousTraversal,
55 | /// This is a catch-all error since we don't control the `std::path` API a
56 | /// Components iterator might decide (intentionally or not) to produce
57 | /// components which violates its own contract.
58 | ///
59 | /// In particular we rely on only relative components being produced after
60 | /// the absolute prefix has been consumed.
61 | IllegalComponent,
62 | }
63 |
64 | impl fmt::Display for RelativeToError {
65 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
66 | match self.kind {
67 | RelativeToErrorKind::NonUtf8 => "path contains non-utf8 component".fmt(fmt),
68 | RelativeToErrorKind::PrefixMismatch => {
69 | "paths contain different absolute prefixes".fmt(fmt)
70 | }
71 | RelativeToErrorKind::AmbiguousTraversal => {
72 | "path traversal cannot be determined".fmt(fmt)
73 | }
74 | RelativeToErrorKind::IllegalComponent => "path contains illegal components".fmt(fmt),
75 | }
76 | }
77 | }
78 |
79 | impl error::Error for RelativeToError {}
80 |
81 | impl From for RelativeToError {
82 | #[inline]
83 | fn from(kind: RelativeToErrorKind) -> Self {
84 | Self { kind }
85 | }
86 | }
87 |
88 | /// Extension methods for [`Path`] and [`PathBuf`] to for building and
89 | /// interacting with [`RelativePath`].
90 | ///
91 | /// [`RelativePath`]: crate::RelativePath
92 | pub trait PathExt: sealed::Sealed {
93 | /// Build a relative path from the provided directory to `self`.
94 | ///
95 | /// Producing a relative path like this is a logical operation and does not
96 | /// guarantee that the constructed path corresponds to what the filesystem
97 | /// would do. On Linux for example symbolic links could mean that the
98 | /// logical path doesn't correspond to the filesystem path.
99 | ///
100 | /// # Examples
101 | ///
102 | /// ```
103 | /// use std::path::Path;
104 | /// use relative_path::{RelativePath, PathExt};
105 | ///
106 | /// let baz = Path::new("/foo/bar/baz");
107 | /// let bar = Path::new("/foo/bar");
108 | /// let qux = Path::new("/foo/bar/qux");
109 | ///
110 | /// assert_eq!(bar.relative_to(baz)?, RelativePath::new("../"));
111 | /// assert_eq!(baz.relative_to(bar)?, RelativePath::new("baz"));
112 | /// assert_eq!(qux.relative_to(baz)?, RelativePath::new("../qux"));
113 | /// assert_eq!(baz.relative_to(qux)?, RelativePath::new("../baz"));
114 | /// assert_eq!(bar.relative_to(qux)?, RelativePath::new("../"));
115 | /// # Ok::<_, relative_path::RelativeToError>(())
116 | /// ```
117 | ///
118 | /// # Errors
119 | ///
120 | /// Errors in case the provided path contains components which cannot be
121 | /// converted into a relative path as needed, such as non-utf8 data.
122 | fn relative_to(&self, root: P) -> Result
123 | where
124 | P: AsRef;
125 | }
126 |
127 | impl PathExt for Path {
128 | fn relative_to(&self, root: P) -> Result
129 | where
130 | P: AsRef,
131 | {
132 | use std::path::Component::{CurDir, Normal, ParentDir, Prefix, RootDir};
133 |
134 | // Helper function to convert from a std::path::Component to a
135 | // relative_path::Component.
136 | fn std_to_c(c: std::path::Component<'_>) -> Result, RelativeToError> {
137 | Ok(match c {
138 | CurDir => Component::CurDir,
139 | ParentDir => Component::ParentDir,
140 | Normal(n) => Component::Normal(n.to_str().ok_or(RelativeToErrorKind::NonUtf8)?),
141 | _ => return Err(RelativeToErrorKind::IllegalComponent.into()),
142 | })
143 | }
144 |
145 | let root = root.as_ref();
146 | let mut a_it = self.components();
147 | let mut b_it = root.components();
148 |
149 | // Ensure that the two paths are both either relative, or have the same
150 | // prefix. Strips any common prefix the two paths do have. Prefixes are
151 | // platform dependent, but different prefixes would for example indicate
152 | // paths for different drives on Windows.
153 | let (a_head, b_head) = loop {
154 | match (a_it.next(), b_it.next()) {
155 | (Some(RootDir), Some(RootDir)) => (),
156 | (Some(Prefix(a)), Some(Prefix(b))) if a == b => (),
157 | (Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => {
158 | return Err(RelativeToErrorKind::PrefixMismatch.into());
159 | }
160 | (None, None) => break (None, None),
161 | (a, b) if a != b => break (a, b),
162 | _ => (),
163 | }
164 | };
165 |
166 | let mut a_it = a_head.into_iter().chain(a_it);
167 | let mut b_it = b_head.into_iter().chain(b_it);
168 | let mut buf = RelativePathBuf::new();
169 |
170 | loop {
171 | let a = if let Some(a) = a_it.next() {
172 | a
173 | } else {
174 | for _ in b_it {
175 | buf.push(Component::ParentDir);
176 | }
177 |
178 | break;
179 | };
180 |
181 | match b_it.next() {
182 | Some(CurDir) => buf.push(std_to_c(a)?),
183 | Some(ParentDir) => {
184 | return Err(RelativeToErrorKind::AmbiguousTraversal.into());
185 | }
186 | root => {
187 | if root.is_some() {
188 | buf.push(Component::ParentDir);
189 | }
190 |
191 | for comp in b_it {
192 | match comp {
193 | ParentDir => {
194 | if !buf.pop() {
195 | return Err(RelativeToErrorKind::AmbiguousTraversal.into());
196 | }
197 | }
198 | CurDir => (),
199 | _ => buf.push(Component::ParentDir),
200 | }
201 | }
202 |
203 | buf.push(std_to_c(a)?);
204 |
205 | for c in a_it {
206 | buf.push(std_to_c(c)?);
207 | }
208 |
209 | break;
210 | }
211 | }
212 | }
213 |
214 | Ok(buf)
215 | }
216 | }
217 |
218 | impl PathExt for PathBuf {
219 | #[inline]
220 | fn relative_to(&self, root: P) -> Result
221 | where
222 | P: AsRef,
223 | {
224 | self.as_path().relative_to(root)
225 | }
226 | }
227 |
228 | #[cfg(test)]
229 | mod tests {
230 | use std::path::Path;
231 |
232 | use super::{PathExt, RelativeToErrorKind};
233 | use crate::{RelativePathBuf, RelativeToError};
234 |
235 | macro_rules! assert_relative_to {
236 | ($path:expr, $base:expr, Ok($expected:expr) $(,)?) => {
237 | assert_eq!(
238 | Path::new($path).relative_to($base),
239 | Ok(RelativePathBuf::from($expected))
240 | );
241 | };
242 |
243 | ($path:expr, $base:expr, Err($expected:ident) $(,)?) => {
244 | assert_eq!(
245 | Path::new($path).relative_to($base),
246 | Err(RelativeToError::from(RelativeToErrorKind::$expected))
247 | );
248 | };
249 | }
250 |
251 | #[cfg(windows)]
252 | macro_rules! abs {
253 | ($path:expr) => {
254 | Path::new(concat!("C:\\", $path))
255 | };
256 | }
257 |
258 | #[cfg(not(windows))]
259 | macro_rules! abs {
260 | ($path:expr) => {
261 | Path::new(concat!("/", $path))
262 | };
263 | }
264 |
265 | #[test]
266 | #[cfg(windows)]
267 | fn test_different_prefixes() {
268 | assert_relative_to!("C:\\repo", "D:\\repo", Err(PrefixMismatch),);
269 | assert_relative_to!("C:\\repo", "C:\\repo", Ok(""));
270 | assert_relative_to!(
271 | "\\\\server\\share\\repo",
272 | "\\\\server2\\share\\repo",
273 | Err(PrefixMismatch),
274 | );
275 | }
276 |
277 | #[test]
278 | fn test_absolute() {
279 | assert_relative_to!(abs!("foo"), abs!("bar"), Ok("../foo"));
280 | assert_relative_to!("foo", "bar", Ok("../foo"));
281 | assert_relative_to!(abs!("foo"), "bar", Err(PrefixMismatch));
282 | assert_relative_to!("foo", abs!("bar"), Err(PrefixMismatch));
283 | }
284 |
285 | #[test]
286 | fn test_identity() {
287 | assert_relative_to!(".", ".", Ok(""));
288 | assert_relative_to!("../foo", "../foo", Ok(""));
289 | assert_relative_to!("./foo", "./foo", Ok(""));
290 | assert_relative_to!("/foo", "/foo", Ok(""));
291 | assert_relative_to!("foo", "foo", Ok(""));
292 |
293 | assert_relative_to!("../foo/bar/baz", "../foo/bar/baz", Ok(""));
294 | assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok(""));
295 | }
296 |
297 | #[test]
298 | fn test_subset() {
299 | assert_relative_to!("foo", "fo", Ok("../foo"));
300 | assert_relative_to!("fo", "foo", Ok("../fo"));
301 | }
302 |
303 | #[test]
304 | fn test_empty() {
305 | assert_relative_to!("", "", Ok(""));
306 | assert_relative_to!("foo", "", Ok("foo"));
307 | assert_relative_to!("", "foo", Ok(".."));
308 | }
309 |
310 | #[test]
311 | fn test_relative() {
312 | assert_relative_to!("../foo", "../bar", Ok("../foo"));
313 | assert_relative_to!("../foo", "../foo/bar/baz", Ok("../.."));
314 | assert_relative_to!("../foo/bar/baz", "../foo", Ok("bar/baz"));
315 |
316 | assert_relative_to!("foo/bar/baz", "foo", Ok("bar/baz"));
317 | assert_relative_to!("foo/bar/baz", "foo/bar", Ok("baz"));
318 | assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok(""));
319 | assert_relative_to!("foo/bar/baz", "foo/bar/baz/", Ok(""));
320 |
321 | assert_relative_to!("foo/bar/baz/", "foo", Ok("bar/baz"));
322 | assert_relative_to!("foo/bar/baz/", "foo/bar", Ok("baz"));
323 | assert_relative_to!("foo/bar/baz/", "foo/bar/baz", Ok(""));
324 | assert_relative_to!("foo/bar/baz/", "foo/bar/baz/", Ok(""));
325 |
326 | assert_relative_to!("foo/bar/baz", "foo/", Ok("bar/baz"));
327 | assert_relative_to!("foo/bar/baz", "foo/bar/", Ok("baz"));
328 | assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok(""));
329 | }
330 |
331 | #[test]
332 | fn test_current_directory() {
333 | assert_relative_to!(".", "foo", Ok("../."));
334 | assert_relative_to!("foo", ".", Ok("foo"));
335 | assert_relative_to!("/foo", "/.", Ok("foo"));
336 | }
337 |
338 | #[test]
339 | fn assert_does_not_skip_parents() {
340 | assert_relative_to!("some/path", "some/foo/baz/path", Ok("../../../path"));
341 | assert_relative_to!("some/path", "some/foo/bar/../baz/path", Ok("../../../path"));
342 | }
343 |
344 | #[test]
345 | fn test_ambiguous_paths() {
346 | // Parent directory name is unknown, so trying to make current directory
347 | // relative to it is impossible.
348 | assert_relative_to!(".", "../..", Err(AmbiguousTraversal));
349 | assert_relative_to!(".", "a/../..", Err(AmbiguousTraversal));
350 | // Common prefixes are ok.
351 | assert_relative_to!("../a/..", "../a/../b", Ok(".."));
352 | assert_relative_to!("../a/../b", "../a/..", Ok("b"));
353 | }
354 | }
355 |
--------------------------------------------------------------------------------
/relative-path/src/relative_path_buf.rs:
--------------------------------------------------------------------------------
1 | use core::cmp;
2 | use core::fmt;
3 | use core::hash::{Hash, Hasher};
4 | use core::iter::FromIterator;
5 | use core::ops;
6 | use core::str;
7 |
8 | use alloc::borrow::{Borrow, Cow, ToOwned};
9 | use alloc::boxed::Box;
10 | use alloc::string::String;
11 |
12 | #[cfg(feature = "std")]
13 | use std::path;
14 |
15 | use super::{Component, RelativePath, PARENT_STR, SEP, STEM_SEP};
16 |
17 | #[cfg(feature = "std")]
18 | use super::{FromPathError, FromPathErrorKind};
19 |
20 | /// Traverse the given components and apply to the provided stack.
21 | ///
22 | /// This takes '.', and '..' into account. Where '.' doesn't change the stack, and '..' pops the
23 | /// last item or further adds parent components.
24 | #[inline(always)]
25 | pub(super) fn relative_traversal<'a, C>(buf: &mut RelativePathBuf, components: C)
26 | where
27 | C: IntoIterator- >,
28 | {
29 | use self::Component::{CurDir, Normal, ParentDir};
30 |
31 | for c in components {
32 | match c {
33 | CurDir => (),
34 | ParentDir => match buf.components().next_back() {
35 | Some(Component::ParentDir) | None => {
36 | buf.push(PARENT_STR);
37 | }
38 | _ => {
39 | buf.pop();
40 | }
41 | },
42 | Normal(name) => {
43 | buf.push(name);
44 | }
45 | }
46 | }
47 | }
48 |
49 | /// An owned, mutable relative path.
50 | ///
51 | /// This type provides methods to manipulate relative path objects.
52 | #[derive(Clone)]
53 | pub struct RelativePathBuf {
54 | inner: String,
55 | }
56 |
57 | impl RelativePathBuf {
58 | /// Create a new relative path buffer.
59 | #[must_use]
60 | #[inline]
61 | pub fn new() -> RelativePathBuf {
62 | RelativePathBuf {
63 | inner: String::new(),
64 | }
65 | }
66 |
67 | /// Internal constructor to allocate a relative path buf with the given capacity.
68 | #[inline]
69 | pub(super) fn with_capacity(cap: usize) -> RelativePathBuf {
70 | RelativePathBuf {
71 | inner: String::with_capacity(cap),
72 | }
73 | }
74 |
75 | /// Get the length of the underlying string in bytes.
76 | #[inline]
77 | pub(super) fn len(&self) -> usize {
78 | self.inner.len()
79 | }
80 |
81 | /// Try to convert a [`Path`] to a [`RelativePathBuf`].
82 | ///
83 | /// [`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html
84 | ///
85 | /// # Examples
86 | ///
87 | /// ```
88 | /// use relative_path::{RelativePath, RelativePathBuf, FromPathErrorKind};
89 | /// use std::path::Path;
90 | ///
91 | /// assert_eq!(
92 | /// Ok(RelativePath::new("foo/bar").to_owned()),
93 | /// RelativePathBuf::from_path(Path::new("foo/bar"))
94 | /// );
95 | /// ```
96 | ///
97 | /// # Errors
98 | ///
99 | /// This will error in case the provided path is not a relative path, which
100 | /// is identifier by it having a [`Prefix`] or [`RootDir`] component.
101 | ///
102 | /// [`Prefix`]: std::path::Component::Prefix
103 | /// [`RootDir`]: std::path::Component::RootDir
104 | #[cfg(feature = "std")]
105 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "std")))]
106 | #[inline]
107 | pub fn from_path
(path: P) -> Result
108 | where
109 | P: AsRef,
110 | {
111 | use std::path::Component::{CurDir, Normal, ParentDir, Prefix, RootDir};
112 |
113 | let mut buffer = RelativePathBuf::new();
114 |
115 | for c in path.as_ref().components() {
116 | match c {
117 | Prefix(_) | RootDir => return Err(FromPathErrorKind::NonRelative.into()),
118 | CurDir => continue,
119 | ParentDir => buffer.push(PARENT_STR),
120 | Normal(s) => buffer.push(s.to_str().ok_or(FromPathErrorKind::NonUtf8)?),
121 | }
122 | }
123 |
124 | Ok(buffer)
125 | }
126 |
127 | /// Extends `self` with `path`.
128 | ///
129 | /// # Examples
130 | ///
131 | /// ```
132 | /// use relative_path::RelativePathBuf;
133 | ///
134 | /// let mut path = RelativePathBuf::new();
135 | /// path.push("foo");
136 | /// path.push("bar");
137 | ///
138 | /// assert_eq!("foo/bar", path);
139 | ///
140 | /// let mut path = RelativePathBuf::new();
141 | /// path.push("foo");
142 | /// path.push("/bar");
143 | ///
144 | /// assert_eq!("foo/bar", path);
145 | /// ```
146 | pub fn push(&mut self, path: P)
147 | where
148 | P: AsRef,
149 | {
150 | let other = path.as_ref();
151 |
152 | let other = if other.starts_with_sep() {
153 | &other.inner[1..]
154 | } else {
155 | &other.inner[..]
156 | };
157 |
158 | if !self.inner.is_empty() && !self.ends_with_sep() {
159 | self.inner.push(SEP);
160 | }
161 |
162 | self.inner.push_str(other);
163 | }
164 |
165 | /// Updates [`file_name`] to `file_name`.
166 | ///
167 | /// If [`file_name`] was [`None`], this is equivalent to pushing
168 | /// `file_name`.
169 | ///
170 | /// Otherwise it is equivalent to calling [`pop`] and then pushing
171 | /// `file_name`. The new path will be a sibling of the original path. (That
172 | /// is, it will have the same parent.)
173 | ///
174 | /// [`file_name`]: RelativePath::file_name
175 | /// [`pop`]: RelativePathBuf::pop
176 | /// [`None`]: https://doc.rust-lang.org/std/option/enum.Option.html
177 | ///
178 | /// # Examples
179 | ///
180 | /// ```
181 | /// use relative_path::RelativePathBuf;
182 | ///
183 | /// let mut buf = RelativePathBuf::from("");
184 | /// assert!(buf.file_name() == None);
185 | /// buf.set_file_name("bar");
186 | /// assert_eq!(RelativePathBuf::from("bar"), buf);
187 | ///
188 | /// assert!(buf.file_name().is_some());
189 | /// buf.set_file_name("baz.txt");
190 | /// assert_eq!(RelativePathBuf::from("baz.txt"), buf);
191 | ///
192 | /// buf.push("bar");
193 | /// assert!(buf.file_name().is_some());
194 | /// buf.set_file_name("bar.txt");
195 | /// assert_eq!(RelativePathBuf::from("baz.txt/bar.txt"), buf);
196 | /// ```
197 | pub fn set_file_name(&mut self, file_name: S)
198 | where
199 | S: AsRef,
200 | {
201 | if self.file_name().is_some() {
202 | let popped = self.pop();
203 | debug_assert!(popped);
204 | }
205 |
206 | self.push(file_name.as_ref());
207 | }
208 |
209 | /// Updates [`extension`] to `extension`.
210 | ///
211 | /// Returns `false` and does nothing if
212 | /// [`file_name`][RelativePath::file_name] is [`None`], returns `true` and
213 | /// updates the extension otherwise.
214 | ///
215 | /// If [`extension`] is [`None`], the extension is added; otherwise it is
216 | /// replaced.
217 | ///
218 | /// [`extension`]: RelativePath::extension
219 | ///
220 | /// # Examples
221 | ///
222 | /// ```
223 | /// use relative_path::{RelativePath, RelativePathBuf};
224 | ///
225 | /// let mut p = RelativePathBuf::from("feel/the");
226 | ///
227 | /// p.set_extension("force");
228 | /// assert_eq!(RelativePath::new("feel/the.force"), p);
229 | ///
230 | /// p.set_extension("dark_side");
231 | /// assert_eq!(RelativePath::new("feel/the.dark_side"), p);
232 | ///
233 | /// assert!(p.pop());
234 | /// p.set_extension("nothing");
235 | /// assert_eq!(RelativePath::new("feel.nothing"), p);
236 | /// ```
237 | pub fn set_extension(&mut self, extension: S) -> bool
238 | where
239 | S: AsRef,
240 | {
241 | let file_stem = match self.file_stem() {
242 | Some(stem) => stem,
243 | None => return false,
244 | };
245 |
246 | let end_file_stem = file_stem[file_stem.len()..].as_ptr() as usize;
247 | let start = self.inner.as_ptr() as usize;
248 | self.inner.truncate(end_file_stem.wrapping_sub(start));
249 |
250 | let extension = extension.as_ref();
251 |
252 | if !extension.is_empty() {
253 | self.inner.push(STEM_SEP);
254 | self.inner.push_str(extension);
255 | }
256 |
257 | true
258 | }
259 |
260 | /// Truncates `self` to [`parent`][RelativePath::parent].
261 | ///
262 | /// # Examples
263 | ///
264 | /// ```
265 | /// use relative_path::{RelativePath, RelativePathBuf};
266 | ///
267 | /// let mut p = RelativePathBuf::from("test/test.rs");
268 | ///
269 | /// assert_eq!(true, p.pop());
270 | /// assert_eq!(RelativePath::new("test"), p);
271 | /// assert_eq!(true, p.pop());
272 | /// assert_eq!(RelativePath::new(""), p);
273 | /// assert_eq!(false, p.pop());
274 | /// assert_eq!(RelativePath::new(""), p);
275 | /// ```
276 | pub fn pop(&mut self) -> bool {
277 | match self.parent().map(|p| p.inner.len()) {
278 | Some(len) => {
279 | self.inner.truncate(len);
280 | true
281 | }
282 | None => false,
283 | }
284 | }
285 |
286 | /// Coerce to a [`RelativePath`] slice.
287 | #[must_use]
288 | #[inline]
289 | pub fn as_relative_path(&self) -> &RelativePath {
290 | self
291 | }
292 |
293 | /// Consumes the `RelativePathBuf`, yielding its internal [`String`] storage.
294 | ///
295 | /// # Examples
296 | ///
297 | /// ```
298 | /// use relative_path::RelativePathBuf;
299 | ///
300 | /// let p = RelativePathBuf::from("/the/head");
301 | /// let string = p.into_string();
302 | /// assert_eq!(string, "/the/head".to_owned());
303 | /// ```
304 | #[must_use]
305 | #[inline]
306 | pub fn into_string(self) -> String {
307 | self.inner
308 | }
309 |
310 | /// Converts this `RelativePathBuf` into a [boxed][alloc::boxed::Box]
311 | /// [`RelativePath`].
312 | #[must_use]
313 | #[inline]
314 | pub fn into_boxed_relative_path(self) -> Box {
315 | let rw = Box::into_raw(self.inner.into_boxed_str()) as *mut RelativePath;
316 | unsafe { Box::from_raw(rw) }
317 | }
318 | }
319 |
320 | impl Default for RelativePathBuf {
321 | #[inline]
322 | fn default() -> Self {
323 | RelativePathBuf::new()
324 | }
325 | }
326 |
327 | impl<'a> From<&'a RelativePath> for Cow<'a, RelativePath> {
328 | #[inline]
329 | fn from(s: &'a RelativePath) -> Cow<'a, RelativePath> {
330 | Cow::Borrowed(s)
331 | }
332 | }
333 |
334 | impl<'a> From for Cow<'a, RelativePath> {
335 | #[inline]
336 | fn from(s: RelativePathBuf) -> Cow<'a, RelativePath> {
337 | Cow::Owned(s)
338 | }
339 | }
340 |
341 | impl fmt::Debug for RelativePathBuf {
342 | #[inline]
343 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
344 | write!(fmt, "{:?}", &self.inner)
345 | }
346 | }
347 |
348 | impl AsRef for RelativePathBuf {
349 | #[inline]
350 | fn as_ref(&self) -> &RelativePath {
351 | RelativePath::new(&self.inner)
352 | }
353 | }
354 |
355 | impl AsRef for RelativePath {
356 | #[inline]
357 | fn as_ref(&self) -> &str {
358 | &self.inner
359 | }
360 | }
361 |
362 | impl Borrow for RelativePathBuf {
363 | #[inline]
364 | fn borrow(&self) -> &RelativePath {
365 | self
366 | }
367 | }
368 |
369 | impl<'a, T: ?Sized + AsRef> From<&'a T> for RelativePathBuf {
370 | #[inline]
371 | fn from(path: &'a T) -> RelativePathBuf {
372 | RelativePathBuf {
373 | inner: path.as_ref().to_owned(),
374 | }
375 | }
376 | }
377 |
378 | impl From for RelativePathBuf {
379 | #[inline]
380 | fn from(path: String) -> RelativePathBuf {
381 | RelativePathBuf { inner: path }
382 | }
383 | }
384 |
385 | impl From for String {
386 | #[inline]
387 | fn from(path: RelativePathBuf) -> String {
388 | path.into_string()
389 | }
390 | }
391 |
392 | impl ops::Deref for RelativePathBuf {
393 | type Target = RelativePath;
394 |
395 | #[inline]
396 | fn deref(&self) -> &RelativePath {
397 | RelativePath::new(&self.inner)
398 | }
399 | }
400 |
401 | impl cmp::PartialEq for RelativePathBuf {
402 | #[inline]
403 | fn eq(&self, other: &RelativePathBuf) -> bool {
404 | self.components() == other.components()
405 | }
406 | }
407 |
408 | impl cmp::Eq for RelativePathBuf {}
409 |
410 | impl cmp::PartialOrd for RelativePathBuf {
411 | #[inline]
412 | fn partial_cmp(&self, other: &RelativePathBuf) -> Option {
413 | Some(self.cmp(other))
414 | }
415 | }
416 |
417 | impl cmp::Ord for RelativePathBuf {
418 | #[inline]
419 | fn cmp(&self, other: &RelativePathBuf) -> cmp::Ordering {
420 | self.components().cmp(other.components())
421 | }
422 | }
423 |
424 | impl Hash for RelativePathBuf {
425 | #[inline]
426 | fn hash(&self, h: &mut H)
427 | where
428 | H: Hasher,
429 | {
430 | self.as_relative_path().hash(h);
431 | }
432 | }
433 |
434 | impl Extend
for RelativePathBuf
435 | where
436 | P: AsRef,
437 | {
438 | #[inline]
439 | fn extend(&mut self, iter: I)
440 | where
441 | I: IntoIterator- ,
442 | {
443 | iter.into_iter().for_each(move |p| self.push(p.as_ref()));
444 | }
445 | }
446 |
447 | impl
FromIterator
for RelativePathBuf
448 | where
449 | P: AsRef,
450 | {
451 | #[inline]
452 | fn from_iter(iter: I) -> RelativePathBuf
453 | where
454 | I: IntoIterator- ,
455 | {
456 | let mut buf = RelativePathBuf::new();
457 | buf.extend(iter);
458 | buf
459 | }
460 | }
461 |
462 | impl fmt::Display for RelativePathBuf {
463 | #[inline]
464 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
465 | fmt::Display::fmt(&self.inner, f)
466 | }
467 | }
468 |
469 | /// [`AsRef`] implementation for [`RelativePathBuf`].
470 | ///
471 | /// # Examples
472 | ///
473 | /// ```
474 | /// use relative_path::RelativePathBuf;
475 | ///
476 | /// let path = RelativePathBuf::from("foo/bar");
477 | /// let string: &str = path.as_ref();
478 | /// assert_eq!(string, "foo/bar");
479 | /// ```
480 | impl AsRef for RelativePathBuf {
481 | #[inline]
482 | fn as_ref(&self) -> &str {
483 | &self.inner
484 | }
485 | }
486 |
487 | /// [`serde::ser::Serialize`] implementation for [`RelativePathBuf`].
488 | ///
489 | /// ```
490 | /// use serde::Serialize;
491 | /// use relative_path::RelativePathBuf;
492 | ///
493 | /// #[derive(Serialize)]
494 | /// struct Document {
495 | /// path: RelativePathBuf,
496 | /// }
497 | /// ```
498 | #[cfg(feature = "serde")]
499 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "serde")))]
500 | impl serde::ser::Serialize for RelativePathBuf {
501 | #[inline]
502 | fn serialize
(&self, serializer: S) -> Result
503 | where
504 | S: serde::ser::Serializer,
505 | {
506 | serializer.serialize_str(&self.inner)
507 | }
508 | }
509 |
510 | /// [`serde::de::Deserialize`] implementation for [`RelativePathBuf`].
511 | ///
512 | /// ```
513 | /// use serde::Deserialize;
514 | /// use relative_path::RelativePathBuf;
515 | ///
516 | /// #[derive(Deserialize)]
517 | /// struct Document {
518 | /// path: RelativePathBuf,
519 | /// }
520 | /// ```
521 | #[cfg(feature = "serde")]
522 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "serde")))]
523 | impl<'de> serde::de::Deserialize<'de> for RelativePathBuf {
524 | fn deserialize(deserializer: D) -> Result
525 | where
526 | D: serde::de::Deserializer<'de>,
527 | {
528 | struct Visitor;
529 |
530 | impl serde::de::Visitor<'_> for Visitor {
531 | type Value = RelativePathBuf;
532 |
533 | #[inline]
534 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
535 | formatter.write_str("a relative path")
536 | }
537 |
538 | #[inline]
539 | fn visit_string(self, input: String) -> Result
540 | where
541 | E: serde::de::Error,
542 | {
543 | Ok(RelativePathBuf::from(input))
544 | }
545 |
546 | #[inline]
547 | fn visit_str(self, input: &str) -> Result
548 | where
549 | E: serde::de::Error,
550 | {
551 | Ok(RelativePathBuf::from(input.to_owned()))
552 | }
553 | }
554 |
555 | deserializer.deserialize_str(Visitor)
556 | }
557 | }
558 |
--------------------------------------------------------------------------------
/relative-path/src/tests.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::too_many_lines)]
2 |
3 | use super::*;
4 |
5 | use std::format;
6 | use std::path::Path;
7 | use std::rc::Rc;
8 | use std::string::ToString;
9 | use std::sync::Arc;
10 | use std::vec;
11 | use std::vec::Vec;
12 |
13 | macro_rules! t(
14 | ($path:expr, iter: $iter:expr) => (
15 | {
16 | let path = RelativePath::new($path);
17 |
18 | // Forward iteration
19 | let comps = path.iter().map(str::to_string).collect::>();
20 | let exp: &[&str] = &$iter;
21 | let exps = exp.iter().map(|s| s.to_string()).collect::>();
22 | assert!(comps == exps, "iter: Expected {:?}, found {:?}",
23 | exps, comps);
24 |
25 | // Reverse iteration
26 | let comps = RelativePath::new($path).iter().rev().map(str::to_string)
27 | .collect::>();
28 | let exps = exps.into_iter().rev().collect::>();
29 | assert!(comps == exps, "iter().rev(): Expected {:?}, found {:?}",
30 | exps, comps);
31 | }
32 | );
33 |
34 | ($path:expr, parent: $parent:expr, file_name: $file:expr) => (
35 | {
36 | let path = RelativePath::new($path);
37 |
38 | let parent = path.parent().map(|p| p.as_str());
39 | let exp_parent: Option<&str> = $parent;
40 | assert!(parent == exp_parent, "parent: Expected {:?}, found {:?}",
41 | exp_parent, parent);
42 |
43 | let file = path.file_name();
44 | let exp_file: Option<&str> = $file;
45 | assert!(file == exp_file, "file_name: Expected {:?}, found {:?}",
46 | exp_file, file);
47 | }
48 | );
49 |
50 | ($path:expr, file_stem: $file_stem:expr, extension: $extension:expr) => (
51 | {
52 | let path = RelativePath::new($path);
53 |
54 | let stem = path.file_stem();
55 | let exp_stem: Option<&str> = $file_stem;
56 | assert!(stem == exp_stem, "file_stem: Expected {:?}, found {:?}",
57 | exp_stem, stem);
58 |
59 | let ext = path.extension();
60 | let exp_ext: Option<&str> = $extension;
61 | assert!(ext == exp_ext, "extension: Expected {:?}, found {:?}",
62 | exp_ext, ext);
63 | }
64 | );
65 |
66 | ($path:expr, iter: $iter:expr,
67 | parent: $parent:expr, file_name: $file:expr,
68 | file_stem: $file_stem:expr, extension: $extension:expr) => (
69 | {
70 | t!($path, iter: $iter);
71 | t!($path, parent: $parent, file_name: $file);
72 | t!($path, file_stem: $file_stem, extension: $extension);
73 | }
74 | );
75 | );
76 |
77 | fn assert_components(components: &[&str], path: &RelativePath) {
78 | let components = components
79 | .iter()
80 | .copied()
81 | .map(Component::Normal)
82 | .collect::>();
83 | let result: Vec<_> = path.components().collect();
84 | assert_eq!(&components[..], &result[..]);
85 | }
86 |
87 | fn rp(input: &str) -> &RelativePath {
88 | RelativePath::new(input)
89 | }
90 |
91 | #[test]
92 | #[allow(clippy::cognitive_complexity)]
93 | pub fn test_decompositions() {
94 | t!("",
95 | iter: [],
96 | parent: None,
97 | file_name: None,
98 | file_stem: None,
99 | extension: None
100 | );
101 |
102 | t!("foo",
103 | iter: ["foo"],
104 | parent: Some(""),
105 | file_name: Some("foo"),
106 | file_stem: Some("foo"),
107 | extension: None
108 | );
109 |
110 | t!("/",
111 | iter: [],
112 | parent: Some(""),
113 | file_name: None,
114 | file_stem: None,
115 | extension: None
116 | );
117 |
118 | t!("/foo",
119 | iter: ["foo"],
120 | parent: Some(""),
121 | file_name: Some("foo"),
122 | file_stem: Some("foo"),
123 | extension: None
124 | );
125 |
126 | t!("foo/",
127 | iter: ["foo"],
128 | parent: Some(""),
129 | file_name: Some("foo"),
130 | file_stem: Some("foo"),
131 | extension: None
132 | );
133 |
134 | t!("/foo/",
135 | iter: ["foo"],
136 | parent: Some(""),
137 | file_name: Some("foo"),
138 | file_stem: Some("foo"),
139 | extension: None
140 | );
141 |
142 | t!("foo/bar",
143 | iter: ["foo", "bar"],
144 | parent: Some("foo"),
145 | file_name: Some("bar"),
146 | file_stem: Some("bar"),
147 | extension: None
148 | );
149 |
150 | t!("/foo/bar",
151 | iter: ["foo", "bar"],
152 | parent: Some("/foo"),
153 | file_name: Some("bar"),
154 | file_stem: Some("bar"),
155 | extension: None
156 | );
157 |
158 | t!("///foo///",
159 | iter: ["foo"],
160 | parent: Some(""),
161 | file_name: Some("foo"),
162 | file_stem: Some("foo"),
163 | extension: None
164 | );
165 |
166 | t!("///foo///bar",
167 | iter: ["foo", "bar"],
168 | parent: Some("///foo"),
169 | file_name: Some("bar"),
170 | file_stem: Some("bar"),
171 | extension: None
172 | );
173 |
174 | t!("./.",
175 | iter: [".", "."],
176 | parent: Some(""),
177 | file_name: None,
178 | file_stem: None,
179 | extension: None
180 | );
181 |
182 | t!("/..",
183 | iter: [".."],
184 | parent: Some(""),
185 | file_name: None,
186 | file_stem: None,
187 | extension: None
188 | );
189 |
190 | t!("../",
191 | iter: [".."],
192 | parent: Some(""),
193 | file_name: None,
194 | file_stem: None,
195 | extension: None
196 | );
197 |
198 | t!("foo/.",
199 | iter: ["foo", "."],
200 | parent: Some(""),
201 | file_name: Some("foo"),
202 | file_stem: Some("foo"),
203 | extension: None
204 | );
205 |
206 | t!("foo/..",
207 | iter: ["foo", ".."],
208 | parent: Some("foo"),
209 | file_name: None,
210 | file_stem: None,
211 | extension: None
212 | );
213 |
214 | t!("foo/./",
215 | iter: ["foo", "."],
216 | parent: Some(""),
217 | file_name: Some("foo"),
218 | file_stem: Some("foo"),
219 | extension: None
220 | );
221 |
222 | t!("foo/./bar",
223 | iter: ["foo", ".", "bar"],
224 | parent: Some("foo/."),
225 | file_name: Some("bar"),
226 | file_stem: Some("bar"),
227 | extension: None
228 | );
229 |
230 | t!("foo/../",
231 | iter: ["foo", ".."],
232 | parent: Some("foo"),
233 | file_name: None,
234 | file_stem: None,
235 | extension: None
236 | );
237 |
238 | t!("foo/../bar",
239 | iter: ["foo", "..", "bar"],
240 | parent: Some("foo/.."),
241 | file_name: Some("bar"),
242 | file_stem: Some("bar"),
243 | extension: None
244 | );
245 |
246 | t!("./a",
247 | iter: [".", "a"],
248 | parent: Some("."),
249 | file_name: Some("a"),
250 | file_stem: Some("a"),
251 | extension: None
252 | );
253 |
254 | t!(".",
255 | iter: ["."],
256 | parent: Some(""),
257 | file_name: None,
258 | file_stem: None,
259 | extension: None
260 | );
261 |
262 | t!("./",
263 | iter: ["."],
264 | parent: Some(""),
265 | file_name: None,
266 | file_stem: None,
267 | extension: None
268 | );
269 |
270 | t!("a/b",
271 | iter: ["a", "b"],
272 | parent: Some("a"),
273 | file_name: Some("b"),
274 | file_stem: Some("b"),
275 | extension: None
276 | );
277 |
278 | t!("a//b",
279 | iter: ["a", "b"],
280 | parent: Some("a"),
281 | file_name: Some("b"),
282 | file_stem: Some("b"),
283 | extension: None
284 | );
285 |
286 | t!("a/./b",
287 | iter: ["a", ".", "b"],
288 | parent: Some("a/."),
289 | file_name: Some("b"),
290 | file_stem: Some("b"),
291 | extension: None
292 | );
293 |
294 | t!("a/b/c",
295 | iter: ["a", "b", "c"],
296 | parent: Some("a/b"),
297 | file_name: Some("c"),
298 | file_stem: Some("c"),
299 | extension: None
300 | );
301 |
302 | t!(".foo",
303 | iter: [".foo"],
304 | parent: Some(""),
305 | file_name: Some(".foo"),
306 | file_stem: Some(".foo"),
307 | extension: None
308 | );
309 | }
310 |
311 | #[test]
312 | pub fn test_stem_ext() {
313 | t!("foo",
314 | file_stem: Some("foo"),
315 | extension: None
316 | );
317 |
318 | t!("foo.",
319 | file_stem: Some("foo"),
320 | extension: Some("")
321 | );
322 |
323 | t!(".foo",
324 | file_stem: Some(".foo"),
325 | extension: None
326 | );
327 |
328 | t!("foo.txt",
329 | file_stem: Some("foo"),
330 | extension: Some("txt")
331 | );
332 |
333 | t!("foo.bar.txt",
334 | file_stem: Some("foo.bar"),
335 | extension: Some("txt")
336 | );
337 |
338 | t!("foo.bar.",
339 | file_stem: Some("foo.bar"),
340 | extension: Some("")
341 | );
342 |
343 | t!(".", file_stem: None, extension: None);
344 |
345 | t!("..", file_stem: None, extension: None);
346 |
347 | t!("", file_stem: None, extension: None);
348 | }
349 |
350 | #[test]
351 | pub fn test_set_file_name() {
352 | macro_rules! tfn(
353 | ($path:expr, $file:expr, $expected:expr) => ( {
354 | let mut p = RelativePathBuf::from($path);
355 | p.set_file_name($file);
356 | assert!(p.as_str() == $expected,
357 | "setting file name of {:?} to {:?}: Expected {:?}, got {:?}",
358 | $path, $file, $expected,
359 | p.as_str());
360 | });
361 | );
362 |
363 | tfn!("foo", "foo", "foo");
364 | tfn!("foo", "bar", "bar");
365 | tfn!("foo", "", "");
366 | tfn!("", "foo", "foo");
367 |
368 | tfn!(".", "foo", "./foo");
369 | tfn!("foo/", "bar", "bar");
370 | tfn!("foo/.", "bar", "bar");
371 | tfn!("..", "foo", "../foo");
372 | tfn!("foo/..", "bar", "foo/../bar");
373 | tfn!("/", "foo", "/foo");
374 | }
375 |
376 | #[test]
377 | pub fn test_set_extension() {
378 | macro_rules! tse(
379 | ($path:expr, $ext:expr, $expected:expr, $output:expr) => ( {
380 | let mut p = RelativePathBuf::from($path);
381 | let output = p.set_extension($ext);
382 | assert!(p.as_str() == $expected && output == $output,
383 | "setting extension of {:?} to {:?}: Expected {:?}/{:?}, got {:?}/{:?}",
384 | $path, $ext, $expected, $output,
385 | p.as_str(), output);
386 | });
387 | );
388 |
389 | tse!("foo", "txt", "foo.txt", true);
390 | tse!("foo.bar", "txt", "foo.txt", true);
391 | tse!("foo.bar.baz", "txt", "foo.bar.txt", true);
392 | tse!(".test", "txt", ".test.txt", true);
393 | tse!("foo.txt", "", "foo", true);
394 | tse!("foo", "", "foo", true);
395 | tse!("", "foo", "", false);
396 | tse!(".", "foo", ".", false);
397 | tse!("foo/", "bar", "foo.bar", true);
398 | tse!("foo/.", "bar", "foo.bar", true);
399 | tse!("..", "foo", "..", false);
400 | tse!("foo/..", "bar", "foo/..", false);
401 | tse!("/", "foo", "/", false);
402 | }
403 |
404 | #[test]
405 | fn test_eq_recievers() {
406 | use std::borrow::Cow;
407 |
408 | let borrowed: &RelativePath = RelativePath::new("foo/bar");
409 | let mut owned: RelativePathBuf = RelativePathBuf::new();
410 | owned.push("foo");
411 | owned.push("bar");
412 | let borrowed_cow: Cow = borrowed.into();
413 | let owned_cow: Cow = owned.clone().into();
414 |
415 | macro_rules! t {
416 | ($($current:expr),+) => {
417 | $(
418 | assert_eq!($current, borrowed);
419 | assert_eq!($current, owned);
420 | assert_eq!($current, borrowed_cow);
421 | assert_eq!($current, owned_cow);
422 | )+
423 | }
424 | }
425 |
426 | t!(borrowed, owned, borrowed_cow, owned_cow);
427 | }
428 |
429 | #[test]
430 | #[allow(clippy::cognitive_complexity)]
431 | pub fn test_compare() {
432 | use std::collections::hash_map::DefaultHasher;
433 | use std::hash::{Hash, Hasher};
434 |
435 | fn hash(t: T) -> u64 {
436 | let mut s = DefaultHasher::new();
437 | t.hash(&mut s);
438 | s.finish()
439 | }
440 |
441 | macro_rules! tc(
442 | ($path1:expr, $path2:expr, eq: $eq:expr,
443 | starts_with: $starts_with:expr, ends_with: $ends_with:expr,
444 | relative_from: $relative_from:expr) => ({
445 | let path1 = RelativePath::new($path1);
446 | let path2 = RelativePath::new($path2);
447 |
448 | let eq = path1 == path2;
449 | assert!(eq == $eq, "{:?} == {:?}, expected {:?}, got {:?}",
450 | $path1, $path2, $eq, eq);
451 | assert!($eq == (hash(path1) == hash(path2)),
452 | "{:?} == {:?}, expected {:?}, got {} and {}",
453 | $path1, $path2, $eq, hash(path1), hash(path2));
454 |
455 | let starts_with = path1.starts_with(path2);
456 | assert!(starts_with == $starts_with,
457 | "{:?}.starts_with({:?}), expected {:?}, got {:?}", $path1, $path2,
458 | $starts_with, starts_with);
459 |
460 | let ends_with = path1.ends_with(path2);
461 | assert!(ends_with == $ends_with,
462 | "{:?}.ends_with({:?}), expected {:?}, got {:?}", $path1, $path2,
463 | $ends_with, ends_with);
464 |
465 | let relative_from = path1.strip_prefix(path2)
466 | .map(|p| p.as_str())
467 | .ok();
468 | let exp: Option<&str> = $relative_from;
469 | assert!(relative_from == exp,
470 | "{:?}.strip_prefix({:?}), expected {:?}, got {:?}",
471 | $path1, $path2, exp, relative_from);
472 | });
473 | );
474 |
475 | tc!("", "",
476 | eq: true,
477 | starts_with: true,
478 | ends_with: true,
479 | relative_from: Some("")
480 | );
481 |
482 | tc!("foo", "",
483 | eq: false,
484 | starts_with: true,
485 | ends_with: true,
486 | relative_from: Some("foo")
487 | );
488 |
489 | tc!("", "foo",
490 | eq: false,
491 | starts_with: false,
492 | ends_with: false,
493 | relative_from: None
494 | );
495 |
496 | tc!("foo", "foo",
497 | eq: true,
498 | starts_with: true,
499 | ends_with: true,
500 | relative_from: Some("")
501 | );
502 |
503 | tc!("foo/", "foo",
504 | eq: true,
505 | starts_with: true,
506 | ends_with: true,
507 | relative_from: Some("")
508 | );
509 |
510 | tc!("foo/bar", "foo",
511 | eq: false,
512 | starts_with: true,
513 | ends_with: false,
514 | relative_from: Some("bar")
515 | );
516 |
517 | tc!("foo/bar/baz", "foo/bar",
518 | eq: false,
519 | starts_with: true,
520 | ends_with: false,
521 | relative_from: Some("baz")
522 | );
523 |
524 | tc!("foo/bar", "foo/bar/baz",
525 | eq: false,
526 | starts_with: false,
527 | ends_with: false,
528 | relative_from: None
529 | );
530 | }
531 |
532 | #[test]
533 | fn test_join() {
534 | assert_components(&["foo", "bar", "baz"], &rp("foo/bar").join("baz///"));
535 | assert_components(
536 | &["hello", "world", "foo", "bar", "baz"],
537 | &rp("hello/world").join("///foo/bar/baz"),
538 | );
539 | assert_components(&["foo", "bar", "baz"], &rp("").join("foo/bar/baz"));
540 | }
541 |
542 | #[test]
543 | fn test_components_iterator() {
544 | use self::Component::*;
545 |
546 | assert_eq!(
547 | vec![Normal("hello"), Normal("world")],
548 | rp("/hello///world//").components().collect::>()
549 | );
550 | }
551 |
552 | #[test]
553 | fn test_to_path_buf() {
554 | let path = rp("/hello///world//");
555 | let path_buf = path.to_path(".");
556 | let expected = Path::new(".").join("hello").join("world");
557 | assert_eq!(expected, path_buf);
558 | }
559 |
560 | #[test]
561 | fn test_eq() {
562 | assert_eq!(rp("//foo///bar"), rp("/foo/bar"));
563 | assert_eq!(rp("foo///bar"), rp("foo/bar"));
564 | assert_eq!(rp("foo"), rp("foo"));
565 | assert_eq!(rp("foo"), rp("foo").to_relative_path_buf());
566 | }
567 |
568 | #[test]
569 | fn test_next_back() {
570 | use self::Component::*;
571 |
572 | let mut it = rp("baz/bar///foo").components();
573 | assert_eq!(Some(Normal("foo")), it.next_back());
574 | assert_eq!(Some(Normal("bar")), it.next_back());
575 | assert_eq!(Some(Normal("baz")), it.next_back());
576 | assert_eq!(None, it.next_back());
577 | }
578 |
579 | #[test]
580 | fn test_parent() {
581 | let path = rp("baz/./bar/foo//./.");
582 |
583 | assert_eq!(Some(rp("baz/./bar")), path.parent());
584 | assert_eq!(
585 | Some(rp("baz/.")),
586 | path.parent().and_then(RelativePath::parent)
587 | );
588 | assert_eq!(
589 | Some(rp("")),
590 | path.parent()
591 | .and_then(RelativePath::parent)
592 | .and_then(RelativePath::parent)
593 | );
594 | assert_eq!(
595 | None,
596 | path.parent()
597 | .and_then(RelativePath::parent)
598 | .and_then(RelativePath::parent)
599 | .and_then(RelativePath::parent)
600 | );
601 | }
602 |
603 | #[test]
604 | fn test_relative_path_buf() {
605 | assert_eq!(
606 | rp("hello/world/."),
607 | rp("/hello///world//").to_owned().join(".")
608 | );
609 | }
610 |
611 | #[test]
612 | fn test_normalize() {
613 | assert_eq!(rp("c/d"), rp("a/.././b/../c/d").normalize());
614 | }
615 |
616 | #[test]
617 | fn test_relative_to() {
618 | assert_eq!(
619 | rp("foo/foo/bar"),
620 | rp("foo/bar").join_normalized("../foo/bar")
621 | );
622 |
623 | assert_eq!(
624 | rp("../c/e"),
625 | rp("x/y").join_normalized("../../a/b/../../../c/d/../e")
626 | );
627 | }
628 |
629 | #[test]
630 | fn test_from() {
631 | assert_eq!(
632 | rp("foo/bar").to_owned(),
633 | RelativePathBuf::from(String::from("foo/bar")),
634 | );
635 |
636 | assert_eq!(
637 | RelativePathBuf::from(rp("foo/bar")),
638 | RelativePathBuf::from("foo/bar"),
639 | );
640 |
641 | assert_eq!(rp("foo/bar").to_owned(), RelativePathBuf::from("foo/bar"),);
642 |
643 | assert_eq!(&*Box::::from(rp("foo/bar")), rp("foo/bar"));
644 | assert_eq!(
645 | &*Box::::from(RelativePathBuf::from("foo/bar")),
646 | rp("foo/bar")
647 | );
648 |
649 | assert_eq!(&*Arc::::from(rp("foo/bar")), rp("foo/bar"));
650 | assert_eq!(
651 | &*Arc::::from(RelativePathBuf::from("foo/bar")),
652 | rp("foo/bar")
653 | );
654 |
655 | assert_eq!(&*Rc::::from(rp("foo/bar")), rp("foo/bar"));
656 | assert_eq!(
657 | &*Rc::::from(RelativePathBuf::from("foo/bar")),
658 | rp("foo/bar")
659 | );
660 | }
661 |
662 | #[test]
663 | fn test_relative_path_asref_str() {
664 | assert_eq!(
665 | >::as_ref(rp("foo/bar")),
666 | "foo/bar"
667 | );
668 | }
669 |
670 | #[test]
671 | fn test_default() {
672 | assert_eq!(RelativePathBuf::new(), RelativePathBuf::default(),);
673 | }
674 |
675 | #[test]
676 | pub fn test_push() {
677 | macro_rules! tp(
678 | ($path:expr, $push:expr, $expected:expr) => ( {
679 | let mut actual = RelativePathBuf::from($path);
680 | actual.push($push);
681 | assert!(actual.as_str() == $expected,
682 | "pushing {:?} onto {:?}: Expected {:?}, got {:?}",
683 | $push, $path, $expected, actual.as_str());
684 | });
685 | );
686 |
687 | tp!("", "foo", "foo");
688 | tp!("foo", "bar", "foo/bar");
689 | tp!("foo/", "bar", "foo/bar");
690 | tp!("foo//", "bar", "foo//bar");
691 | tp!("foo/.", "bar", "foo/./bar");
692 | tp!("foo./.", "bar", "foo././bar");
693 | tp!("foo", "", "foo/");
694 | tp!("foo", ".", "foo/.");
695 | tp!("foo", "..", "foo/..");
696 | }
697 |
698 | #[test]
699 | pub fn test_pop() {
700 | macro_rules! tp(
701 | ($path:expr, $expected:expr, $output:expr) => ( {
702 | let mut actual = RelativePathBuf::from($path);
703 | let output = actual.pop();
704 | assert!(actual.as_str() == $expected && output == $output,
705 | "popping from {:?}: Expected {:?}/{:?}, got {:?}/{:?}",
706 | $path, $expected, $output,
707 | actual.as_str(), output);
708 | });
709 | );
710 |
711 | tp!("", "", false);
712 | tp!("/", "", true);
713 | tp!("foo", "", true);
714 | tp!(".", "", true);
715 | tp!("/foo", "", true);
716 | tp!("/foo/bar", "/foo", true);
717 | tp!("/foo/bar/.", "/foo", true);
718 | tp!("foo/bar", "foo", true);
719 | tp!("foo/.", "", true);
720 | tp!("foo//bar", "foo", true);
721 | }
722 |
723 | #[test]
724 | pub fn test_display() {
725 | // NB: display delegated to the underlying string.
726 | assert_eq!(RelativePathBuf::from("foo/bar").to_string(), "foo/bar");
727 | assert_eq!(RelativePath::new("foo/bar").to_string(), "foo/bar");
728 |
729 | assert_eq!(format!("{}", RelativePathBuf::from("foo/bar")), "foo/bar");
730 | assert_eq!(format!("{}", RelativePath::new("foo/bar")), "foo/bar");
731 | }
732 |
733 | #[cfg(unix)]
734 | #[test]
735 | pub fn test_unix_from_path() {
736 | use std::ffi::OsStr;
737 | use std::os::unix::ffi::OsStrExt;
738 |
739 | assert_eq!(
740 | Err(FromPathErrorKind::NonRelative.into()),
741 | RelativePath::from_path("/foo/bar")
742 | );
743 |
744 | // Continuation byte without continuation.
745 | let non_utf8 = OsStr::from_bytes(&[0x80u8]);
746 |
747 | assert_eq!(
748 | Err(FromPathErrorKind::NonUtf8.into()),
749 | RelativePath::from_path(non_utf8)
750 | );
751 | }
752 |
753 | #[cfg(windows)]
754 | #[test]
755 | pub fn test_windows_from_path() {
756 | assert_eq!(
757 | Err(FromPathErrorKind::NonRelative.into()),
758 | RelativePath::from_path("c:\\foo\\bar")
759 | );
760 |
761 | assert_eq!(
762 | Err(FromPathErrorKind::BadSeparator.into()),
763 | RelativePath::from_path("foo\\bar")
764 | );
765 | }
766 |
767 | #[cfg(unix)]
768 | #[test]
769 | pub fn test_unix_owned_from_path() {
770 | use std::ffi::OsStr;
771 | use std::os::unix::ffi::OsStrExt;
772 |
773 | assert_eq!(
774 | Err(FromPathErrorKind::NonRelative.into()),
775 | RelativePathBuf::from_path(Path::new("/foo/bar"))
776 | );
777 |
778 | // Continuation byte without continuation.
779 | let non_utf8 = OsStr::from_bytes(&[0x80u8]);
780 |
781 | assert_eq!(
782 | Err(FromPathErrorKind::NonUtf8.into()),
783 | RelativePathBuf::from_path(Path::new(non_utf8))
784 | );
785 | }
786 |
787 | #[cfg(windows)]
788 | #[test]
789 | pub fn test_windows_owned_from_path() {
790 | assert_eq!(
791 | Err(FromPathErrorKind::NonRelative.into()),
792 | RelativePathBuf::from_path(Path::new("c:\\foo\\bar"))
793 | );
794 | }
795 |
--------------------------------------------------------------------------------