dirbuf/lib.rs
1#![cfg_attr(doc, doc = include_str!("../README.md"))]
2
3#![cfg_attr(feature = "nightly-safe-impl", feature(os_string_truncate))]
4
5#![warn(
6 clippy::pedantic,
7 clippy::undocumented_unsafe_blocks,
8 clippy::unnecessary_safety_doc,
9)]
10#![allow(clippy::inline_always)]
11
12use std::path::{Path, PathBuf};
13use std::ffi::OsString;
14
15mod inner;
16
17pub use inner::{DirBuf, DirBufEntry};
18
19macro_rules! impl_common {
20 ($($x:tt)+) => {
21 impl $($x)+ {
22 /// Efficiently join a path with another.
23 ///
24 /// The returned [`DirBufEntry`] reuses the underlying buffer,
25 /// and must be dropped before `join` can be called again.
26 /// The `DirBufEntry` holds a mutable reference to ensure
27 /// `join` cannot be called before the `DirBufEntry` is dropped.
28 ///
29 /// If the path segment is coming from untrusted user data and
30 /// panics are undesirable, use [`try_join`](Self::try_join) instead.
31 ///
32 /// # Panics
33 /// Panics if `path` is not a [trivial path](crate#trivial-paths).
34 ///
35 /// # Performance
36 /// The overhead of the panic check is not insubstantial.
37 /// When multiple `join`s are used, this is still usually
38 /// faster than using a `PathBuf`, but if you know
39 /// the path is trivial (e.g. if it is a string literal),
40 /// consider using [`join_unchecked`](Self::join_unchecked) to remove this overhead.
41 ///
42 #[inline]
43 #[must_use]
44 pub fn join(&mut self, path: impl AsRef<Path>) -> DirBufEntry<'_> {
45 match self.try_join_inner(path.as_ref()) {
46 Err(err) => panic!("unable to join paths: {err}"),
47 Ok(entry) => entry,
48 }
49 }
50
51 /// Variant of `join` optimized for string literals.
52 ///
53 /// Semantic behavior is identical to [`Self::join`],
54 /// the only difference is performance.
55 ///
56 /// For simple cases, this can have performance equivalent to
57 /// that of [`Self::join_unchecked`] without the unsafety,
58 /// but for more complex cases,
59 /// [`Self::join`] may be more performant.
60 ///
61 /// It achieves this performance gain by leveraging inlining and
62 /// constant folding.
63 #[inline(always)]
64 #[must_use]
65 pub fn join_lit(&mut self, path: &str) -> DirBufEntry<'_> {
66 #[cfg(unix)]
67 {
68 if trivially_trivial(path) {
69 // SAFETY: just checked if it is trivial
70 return unsafe { self.join_inner(path.as_ref()) };
71 }
72 }
73 self.join(path)
74 }
75
76 /// Attempt to join a path with another.
77 ///
78 /// The returned [`DirBufEntry`] reuses the underlying buffer,
79 /// and must be dropped before `join` can be called again.
80 /// The `DirBufEntry` holds a mutable reference to ensure
81 /// `join` cannot be called before the `DirBufEntry` is dropped.
82 ///
83 /// # Errors
84 ///
85 /// An error is returned if the path is not [trivial](crate#trivial-paths).
86 #[inline]
87 pub fn try_join(&mut self, path: impl AsRef<Path>) -> Result<DirBufEntry<'_>, JoinError> {
88 self.try_join_inner(path.as_ref())
89 }
90
91 /// Join a path to another without checking that it is trivial.
92 ///
93 /// # Safety
94 /// `path` must be a [trivial path](crate#trivial-paths).
95 #[inline]
96 #[must_use]
97 pub unsafe fn join_unchecked(&mut self, path: impl AsRef<Path>) -> DirBufEntry<'_> {
98 // SAFETY: invariant guranteed by caller
99 unsafe { self.join_inner(path.as_ref()) }
100 }
101
102 #[inline]
103 fn try_join_inner(&mut self, path: &Path) -> Result<DirBufEntry<'_>, JoinError> {
104 check_trivial(path)?;
105 // SAFETY: we just checked if the path is trivial
106 unsafe { Ok(self.join_inner(path)) }
107 }
108
109 /// # Safety
110 /// The path must be a [trivial path](crate#trivial-paths).
111 #[must_use]
112 unsafe fn join_inner(&mut self, path: &Path) -> DirBufEntry<'_> {
113
114 // we call this before each operation instead of when `DirBufEntry` is dropped
115 // to make sure it is leak-safe.
116 // SAFETY: the path must be trivial by this function's precondition
117 unsafe {self.push(path)};
118 self.as_entry()
119 }
120
121 /// Borrow as a [`Path`].
122 pub fn as_path(&self) -> &Path {
123
124 self.as_ref()
125 }
126 }
127 }
128}
129
130impl DirBuf {
131 /// Initialize an empty `DirBuf` with the given capacity.
132 #[must_use]
133 pub fn with_capacity(capacity: usize) -> Self {
134 let buf = PathBuf::with_capacity(capacity);
135 DirBuf::new(buf)
136 }
137
138 /// Return the internal buffer, resetting it to its initial state first.
139 #[must_use]
140 pub fn into_inner(mut self) -> PathBuf {
141 let mut r = PathBuf::new();
142 // SAFETY: we consume the `DirBuf` so no further operations can be performed on it.
143 std::mem::swap(unsafe { self.reset() }, &mut r);
144 r
145 }
146}
147
148
149impl<T: AsRef<Path>> PartialEq<T> for DirBuf {
150 fn eq(&self, other: &T) -> bool {
151 self.as_ref() == other.as_ref()
152 }
153}
154
155impl<T: AsRef<Path>> PartialEq<T> for DirBufEntry<'_> {
156 fn eq(&self, other: &T) -> bool {
157 self.as_ref() == other.as_ref()
158 }
159}
160
161impl_common!(DirBuf);
162impl_common!(<'buf> DirBufEntry<'buf>);
163
164/// An error that occurred while attempting to join two paths.
165#[derive(thiserror::Error, Debug, Clone)]
166#[error("path {path:?} is not trivial due to {component:?}")]
167pub struct JoinError {
168 path: PathBuf,
169 component: OsString,
170}
171
172#[cold]
173fn cold<T>(x: T) -> T {
174 x
175}
176
177#[inline(always)]
178const fn trivially_trivial(s: &str) -> bool {
179 let mut i = 0;
180 let bytes = s.as_bytes();
181 if bytes.is_empty() { return true; }
182 if bytes[0] == b'/' { return false; }
183 while i < bytes.len() {
184 if bytes[i] == b'.' {
185 return false;
186 }
187 i += 1;
188 }
189 true
190}
191
192#[inline]
193fn check_trivial(path: &Path) -> Result<(), JoinError> {
194 use std::path::Component as Comp;
195 for comp in path.components() {
196 match &comp {
197 Comp::CurDir | Comp::Normal(..) => {
198 // ok, do nothing
199 },
200 _ => return cold(Err(JoinError{
201 path: path.into(),
202 component: comp.as_os_str().to_owned(),
203 })),
204 }
205 }
206 Ok(())
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn it_works() {
215 let mut d = DirBuf::new("foo");
216 assert_eq!(d.join("bar"), Path::new("foo/bar"));
217 assert_eq!(d, "foo");
218 }
219}