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}