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}