redoubt_codec_core/
codec_buffer.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
5//! Secure buffer with locked capacity and automatic zeroization.
6use alloc::vec::Vec;
7
8use redoubt_alloc::AllockedVec;
9
10#[cfg(feature = "zeroize")]
11use redoubt_zero::{FastZeroizable, RedoubtZero, ZeroizeOnDropSentinel};
12
13use crate::error::RedoubtCodecBufferError;
14
15#[cfg_attr(feature = "zeroize", derive(RedoubtZero))]
16pub struct RedoubtCodecBuffer {
17    cursor: usize,
18    capacity: usize,
19    allocked_vec: AllockedVec<u8>,
20    #[cfg(feature = "zeroize")]
21    __sentinel: ZeroizeOnDropSentinel,
22}
23
24#[cfg(feature = "zeroize")]
25impl Drop for RedoubtCodecBuffer {
26    fn drop(&mut self) {
27        self.fast_zeroize();
28    }
29}
30
31impl Default for RedoubtCodecBuffer {
32    fn default() -> Self {
33        Self::with_capacity(0)
34    }
35}
36
37impl RedoubtCodecBuffer {
38    #[inline(always)]
39    fn debug_assert_invariant(&self) {
40        debug_assert!(
41            self.cursor <= self.capacity,
42            "Invariant violated: cursor ({}) <= capacity ({})",
43            self.cursor,
44            self.capacity
45        );
46    }
47
48    #[inline(always)]
49    pub fn with_capacity(capacity: usize) -> Self {
50        let allocked_vec = AllockedVec::<u8>::with_capacity(capacity);
51
52        Self {
53            cursor: 0,
54            capacity,
55            allocked_vec,
56            #[cfg(feature = "zeroize")]
57            __sentinel: ZeroizeOnDropSentinel::default(),
58        }
59    }
60
61    #[inline(always)]
62    pub fn realloc_with_capacity(&mut self, capacity: usize) {
63        self.allocked_vec.realloc_with_capacity(capacity);
64        self.allocked_vec.fill_with_default();
65
66        self.capacity = capacity;
67        self.cursor = 0;
68    }
69
70    #[inline(always)]
71    pub fn clear(&mut self) {
72        self.cursor = 0;
73        #[cfg(feature = "zeroize")]
74        self.allocked_vec.fast_zeroize();
75    }
76
77    #[inline(always)]
78    pub fn as_slice(&self) -> &[u8] {
79        unsafe { self.allocked_vec.as_capacity_slice() }
80    }
81
82    #[inline(always)]
83    pub fn as_mut_slice(&mut self) -> &mut [u8] {
84        unsafe { self.allocked_vec.as_capacity_mut_slice() }
85    }
86
87    #[inline(always)]
88    pub fn len(&self) -> usize {
89        unsafe { self.allocked_vec.as_capacity_slice().len() }
90    }
91
92    #[inline(always)]
93    pub fn is_empty(&self) -> bool {
94        self.len() == 0
95    }
96
97    #[inline(always)]
98    pub fn write<T>(&mut self, src: &mut T) -> Result<(), RedoubtCodecBufferError> {
99        let len = core::mem::size_of::<T>();
100
101        if self.cursor + len > self.capacity {
102            return Err(RedoubtCodecBufferError::CapacityExceeded);
103        }
104
105        unsafe {
106            let ptr = self.allocked_vec.as_mut_ptr().add(self.cursor);
107            core::ptr::copy_nonoverlapping(src as *const T as *const u8, ptr, len);
108        }
109        self.cursor += len;
110
111        // Invariant must be preserved before returning.
112        self.debug_assert_invariant();
113
114        Ok(())
115    }
116
117    #[inline(always)]
118    pub fn write_slice<T>(&mut self, src: &mut [T]) -> Result<(), RedoubtCodecBufferError> {
119        let byte_len = core::mem::size_of_val(src);
120
121        if self.cursor + byte_len > self.capacity {
122            return Err(RedoubtCodecBufferError::CapacityExceeded);
123        }
124
125        unsafe {
126            let ptr = self.allocked_vec.as_mut_ptr().add(self.cursor);
127            core::ptr::copy_nonoverlapping(src.as_ptr() as *const u8, ptr, byte_len);
128        }
129        self.cursor += byte_len;
130
131        // Invariant must be preserved before returning.
132        self.debug_assert_invariant();
133
134        Ok(())
135    }
136
137    /// Exports the buffer contents as a `Vec<u8>` and zeroizes the internal buffer.
138    ///
139    /// This method creates a new `Vec` containing a copy of the buffer's data,
140    /// then immediately zeroizes the internal buffer. The zeroization ensures
141    /// that sensitive data is cleared from the `RedoubtCodecBuffer` after export,
142    /// preventing potential memory leaks of plaintext data.
143    ///
144    /// # Security
145    ///
146    /// The zeroization happens **after** copying the data to the returned `Vec`,
147    /// ensuring the internal buffer is always cleaned up when data is exported.
148    /// This is crucial when the `RedoubtCodecBuffer` contains sensitive plaintext that
149    /// should not remain in memory after encoding is complete.
150    ///
151    /// # Example
152    ///
153    /// ```ignore
154    /// let mut buf = RedoubtCodecBuffer::with_capacity(10);
155    /// buf.write_usize(&42).unwrap();
156    /// let exported = buf.export_as_vec();
157    /// // buf is now zeroized, exported contains the data
158    /// ```
159    #[inline(always)]
160    pub fn export_as_vec(&mut self) -> Vec<u8> {
161        let vec = self.as_slice().to_vec();
162        self.fast_zeroize();
163        vec
164    }
165}