Skip to main content

cloud_sdk/operation/
storage.rs

1//! Request preparation capacity profiles and cleanup guards.
2
3use core::fmt;
4
5use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes};
6
7use super::{PreparationStorage, PrepareOperation, PreparedRequest};
8use crate::transport::MAX_REQUEST_TARGET_BYTES;
9
10/// Embedded request-body capacity in bytes.
11pub const EMBEDDED_BODY_BYTES: usize = 16 * 1024;
12/// Default request-body capacity in bytes.
13pub const DEFAULT_BODY_BYTES: usize = 1024 * 1024;
14/// Large request-body capacity in bytes.
15pub const LARGE_BODY_BYTES: usize = 8 * 1024 * 1024;
16
17/// Named, bounded storage capacities for request preparation.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct PreparationCapacityProfile {
20    target_bytes: usize,
21    body_bytes: usize,
22}
23
24impl PreparationCapacityProfile {
25    /// Small profile for constrained devices and ordinary JSON mutations.
26    pub const EMBEDDED: Self = Self::new(1024, EMBEDDED_BODY_BYTES);
27    /// General profile supporting the complete request-target limit.
28    pub const DEFAULT: Self = Self::new(MAX_REQUEST_TARGET_BYTES, DEFAULT_BODY_BYTES);
29    /// Large profile for explicitly admitted bulk request bodies.
30    pub const LARGE: Self = Self::new(MAX_REQUEST_TARGET_BYTES, LARGE_BODY_BYTES);
31
32    const fn new(target_bytes: usize, body_bytes: usize) -> Self {
33        Self {
34            target_bytes,
35            body_bytes,
36        }
37    }
38
39    /// Returns the required request-target capacity.
40    #[must_use]
41    pub const fn target_bytes(self) -> usize {
42        self.target_bytes
43    }
44
45    /// Returns the required request-body capacity.
46    #[must_use]
47    pub const fn body_bytes(self) -> usize {
48        self.body_bytes
49    }
50
51    /// Checks whether two buffers satisfy this profile.
52    pub const fn validate(
53        self,
54        target_bytes: usize,
55        body_bytes: usize,
56    ) -> Result<(), PreparationCapacityError> {
57        if target_bytes < self.target_bytes {
58            return Err(PreparationCapacityError::TargetTooSmall);
59        }
60        if body_bytes < self.body_bytes {
61            return Err(PreparationCapacityError::BodyTooSmall);
62        }
63        Ok(())
64    }
65}
66
67/// Failure while selecting or allocating preparation storage.
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum PreparationCapacityError {
70    /// Request-target storage does not satisfy the selected profile.
71    TargetTooSmall,
72    /// Request-body storage does not satisfy the selected profile.
73    BodyTooSmall,
74    /// The allocator rejected the requested bounded profile.
75    AllocationFailed,
76}
77
78impl_static_error!(PreparationCapacityError,
79    Self::TargetTooSmall => "preparation target storage is too small",
80    Self::BodyTooSmall => "preparation body storage is too small",
81    Self::AllocationFailed => "preparation storage allocation failed",
82);
83
84/// Caller-owned preparation buffers that are cleared together on drop.
85///
86/// A prepared request borrows this guard through [`Self::prepare`], so safe
87/// Rust prevents the guard from being dropped before transport use completes.
88/// Each preparation attempt first volatile-clears both complete borrowed
89/// buffers, including residue from a previous request. Dropping the guard
90/// performs the same complete cleanup.
91pub struct PreparationStorageGuard<'storage> {
92    target: SecretBuffer<'storage>,
93    body: SecretBuffer<'storage>,
94}
95
96impl<'storage> PreparationStorageGuard<'storage> {
97    /// Guards two independent caller-owned buffers.
98    #[must_use]
99    pub const fn new(target: &'storage mut [u8], body: &'storage mut [u8]) -> Self {
100        Self {
101            target: SecretBuffer::new(target),
102            body: SecretBuffer::new(body),
103        }
104    }
105
106    /// Guards buffers after validating one named capacity profile.
107    pub fn for_profile(
108        target: &'storage mut [u8],
109        body: &'storage mut [u8],
110        profile: PreparationCapacityProfile,
111    ) -> Result<Self, PreparationCapacityError> {
112        let guard = Self::new(target, body);
113        let (target_bytes, body_bytes) = guard.capacities();
114        profile.validate(target_bytes, body_bytes)?;
115        Ok(guard)
116    }
117
118    /// Prepares one operation while retaining cleanup ownership.
119    ///
120    /// Both complete buffers are volatile-cleared before each attempt. Reusing
121    /// one guard therefore does not retain bytes from an earlier request in an
122    /// unused tail.
123    pub fn prepare<'guard, O>(
124        &'guard mut self,
125        operation: &O,
126    ) -> Result<PreparedRequest<'guard>, O::Error>
127    where
128        O: PrepareOperation,
129    {
130        sanitize_bytes(self.target.as_mut_slice());
131        sanitize_bytes(self.body.as_mut_slice());
132        operation.prepare(PreparationStorage::new(
133            self.target.as_mut_slice(),
134            self.body.as_mut_slice(),
135        ))
136    }
137
138    /// Returns capacities without exposing stored request bytes.
139    #[must_use]
140    pub fn capacities(&self) -> (usize, usize) {
141        (self.target.as_slice().len(), self.body.as_slice().len())
142    }
143}
144
145impl fmt::Debug for PreparationStorageGuard<'_> {
146    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
147        let (target_capacity, body_capacity) = self.capacities();
148        formatter
149            .debug_struct("PreparationStorageGuard")
150            .field("target_capacity", &target_capacity)
151            .field("body_capacity", &body_capacity)
152            .finish_non_exhaustive()
153    }
154}
155
156/// Fallibly allocated preparation buffers cleared in full on drop.
157#[cfg(feature = "alloc")]
158pub struct OwnedPreparationStorage {
159    target: alloc::boxed::Box<[u8]>,
160    body: alloc::boxed::Box<[u8]>,
161}
162
163#[cfg(feature = "alloc")]
164impl OwnedPreparationStorage {
165    /// Allocates exactly one named profile without panicking on allocation
166    /// failure.
167    pub fn try_for_profile(
168        profile: PreparationCapacityProfile,
169    ) -> Result<Self, PreparationCapacityError> {
170        let target = allocate_zeroed(profile.target_bytes)?;
171        let body = allocate_zeroed(profile.body_bytes)?;
172        Ok(Self { target, body })
173    }
174
175    /// Borrows both owned buffers behind a cleanup guard.
176    pub fn guard(&mut self) -> PreparationStorageGuard<'_> {
177        PreparationStorageGuard::new(&mut self.target, &mut self.body)
178    }
179
180    /// Returns capacities without exposing stored request bytes.
181    #[must_use]
182    pub fn capacities(&self) -> (usize, usize) {
183        (self.target.len(), self.body.len())
184    }
185}
186
187#[cfg(feature = "alloc")]
188impl Drop for OwnedPreparationStorage {
189    fn drop(&mut self) {
190        cloud_sdk_sanitization::sanitize_bytes(&mut self.target);
191        cloud_sdk_sanitization::sanitize_bytes(&mut self.body);
192    }
193}
194
195#[cfg(feature = "alloc")]
196impl fmt::Debug for OwnedPreparationStorage {
197    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
198        let (target_capacity, body_capacity) = self.capacities();
199        formatter
200            .debug_struct("OwnedPreparationStorage")
201            .field("target_capacity", &target_capacity)
202            .field("body_capacity", &body_capacity)
203            .finish_non_exhaustive()
204    }
205}
206
207#[cfg(feature = "alloc")]
208fn allocate_zeroed(len: usize) -> Result<alloc::boxed::Box<[u8]>, PreparationCapacityError> {
209    let mut bytes = alloc::vec::Vec::new();
210    bytes
211        .try_reserve_exact(len)
212        .map_err(|_| PreparationCapacityError::AllocationFailed)?;
213    bytes.resize(len, 0);
214    Ok(bytes.into_boxed_slice())
215}
216
217#[cfg(test)]
218mod tests {
219    use crate::operation::{PreparationStorage, PrepareOperation, PreparedRequest};
220
221    use super::{PreparationCapacityError, PreparationCapacityProfile, PreparationStorageGuard};
222
223    struct ContaminateStorage;
224
225    impl PrepareOperation for ContaminateStorage {
226        type Error = ();
227
228        fn prepare<'storage>(
229            &self,
230            storage: PreparationStorage<'storage>,
231        ) -> Result<PreparedRequest<'storage>, Self::Error> {
232            let (target, body) = storage.into_parts();
233            target.fill(0xA5);
234            body.fill(0x5A);
235            Err(())
236        }
237    }
238
239    struct AssertCleared;
240
241    impl PrepareOperation for AssertCleared {
242        type Error = ();
243
244        fn prepare<'storage>(
245            &self,
246            storage: PreparationStorage<'storage>,
247        ) -> Result<PreparedRequest<'storage>, Self::Error> {
248            let (target, body) = storage.into_parts();
249            assert!(target.iter().all(|byte| *byte == 0));
250            assert!(body.iter().all(|byte| *byte == 0));
251            Err(())
252        }
253    }
254
255    #[test]
256    fn profiles_are_bounded_and_validate_both_regions() {
257        assert_eq!(
258            PreparationCapacityProfile::DEFAULT.target_bytes(),
259            crate::transport::MAX_REQUEST_TARGET_BYTES
260        );
261        assert_eq!(
262            PreparationCapacityProfile::DEFAULT.validate(8191, usize::MAX),
263            Err(PreparationCapacityError::TargetTooSmall)
264        );
265        assert_eq!(
266            PreparationCapacityProfile::DEFAULT.validate(8192, 1024),
267            Err(PreparationCapacityError::BodyTooSmall)
268        );
269    }
270
271    #[test]
272    fn borrowed_guard_clears_both_complete_buffers() {
273        let mut target = [0xA5_u8; 8];
274        let mut body = [0x5A_u8; 16];
275        {
276            let guard = PreparationStorageGuard::new(&mut target, &mut body);
277            assert_eq!(guard.capacities(), (8, 16));
278        }
279        assert_eq!(target, [0; 8]);
280        assert_eq!(body, [0; 16]);
281    }
282
283    #[test]
284    fn every_preparation_attempt_clears_complete_reused_storage_first() {
285        let mut target = [0_u8; 8];
286        let mut body = [0_u8; 16];
287        let mut guard = PreparationStorageGuard::new(&mut target, &mut body);
288
289        assert!(matches!(guard.prepare(&ContaminateStorage), Err(())));
290        assert!(matches!(guard.prepare(&AssertCleared), Err(())));
291    }
292
293    #[test]
294    fn profile_validation_failures_clear_both_complete_buffers() {
295        let mut short_target = [0xA5_u8; 8];
296        let mut target_failure_body = [0x5A_u8; 16];
297        assert!(matches!(
298            PreparationStorageGuard::for_profile(
299                &mut short_target,
300                &mut target_failure_body,
301                PreparationCapacityProfile::DEFAULT,
302            ),
303            Err(PreparationCapacityError::TargetTooSmall)
304        ));
305        assert_eq!(short_target, [0; 8]);
306        assert_eq!(target_failure_body, [0; 16]);
307
308        let mut target = [0xA5_u8; crate::transport::MAX_REQUEST_TARGET_BYTES];
309        let mut short_body = [0x5A_u8; 16];
310        assert!(matches!(
311            PreparationStorageGuard::for_profile(
312                &mut target,
313                &mut short_body,
314                PreparationCapacityProfile::DEFAULT,
315            ),
316            Err(PreparationCapacityError::BodyTooSmall)
317        ));
318        assert!(target.iter().all(|byte| *byte == 0));
319        assert_eq!(short_body, [0; 16]);
320    }
321
322    #[cfg(feature = "alloc")]
323    #[test]
324    fn owned_profiles_allocate_exact_bounded_regions() {
325        let storage =
326            super::OwnedPreparationStorage::try_for_profile(PreparationCapacityProfile::EMBEDDED);
327        assert!(storage.is_ok());
328        assert_eq!(
329            storage.map(|storage| storage.capacities()),
330            Ok((1024, super::EMBEDDED_BODY_BYTES))
331        );
332    }
333
334    #[cfg(feature = "alloc")]
335    #[test]
336    fn impossible_allocation_fails_without_panicking() {
337        assert_eq!(
338            super::allocate_zeroed(usize::MAX),
339            Err(PreparationCapacityError::AllocationFailed)
340        );
341    }
342}