misc_utils/
path.rs

1//! This module contains functions for file system path manipulation.
2
3use std::{
4    ffi::{OsStr, OsString},
5    path::{Path, PathBuf},
6};
7
8/// This traits extends the available methods on [`Path`].
9pub trait PathExt {
10    /// Iterator over all file extensions of a [`Path`].
11    ///
12    /// This iterator provides access to all file extensions from starting with the last extension.
13    /// File extensions are separated by a `.`-character. This supplements the [`Path::extension`] method,
14    /// which only allows you to access the last file extension.
15    ///
16    /// Accessing multiple extension can be useful, if extensions are chained to provide hints how the
17    /// file is structured, e.g., `archive.tar.xz`.
18    ///
19    /// # Example
20    ///
21    /// ```rust
22    /// # use misc_utils::path::PathExt;
23    /// # use std::ffi::OsStr;
24    /// # use std::path::Path;
25    /// #
26    /// let p = &Path::new("/home/user/projects/misc_utils/This.File.has.many.extensions");
27    /// assert_eq!(
28    ///     p.extensions().collect::<Vec<_>>(),
29    ///     vec![
30    ///         OsStr::new("extensions"),
31    ///         OsStr::new("many"),
32    ///         OsStr::new("has"),
33    ///         OsStr::new("File")
34    ///     ]
35    /// );
36    /// ```
37    fn extensions(&'_ self) -> PathExtensions<'_>;
38}
39
40impl PathExt for Path {
41    fn extensions(&'_ self) -> PathExtensions<'_> {
42        PathExtensions(self)
43    }
44}
45
46/// This traits extends the available methods on [`PathBuf`].
47pub trait PathBufExt {
48    /// Appends `extension` to [`self.file_name`](Path::file_name).
49    ///
50    /// Returns false and does nothing if [`self.file_name`](Path::file_name) is [`None`], returns `true` and appends the extension otherwise.
51    ///
52    /// The API and documentation should fully mirror [`PathBuf::set_extension`].
53    fn add_extension<S: AsRef<OsStr>>(&mut self, extension: S) -> bool;
54}
55
56impl PathBufExt for PathBuf {
57    fn add_extension<S: AsRef<OsStr>>(&mut self, extension: S) -> bool {
58        if self.file_name().is_none() {
59            return false;
60        }
61
62        let mut stem = match self.file_name() {
63            Some(stem) => stem.to_os_string(),
64            None => OsString::new(),
65        };
66
67        if !extension.as_ref().is_empty() {
68            stem.push(".");
69            stem.push(extension.as_ref());
70        }
71        self.set_file_name(&stem);
72
73        true
74    }
75}
76
77/// Iterator over all file extensions of a [`Path`].
78///
79/// This iterator provides access to all file extensions from starting with the last extension.
80/// File extensions are separated by a `.`-character. This supplements the [`Path::extension`] method,
81/// which only allows you to access the last file extension.
82///
83/// Accessing multiple extension can be useful, if extensions are chained to provide hints how the
84/// file is structured, e.g., `archive.tar.xz`.
85///
86/// # Example
87///
88/// ```rust
89/// # use misc_utils::path::PathExt;
90/// # use std::ffi::OsStr;
91/// # use std::path::Path;
92/// #
93/// let p = &Path::new("/home/user/projects/misc_utils/This.File.has.many.extensions");
94/// assert_eq!(
95///     p.extensions().collect::<Vec<_>>(),
96///     vec![
97///         OsStr::new("extensions"),
98///         OsStr::new("many"),
99///         OsStr::new("has"),
100///         OsStr::new("File")
101///     ]
102/// );
103/// ```
104#[derive(Copy, Clone, Debug)]
105pub struct PathExtensions<'a>(&'a Path);
106
107impl<'a> Iterator for PathExtensions<'a> {
108    type Item = &'a OsStr;
109
110    fn next(&mut self) -> Option<&'a OsStr> {
111        let (new_filestem, new_extension) = (self.0.file_stem(), self.0.extension());
112        if new_extension.is_none() {
113            self.0 = Path::new("");
114            None
115        } else {
116            if let Some(new_filestem) = new_filestem {
117                self.0 = Path::new(new_filestem);
118            } else {
119                self.0 = Path::new("");
120            };
121            new_extension
122        }
123    }
124}
125
126#[test]
127fn test_path_extensions() {
128    let p = &Path::new("/home/user/projects/misc_utils/Cargo.toml");
129    assert_eq!(p.extensions().collect::<Vec<_>>(), vec![OsStr::new("toml")]);
130    let p = &Path::new("/home/user/projects/misc_utils/This.File.has.many.extensions");
131    assert_eq!(
132        p.extensions().collect::<Vec<_>>(),
133        vec![
134            OsStr::new("extensions"),
135            OsStr::new("many"),
136            OsStr::new("has"),
137            OsStr::new("File")
138        ]
139    );
140    let p = &Path::new("/home/user/projects/misc_utils/.hidden");
141    assert_eq!(p.extensions().collect::<Vec<_>>(), Vec::<&OsStr>::new());
142    let p = &Path::new("Just-A.file");
143    assert_eq!(p.extensions().collect::<Vec<_>>(), vec![OsStr::new("file")]);
144}
145
146#[test]
147fn test_pathbuf_extensions() {
148    let p = PathBuf::from("/home/user/projects/misc_utils/Cargo.toml");
149    assert_eq!(p.extensions().collect::<Vec<_>>(), vec![OsStr::new("toml")]);
150    let p = PathBuf::from("/home/user/projects/misc_utils/This.File.has.many.extensions");
151    assert_eq!(
152        p.extensions().collect::<Vec<_>>(),
153        vec![
154            OsStr::new("extensions"),
155            OsStr::new("many"),
156            OsStr::new("has"),
157            OsStr::new("File")
158        ]
159    );
160    let p = PathBuf::from("/home/user/projects/misc_utils/.hidden");
161    assert_eq!(p.extensions().collect::<Vec<_>>(), Vec::<&OsStr>::new());
162    let p = PathBuf::from("Just-A.file");
163    assert_eq!(p.extensions().collect::<Vec<_>>(), vec![OsStr::new("file")]);
164}
165
166#[test]
167fn test_add_extension() {
168    let mut pb = PathBuf::from("some.file");
169    assert_eq!(pb, Path::new("some.file"));
170    assert!(PathBufExt::add_extension(&mut pb, "a"));
171    assert_eq!(pb, Path::new("some.file.a"));
172    assert!(PathBufExt::add_extension(&mut pb, "b"));
173    assert_eq!(pb, Path::new("some.file.a.b"));
174    assert!(PathBufExt::add_extension(&mut pb, "c"));
175    assert_eq!(pb, Path::new("some.file.a.b.c"));
176
177    let mut pb = PathBuf::from("/");
178    assert!(!PathBufExt::add_extension(&mut pb, "ext"));
179}