redoubt_alloc/
redoubt_string.rs

1// Copyright (c) 2025-2026 Federico Hoerth <memparanoid@gmail.com>
2// SPDX-License-Identifier: GPL-3.0-only
3// See LICENSE in the repository root for full license text.
4
5use alloc::string::String;
6use core::ops::{Deref, DerefMut};
7
8use redoubt_zero::{FastZeroizable, RedoubtZero, ZeroizeOnDropSentinel};
9
10/// A String wrapper with automatic zeroization and safe reallocation.
11///
12/// When capacity is exceeded, `RedoubtString` performs a safe reallocation:
13/// 1. Allocates temporary storage with current data
14/// 2. Zeroizes old allocation
15/// 3. Re-allocates with 2x capacity
16/// 4. Drains from temp (zeroizing temp)
17///
18/// This ensures no sensitive data is left in old allocations, at the cost
19/// of performance (double allocation during growth).
20///
21/// # Example
22///
23/// ```rust
24/// use redoubt_alloc::RedoubtString;
25/// use redoubt_zero::ZeroizationProbe;
26///
27/// let mut s = RedoubtString::new();
28/// let mut secret = String::from("password123");
29/// s.extend_from_mut_string(&mut secret);
30///
31/// // Source is guaranteed to be zeroized
32/// assert!(secret.is_zeroized());
33/// ```
34#[derive(RedoubtZero)]
35pub struct RedoubtString {
36    inner: String,
37    __sentinel: ZeroizeOnDropSentinel,
38}
39
40#[cfg(any(test, feature = "test-utils"))]
41impl PartialEq for RedoubtString {
42    fn eq(&self, other: &Self) -> bool {
43        // Skip __sentinel (metadata that changes during zeroization)
44        self.inner == other.inner
45    }
46}
47
48#[cfg(any(test, feature = "test-utils"))]
49impl Eq for RedoubtString {}
50
51impl core::fmt::Debug for RedoubtString {
52    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53        f.debug_struct("RedoubtString")
54            .field("data", &"REDACTED")
55            .field("len", &self.len())
56            .field("capacity", &self.capacity())
57            .finish()
58    }
59}
60
61impl RedoubtString {
62    /// Creates a new empty `RedoubtString`.
63    pub fn new() -> Self {
64        Self {
65            inner: String::new(),
66            __sentinel: ZeroizeOnDropSentinel::default(),
67        }
68    }
69
70    /// Creates a new `RedoubtString` with the specified capacity.
71    pub fn with_capacity(capacity: usize) -> Self {
72        Self {
73            inner: String::with_capacity(capacity),
74            __sentinel: ZeroizeOnDropSentinel::default(),
75        }
76    }
77
78    /// Creates a new `RedoubtString` from a mutable String, zeroizing the source.
79    pub fn from_mut_string(src: &mut String) -> Self {
80        let mut s = Self::new();
81        s.extend_from_mut_string(src);
82        s
83    }
84
85    /// Creates a new `RedoubtString` from a string slice.
86    #[allow(clippy::should_implement_trait)]
87    pub fn from_str(src: &str) -> Self {
88        let mut s = Self::new();
89        s.extend_from_str(src);
90        s
91    }
92
93    /// Returns the length of the string in bytes.
94    #[inline]
95    pub fn len(&self) -> usize {
96        self.inner.len()
97    }
98
99    /// Returns `true` if the string has a length of zero.
100    #[inline]
101    pub fn is_empty(&self) -> bool {
102        self.inner.is_empty()
103    }
104
105    /// Returns the capacity of the string in bytes.
106    #[inline]
107    pub fn capacity(&self) -> usize {
108        self.inner.capacity()
109    }
110
111    /// Grows to at least `min_capacity` if needed.
112    ///
113    /// Rounds up to the next power of 2 to maintain efficient growth pattern
114    /// (1 → 2 → 4 → 8 → 16...). Does nothing if current capacity is sufficient.
115    ///
116    /// # Safety Strategy
117    ///
118    /// 1. Allocate temp String with current data
119    /// 2. Zeroize old allocation
120    /// 3. Re-allocate with new capacity (next power of 2)
121    /// 4. Drain from temp (zeroizes temp)
122    ///
123    /// By accepting `min_capacity` and doing a single grow, this is O(n) instead
124    /// of O(n log n) when growing by large amounts.
125    #[cold]
126    #[inline(never)]
127    fn grow_to(&mut self, min_capacity: usize) {
128        let new_capacity = min_capacity.next_power_of_two();
129
130        // 1. Create temp with current data
131        let mut tmp = self.inner.clone();
132
133        // 2. Zeroize old allocation
134        self.inner.fast_zeroize();
135        self.inner.clear();
136        self.inner.shrink_to_fit();
137
138        // 3. Re-allocate with new capacity
139        self.inner.reserve_exact(new_capacity);
140
141        // 4. Drain from tmp
142        self.extend_from_mut_string(&mut tmp);
143    }
144
145    #[inline(always)]
146    fn maybe_grow_to(&mut self, min_capacity: usize) {
147        if self.capacity() >= min_capacity {
148            return;
149        }
150
151        self.grow_to(min_capacity);
152    }
153
154    /// Extends from a mutable String, zeroizing the source.
155    pub fn extend_from_mut_string(&mut self, src: &mut String) {
156        self.maybe_grow_to(self.len() + src.len());
157
158        self.inner.push_str(src);
159
160        // Zeroize and clear source
161        src.fast_zeroize();
162        src.clear();
163    }
164
165    /// Replaces contents from a mutable String, zeroizing the source.
166    ///
167    /// Equivalent to `clear()` followed by `extend_from_mut_string()`.
168    pub fn replace_from_mut_string(&mut self, src: &mut String) {
169        self.clear();
170        self.extend_from_mut_string(src);
171    }
172
173    /// Extends from str (no zeroization, src is immutable).
174    pub fn extend_from_str(&mut self, src: &str) {
175        self.maybe_grow_to(self.len() + src.len());
176        self.inner.push_str(src);
177    }
178
179    /// Clears the string, removing all contents.
180    pub fn clear(&mut self) {
181        self.inner.fast_zeroize();
182        self.inner.clear();
183    }
184
185    /// Returns a string slice containing the entire string.
186    pub fn as_str(&self) -> &str {
187        &self.inner
188    }
189
190    /// Returns a mutable string slice.
191    pub fn as_mut_str(&mut self) -> &mut str {
192        &mut self.inner
193    }
194
195    /// Returns a reference to the inner String.
196    ///
197    /// This allows direct access to the underlying String for operations
198    /// that require String-specific APIs, such as codec implementations.
199    pub fn as_string(&self) -> &String {
200        &self.inner
201    }
202
203    /// Returns a mutable reference to the inner String.
204    ///
205    /// This allows direct manipulation of the underlying String for operations
206    /// that require String-specific APIs, such as codec implementations or `drain_string`.
207    pub fn as_mut_string(&mut self) -> &mut String {
208        &mut self.inner
209    }
210}
211
212impl Default for RedoubtString {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218impl Deref for RedoubtString {
219    type Target = str;
220
221    fn deref(&self) -> &Self::Target {
222        &self.inner
223    }
224}
225
226impl DerefMut for RedoubtString {
227    fn deref_mut(&mut self) -> &mut Self::Target {
228        &mut self.inner
229    }
230}