hydroperfox_filepaths/
lib.rs

1/*!
2Work with file paths by text only.
3
4In the Windows operating system, absolute paths may either start with a drive letter followed by
5a colon or an UNC path prefix (`\\`). Therefore, this crate provides
6a `FlexPath` that is based on a variant ([_FlexPathVariant_]), which you don't need to always
7specify. This variant indicates whether to interpret Windows absolute paths
8or not.
9
10There are two _FlexPathVariant_ variants currently:
11
12- _Common_
13- _Windows_
14
15The constant `FlexPathVariant::NATIVE` is one of these variants
16based on the target platform. For the Windows operating system, it
17is always _Windows_. For other platforms, it's always _Common_.
18
19# Example
20
21```
22use hydroperfox_filepaths::FlexPath;
23
24assert_eq!("a", FlexPath::new_common("a/b").resolve("..").to_string());
25assert_eq!("a", FlexPath::new_common("a/b/..").to_string());
26assert_eq!("a/b/c/d/e", FlexPath::from_n_common(["a/b", "c/d", "e/f", ".."]).to_string());
27assert_eq!("../../c/d", FlexPath::new_common("/a/b").relative("/c/d"));
28```
29*/
30
31use lazy_regex::*;
32
33pub(crate) mod common;
34pub(crate) mod flexible;
35
36/// Indicates if special absolute paths are considered.
37///
38/// Currently, only two variants are defined, considering that there is
39/// no known operating system with different path support other than Windows:
40/// 
41/// * `Common`
42/// * `Windows`
43#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug)]
44pub enum FlexPathVariant {
45    /// Indicates that the path is manipulated in a common way.
46    Common,
47    /// Indicates that the path is manipulated compatibly with the Windows operating system.
48    Windows,
49}
50
51impl FlexPathVariant {
52    pub(crate) const NATIVE: Self = {
53        #[cfg(target_os = "windows")] {
54            Self::Windows
55        }
56        #[cfg(not(target_os = "windows"))] {
57            Self::Common
58        }
59    };
60
61    /// The variant that represents the build's target platform.
62    pub const fn native() -> Self {
63        Self::NATIVE
64    }
65}
66
67/// The `FlexPath` structure represents an always-resolved textual file path based
68/// on a [_FlexPathVariant_].
69#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
70pub struct FlexPath(String, FlexPathVariant);
71
72impl FlexPath {
73    /// Constructs a `FlexPath` with a given `variant`. This method
74    /// will resolve the specified path.
75    pub fn new(path: &str, variant: FlexPathVariant) -> Self {
76        Self(flexible::resolve_one(path, variant), variant)
77    }
78
79    /// Constructs a `FlexPath` whose variant is `Common`. This method
80    /// will resolve the specified path.
81    pub fn new_common(path: &str) -> Self {
82        Self(flexible::resolve_one(path, FlexPathVariant::Common), FlexPathVariant::Common)
83    }
84
85    /// Constructs a `FlexPath` whose variant is chosen according to the target platform.
86    /// This method will resolve the specified path.
87    pub fn new_native(path: &str) -> Self {
88        Self(flexible::resolve_one(path, FlexPathVariant::NATIVE), FlexPathVariant::NATIVE)
89    }
90
91    /// Constructs a `FlexPath` from multiple paths and a given `variant`.
92    pub fn from_n<'a, T: IntoIterator<Item = &'a str>>(paths: T, variant: FlexPathVariant) -> Self {
93        Self(flexible::resolve_n(paths, variant), variant)
94    }
95
96    /// Constructs a `FlexPath` from multiple paths and a `Common` variant.
97    pub fn from_n_common<'a, T: IntoIterator<Item = &'a str>>(paths: T) -> Self {
98        Self::from_n(paths, FlexPathVariant::Common)
99    }
100
101    /// Constructs a `FlexPath` from multiple paths and a variant based on
102    /// the target platform.
103    pub fn from_n_native<'a, T: IntoIterator<Item = &'a str>>(paths: T) -> Self {
104        Self::from_n(paths, FlexPathVariant::NATIVE)
105    }
106
107    /// Returns the variant this `FlexPath` object is based on.
108    pub fn variant(&self) -> FlexPathVariant {
109        self.1
110    }
111
112    /// Indicates whether the `FlexPath` is absolute or not.
113    pub fn is_absolute(&self) -> bool {
114        flexible::is_absolute(&self.0, self.1)
115    }
116
117    /// Resolves `path2` relative to `path1`.
118    ///
119    /// Behavior:
120    /// - Eliminates the segments `..` and `.`.
121    /// - If `path2` is absolute, this function returns a resolution of solely `path2`.
122    /// - All path separators that are backslashes (`\`) are replaced by forward ones (`/`).
123    /// - If any path is absolute, this function returns an absolute path.
124    /// - Any empty segment and trailing path separators, such as in `a/b/` and `a//b` are eliminated.
125    pub fn resolve(&self, path2: &str) -> FlexPath {
126        FlexPath(flexible::resolve(&self.0, path2, self.1), self.1)
127    }
128
129    /// Resolves multiple paths relative to this path. The
130    /// behavior is similiar to [`.resolve`]. If the given
131    /// set has no items, an empty string is returned.
132    pub fn resolve_n<'a, T: IntoIterator<Item = &'a str>>(&self, paths: T) -> FlexPath {
133        FlexPath(flexible::resolve(&self.0, &flexible::resolve_n(paths, self.1), self.1), self.1)
134    }
135
136    /**
137    Finds the relative path from this path to `to_path`.
138
139    # Behavior:
140
141    - If the paths refer to the same path, this function returns
142    an empty string.
143    - The function ensures that both paths are absolute and resolves
144    any `..` and `.` segments inside.
145    - If both paths have different prefix, `to_path` is returned.
146
147    # Panics
148
149    Panics if given paths are not absolute.
150
151    # Example
152
153    ```
154    use hydroperfox_filepaths::FlexPath;
155    assert_eq!("", FlexPath::new_common("/a/b").relative("/a/b"));
156    assert_eq!("c", FlexPath::new_common("/a/b").relative("/a/b/c"));
157    assert_eq!("../../c/d", FlexPath::new_common("/a/b").relative("/c/d"));
158    assert_eq!("../c", FlexPath::new_common("/a/b").relative("/a/c"));
159    ```
160    */
161    pub fn relative(&self, to_path: &str) -> String {
162        flexible::relative(&self.0, to_path, self.1)
163    }
164
165    /// Changes the extension of a path and returns a new string.
166    /// This method adds any lacking dot (`.`) prefix automatically to the
167    /// `extension` argument.
168    ///
169    /// This method allows multiple dots per extension. If that is not
170    /// desired, use [`.change_last_extension`].
171    ///
172    /// # Example
173    /// 
174    /// ```
175    /// use hydroperfox_filepaths::FlexPath;
176    /// assert_eq!("a.y", FlexPath::new_common("a.x").change_extension(".y").to_string());
177    /// assert_eq!("a.z", FlexPath::new_common("a.x.y").change_extension(".z").to_string());
178    /// assert_eq!("a.z.w", FlexPath::new_common("a.x.y").change_extension(".z.w").to_string());
179    /// ```
180    ///
181    pub fn change_extension(&self, extension: &str) -> FlexPath {
182        Self(change_extension(&self.0, extension), self.1)
183    }
184
185    /// Changes only the last extension of a path and returns a new string.
186    /// This method adds any lacking dot (`.`) prefix automatically to the
187    /// `extension` argument.
188    ///
189    /// # Panics
190    ///
191    /// Panics if the extension contains more than one dot.
192    ///
193    pub fn change_last_extension(&self, extension: &str) -> FlexPath {
194        Self(change_last_extension(&self.0, extension), self.1)
195    }
196
197    /// Checks if a file path has a specific extension.
198    /// This method adds any lacking dot (`.`) prefix automatically to the
199    /// `extension` argument.
200    pub fn has_extension(&self, extension: &str) -> bool {
201        has_extension(&self.0, extension)
202    }
203
204    /// Checks if a file path has any of multiple specific extensions.
205    /// This method adds any lacking dot (`.`) prefix automatically to each
206    /// extension argument.
207    pub fn has_extensions<'a, T: IntoIterator<Item = &'a str>>(&self, extensions: T) -> bool {
208        has_extensions(&self.0, extensions)
209    }
210
211    /// Returns the base name of a file path.
212    ///
213    /// # Example
214    /// 
215    /// ```
216    /// use hydroperfox_filepaths::FlexPath;
217    /// assert_eq!("qux.html", FlexPath::new_common("foo/qux.html").base_name());
218    /// ```
219    pub fn base_name(&self) -> String {
220        base_name(&self.0)
221    }
222
223    /// Returns the base name of a file path, removing any of the specified extensions.
224    /// This method adds any lacking dot (`.`) prefix automatically to each
225    /// extension argument.
226    ///
227    /// # Example
228    /// 
229    /// ```
230    /// use hydroperfox_filepaths::FlexPath;
231    /// assert_eq!("qux", FlexPath::new_common("foo/qux.html").base_name_without_ext([".html"]));
232    /// ```
233    pub fn base_name_without_ext<'a, T>(&self, extensions: T) -> String
234        where T: IntoIterator<Item = &'a str>
235    {
236        base_name_without_ext(&self.0, extensions)
237    }
238
239    /// Returns a string representation of the path,
240    /// delimiting segments with either a forward slash (`/`) or backward slash (`\`)
241    /// depending on the path's `FlexPathVariant`.
242    pub fn to_string_with_flex_separator(&self) -> String {
243        if self.variant() == FlexPathVariant::Windows {
244            self.0.replace('/', "\\")
245        } else {
246            self.0.clone()
247        }
248    }
249}
250
251impl ToString for FlexPath {
252    /// Returns a string representation of the path,
253    /// always delimiting segments with a forward slash (`/`).
254    fn to_string(&self) -> String {
255        self.0.clone()
256    }
257}
258
259static STARTS_WITH_PATH_SEPARATOR: Lazy<Regex> = lazy_regex!(r"^[/\\]");
260
261fn change_extension(path: &str, extension: &str) -> String {
262    let extension = (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension;
263    if regex_find!(r"(\.[^\.]+)+$", path).is_none() {
264        return path.to_owned() + &extension;
265    }
266    regex_replace!(r"(\.[^\.]+)+$", path, |_, _| &extension).into_owned()
267}
268
269fn change_last_extension(path: &str, extension: &str) -> String {
270    let extension = (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension;
271    assert!(
272        extension[1..].find('.').is_none(),
273        "The argument to hydroperfox_filepaths::change_last_extension() must only contain one extension; got {}",
274        extension
275    );
276    if regex_find!(r"(\..+)$", path).is_none() {
277        return path.to_owned() + &extension;
278    }
279    regex_replace!(r"(\..+)$", path, |_, _| &extension).into_owned()
280}
281
282/// Adds prefix dot to extension if missing.
283fn extension_arg(extension: &str) -> String {
284    (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension
285}
286
287fn has_extension(path: &str, extension: &str) -> bool {
288    let extension = (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension;
289    path.ends_with(&extension_arg(&extension))
290}
291
292fn has_extensions<'a, T: IntoIterator<Item = &'a str>>(path: &str, extensions: T) -> bool {
293    extensions.into_iter().any(|ext| has_extension(path, ext))
294}
295
296fn base_name(path: &str) -> String {
297    path.split('/').last().map_or("", |s| s).to_owned()
298}
299
300fn base_name_without_ext<'a, T>(path: &str, extensions: T) -> String
301    where T: IntoIterator<Item = &'a str>
302{
303    let extensions = extensions.into_iter().map(extension_arg).collect::<Vec<String>>();
304    path.split('/').last().map_or("".to_owned(), |base| {
305        regex_replace!(r"(\.[^\.]+)+$", base, |_, prev_ext: &str| {
306            (if extensions.iter().any(|ext| ext == prev_ext) { "" } else { prev_ext }).to_owned()
307        }).into_owned()
308    })
309}
310
311#[cfg(test)]
312mod test {
313    use super::*;
314
315    #[test]
316    fn extension_and_base_name() {
317        assert!(FlexPath::new_common("a.x").has_extensions([".x", ".y"]));
318        assert_eq!("a.y", FlexPath::new_common("a.x").change_extension(".y").to_string());
319        assert_eq!("a.0", FlexPath::new_common("a.x.y").change_extension(".0").to_string());
320        assert_eq!("a.0.1", FlexPath::new_common("a.x.y").change_extension(".0.1").to_string());
321
322        assert_eq!("qux.html", FlexPath::new_common("foo/qux.html").base_name());
323        assert_eq!("qux", FlexPath::new_common("foo/qux.html").base_name_without_ext([".html"]));
324    }
325
326    #[test]
327    fn resolution() {
328        assert_eq!("a", FlexPath::from_n_common(["a/b/.."]).to_string());
329        assert_eq!("a", FlexPath::from_n_common(["a", "b", ".."]).to_string());
330        assert_eq!("/a/b", FlexPath::new_common("/c").resolve("/a/b").to_string());
331        assert_eq!("a", FlexPath::new_common("a/b").resolve("..").to_string());
332        assert_eq!("a/b", FlexPath::new_common("a/b/").to_string());
333        assert_eq!("a/b", FlexPath::new_common("a//b").to_string());
334
335        let windows = FlexPathVariant::Windows;
336        assert_eq!(r"\\Whack/a/Box", FlexPath::from_n(["foo", r"\\Whack////a//Box", "..", "Box"], windows).to_string());
337        assert_eq!("C:/a", FlexPath::new("C:/", windows).resolve("a").to_string());
338        assert_eq!("D:/", FlexPath::new("C:/", windows).resolve("D:/").to_string());
339        assert_eq!("D:/a", FlexPath::new("D:/a", windows).to_string());
340        assert_eq!("C:/a/f/b", FlexPath::new("a", windows).resolve("C:/a///f//b").to_string());
341    }
342
343    #[test]
344    fn relativity() {
345        assert_eq!("", FlexPath::new_common("/a/b").relative("/a/b"));
346        assert_eq!("c", FlexPath::new_common("/a/b").relative("/a/b/c"));
347        assert_eq!("../../c/d", FlexPath::new_common("/a/b/c").relative("/a/c/d"));
348        assert_eq!("..", FlexPath::new_common("/a/b/c").relative("/a/b"));
349        assert_eq!("../..", FlexPath::new_common("/a/b/c").relative("/a"));
350        assert_eq!("..", FlexPath::new_common("/a").relative("/"));
351        assert_eq!("a", FlexPath::new_common("/").relative("/a"));
352        assert_eq!("", FlexPath::new_common("/").relative("/"));
353        assert_eq!("../../c/d", FlexPath::new_common("/a/b").relative("/c/d"));
354        assert_eq!("../c", FlexPath::new_common("/a/b").relative("/a/c"));
355
356        let windows = FlexPathVariant::Windows;
357        assert_eq!("", FlexPath::new("C:/", windows).relative("C:/"));
358        assert_eq!("", FlexPath::new("C:/foo", windows).relative("C:/foo"));
359        assert_eq!(r"\\foo", FlexPath::new("C:/", windows).relative(r"\\foo"));
360        assert_eq!("../../foo", FlexPath::new(r"\\a/b", windows).relative(r"\\foo"));
361        assert_eq!("D:/", FlexPath::new("C:/", windows).relative(r"D:"));
362    }
363}