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