af_core/
path.rs

1// Copyright © 2020 Alexandra Frydl
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! Functions for working with Unicode file system paths.
8
9pub use af_core_macros::{path_join as join, path_normalize as normalize, path_resolve as resolve};
10
11#[doc(inline)]
12pub use std::path::{is_separator, MAIN_SEPARATOR as SEPARATOR};
13
14use crate::{env, prelude::*};
15use std::cell::RefCell;
16use std::path::Path;
17
18thread_local! {
19  /// A thread-local buffer for operations that need temporary storage.
20  static THREAD_BUFFER: RefCell<String> = default();
21}
22
23/// Appends a relative path to a base path.
24///
25/// If `relative` is an absolute path, the base path is replaced completely.
26pub fn append(base: &mut String, relative: &str) {
27  if is_absolute(relative) {
28    base.replace_range(.., relative);
29    return;
30  }
31
32  match base.chars().rev().next() {
33    None => base.replace_range(.., relative),
34    Some(c) if is_separator(c) => base.push_str(relative),
35    Some(_) => {
36      base.reserve(relative.len() + 1);
37      base.push(SEPARATOR);
38      base.push_str(relative);
39    }
40  }
41}
42
43/// Returns `true` if the given path is absolute.
44pub fn is_absolute(path: &str) -> bool {
45  as_std(path).is_absolute()
46}
47
48/// Joins a base path and a relative path.
49///
50/// If `relative` is an absolute path, it is returned unmodified.
51pub fn join<'a, 'b>(base: impl PathLike<'b>, relative: impl PathLike<'a>) -> Cow<'a, str> {
52  let relative = relative.to_cow();
53
54  if is_absolute(&relative) {
55    return relative;
56  }
57
58  let mut output = base.to_owned();
59
60  append(&mut output, &relative);
61
62  output.into()
63}
64
65/// Returns the last component of the path.
66///
67/// If `path` is a root or empty path, this function returns `None`.
68pub fn last(path: &str) -> Option<&str> {
69  as_std(path).file_name()?.to_str()
70}
71
72/// Normalizes a path.
73pub fn normalize(path: &mut String) {
74  THREAD_BUFFER.with(|buffer| {
75    let mut buffer = buffer.borrow_mut();
76
77    buffer.clear();
78
79    normalize_into(path, &mut buffer);
80
81    path.replace_range(.., &buffer);
82  })
83}
84
85/// Returns a normalized version of the given path.
86pub fn normalized<'a>(path: impl PathLike<'a>) -> Cow<'a, str> {
87  let path = path.to_cow();
88
89  THREAD_BUFFER.with(|buffer| {
90    let mut buffer = buffer.borrow_mut();
91
92    buffer.clear();
93    normalize_into(&path, &mut buffer);
94
95    match path.as_ref() == *buffer {
96      true => path,
97      false => buffer.clone().into(),
98    }
99  })
100}
101
102/// Returns the parent of the given path.
103///
104/// If `path` is a root or empty path, this function returns `None`.
105pub fn parent(path: &str) -> Option<&str> {
106  as_std(path).parent()?.to_str()
107}
108
109/// Removes the last component from the path and returns it.
110///
111/// If the path is a root or empty path, this function does nothing and returns
112/// `None`.
113pub fn pop(path: &mut String) -> Option<String> {
114  let split_at = parent(&path)?.len();
115  let lead_seps = path[split_at..].chars().take_while(|c| is_separator(*c)).count();
116  let trail_seps = path.chars().rev().take_while(|c| is_separator(*c)).count();
117  let mut last = path.split_off(split_at + lead_seps);
118
119  path.truncate(split_at);
120  last.truncate(last.len() - trail_seps);
121
122  Some(last)
123}
124
125/// Resolves the given path into an absolute, normalized path.
126pub fn resolve(path: &mut String) -> Result<(), env::WorkingPathError> {
127  if !is_absolute(&path) {
128    let mut buf = env::working_path()?;
129
130    mem::swap(path, &mut buf);
131    append(path, &buf);
132  }
133
134  normalize(path);
135
136  Ok(())
137}
138
139/// Returns an absolute, normalized version of the given path.
140pub fn resolved<'a>(path: impl PathLike<'a>) -> Result<Cow<'a, str>, env::WorkingPathError> {
141  let mut path = path.to_cow();
142
143  if is_absolute(&path) {
144    return Ok(normalized(path));
145  }
146
147  resolve(path.to_mut())?;
148
149  Ok(path)
150}
151
152/// Returns `true` if the first path starts with the second path.
153pub fn starts_with(path: &str, prefix: &str) -> bool {
154  as_std(path).starts_with(prefix)
155}
156
157/// Returns the given path with a trailing separator if it does not already
158/// have one.
159pub fn with_trailing_sep<'a>(path: impl PathLike<'a>) -> Cow<'a, str> {
160  let mut path = path.to_cow();
161
162  match path.chars().rev().next() {
163    Some(c) if is_separator(c) => path,
164
165    _ => {
166      path.to_mut().push(SEPARATOR);
167      path
168    }
169  }
170}
171
172/// Converts a value into a `&Path`.
173pub fn as_std(path: &str) -> &Path {
174  path.as_ref()
175}
176
177/// Normalizes the given path into the output string.
178///
179/// The output is expected to already be empty.
180fn normalize_into(path: &str, output: &mut String) {
181  for component in as_std(path).components() {
182    match component {
183      std::path::Component::CurDir => continue,
184      std::path::Component::Normal(component) => append(output, component.to_str().unwrap()),
185      std::path::Component::Prefix(prefix) => output.push_str(prefix.as_os_str().to_str().unwrap()),
186      std::path::Component::RootDir => output.push(SEPARATOR),
187
188      std::path::Component::ParentDir => {
189        pop(output);
190      }
191    }
192  }
193}
194
195/// A trait for values that can be used in path operations.
196pub trait PathLike<'a>: Sized {
197  /// Converts this value into a `Cow<str>`.
198  fn to_cow(self) -> Cow<'a, str>;
199
200  /// Converts this value into a `String`.
201  fn to_owned(self) -> String {
202    self.to_cow().into()
203  }
204}
205
206// Implement `PathLike` for common string types.
207
208impl<'a> PathLike<'a> for &'a str {
209  fn to_cow(self) -> Cow<'a, str> {
210    self.into()
211  }
212}
213
214impl<'a> PathLike<'a> for &'a &'_ mut str {
215  fn to_cow(self) -> Cow<'a, str> {
216    (&**self).into()
217  }
218}
219
220impl<'a> PathLike<'a> for String {
221  fn to_cow(self) -> Cow<'a, str> {
222    self.into()
223  }
224}
225
226impl<'a> PathLike<'a> for &'a String {
227  fn to_cow(self) -> Cow<'a, str> {
228    self.into()
229  }
230}
231
232impl<'a> PathLike<'a> for &'a &'_ mut String {
233  fn to_cow(self) -> Cow<'a, str> {
234    (&**self).into()
235  }
236}
237
238impl<'a> PathLike<'a> for Cow<'a, str> {
239  fn to_cow(self) -> Cow<'a, str> {
240    self
241  }
242}
243
244impl<'a> PathLike<'a> for &'a Cow<'_, str> {
245  fn to_cow(self) -> Cow<'a, str> {
246    self.as_ref().into()
247  }
248}
249
250impl<'a> PathLike<'a> for &'a &'_ mut Cow<'_, str> {
251  fn to_cow(self) -> Cow<'a, str> {
252    self.as_ref().into()
253  }
254}
255
256impl<'a> PathLike<'a> for &'a std::path::PathBuf {
257  fn to_cow(self) -> Cow<'a, str> {
258    self.to_string_lossy().to_cow()
259  }
260}