redoubt_codec_core/collections/
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;
6
7#[cfg(feature = "zeroize")]
8use redoubt_zero::FastZeroizable;
9
10use crate::codec_buffer::RedoubtCodecBuffer;
11use crate::error::{DecodeError, EncodeError, OverflowError};
12use crate::traits::{
13    BytesRequired, Decode, DecodeSlice, Encode, EncodeSlice, PreAlloc, TryDecode, TryEncode,
14};
15use crate::zeroizing::Zeroizing;
16
17use super::helpers::{header_size, process_header, write_header};
18
19/// Cleanup function for encode errors. Marked #[cold] to keep it out of the hot path.
20#[cfg(feature = "zeroize")]
21#[cold]
22#[inline(never)]
23fn cleanup_encode_error(s: &mut String, buf: &mut RedoubtCodecBuffer) {
24    s.fast_zeroize();
25    buf.fast_zeroize();
26}
27
28/// Cleanup function for decode errors. Marked #[cold] to keep it out of the hot path.
29#[cfg(feature = "zeroize")]
30#[cold]
31#[inline(never)]
32fn cleanup_decode_error(s: &mut String, buf: &mut &mut [u8]) {
33    // SAFETY: We're zeroizing and then clearing, so UTF-8 invariant is restored
34    unsafe {
35        redoubt_util::fast_zeroize_slice(s.as_bytes_mut());
36    }
37    s.clear();
38    redoubt_util::fast_zeroize_slice(buf);
39}
40
41#[inline(always)]
42pub(crate) fn string_bytes_required(len: usize) -> Result<usize, OverflowError> {
43    let bytes_required = header_size().wrapping_add(len);
44
45    if bytes_required < header_size() {
46        return Err(OverflowError {
47            reason: "String bytes_required overflow".into(),
48        });
49    }
50
51    Ok(bytes_required)
52}
53
54impl BytesRequired for String {
55    fn encode_bytes_required(&self) -> Result<usize, OverflowError> {
56        string_bytes_required(self.len())
57    }
58}
59
60impl TryEncode for String {
61    fn try_encode_into(&mut self, buf: &mut RedoubtCodecBuffer) -> Result<(), EncodeError> {
62        let mut bytes_required = Zeroizing::from(&mut self.encode_bytes_required()?);
63        let mut size = Zeroizing::from(&mut self.len());
64
65        write_header(buf, &mut size, &mut bytes_required)?;
66
67        let bytes = unsafe { self.as_bytes_mut() };
68        u8::encode_slice_into(bytes, buf)
69    }
70}
71
72impl Encode for String {
73    #[inline(always)]
74    fn encode_into(&mut self, buf: &mut RedoubtCodecBuffer) -> Result<(), EncodeError> {
75        let result = self.try_encode_into(buf);
76
77        #[cfg(feature = "zeroize")]
78        if result.is_err() {
79            cleanup_encode_error(self, buf);
80        } else {
81            self.fast_zeroize();
82            self.clear();
83        }
84
85        result
86    }
87}
88
89impl EncodeSlice for String {
90    fn encode_slice_into(
91        slice: &mut [Self],
92        buf: &mut RedoubtCodecBuffer,
93    ) -> Result<(), EncodeError> {
94        for elem in slice.iter_mut() {
95            elem.encode_into(buf)?;
96        }
97
98        Ok(())
99    }
100}
101
102impl TryDecode for String {
103    #[inline(always)]
104    fn try_decode_from(&mut self, buf: &mut &mut [u8]) -> Result<(), DecodeError> {
105        let mut size = Zeroizing::from(&mut 0usize);
106
107        process_header(buf, &mut size)?;
108
109        self.prealloc(*size);
110
111        // SAFETY: prealloc sets len, we decode into those bytes
112        let bytes = unsafe { self.as_bytes_mut() };
113        // Note: This error branch is unreachable since process_header already validates
114        // buffer length. We use `?` instead of expect/unwrap to keep the code panic-free.
115        u8::decode_slice_from(bytes, buf)?;
116
117        // Validate UTF-8
118        if core::str::from_utf8(self.as_bytes()).is_err() {
119            return Err(DecodeError::PreconditionViolated);
120        }
121
122        Ok(())
123    }
124}
125
126impl Decode for String {
127    fn decode_from(&mut self, buf: &mut &mut [u8]) -> Result<(), DecodeError> {
128        let result = self.try_decode_from(buf);
129
130        #[cfg(feature = "zeroize")]
131        if result.is_err() {
132            cleanup_decode_error(self, buf);
133        }
134
135        result
136    }
137}
138
139impl DecodeSlice for String {
140    fn decode_slice_from(slice: &mut [Self], buf: &mut &mut [u8]) -> Result<(), DecodeError> {
141        for elem in slice.iter_mut() {
142            elem.decode_from(buf)?;
143        }
144
145        Ok(())
146    }
147}
148
149impl PreAlloc for String {
150    const ZERO_INIT: bool = true;
151
152    fn prealloc(&mut self, size: usize) {
153        self.clear();
154        self.shrink_to_fit();
155        self.reserve_exact(size);
156
157        // SAFETY: We're setting len after reserving capacity
158        // The bytes will be written by decode before being read as UTF-8
159        unsafe {
160            let vec = self.as_mut_vec();
161            redoubt_util::fast_zeroize_vec(vec);
162            vec.set_len(size);
163        }
164    }
165}