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    /// Runs provider-specific preparation while retaining cleanup ownership.
139    ///
140    /// This is the typed counterpart to [`Self::prepare`]. The closure may
141    /// return a provider-specific prepared wrapper that borrows this guard.
142    /// Both complete buffers are cleared before the closure is entered.
143    pub fn prepare_with<'guard, T, E, F>(&'guard mut self, prepare: F) -> Result<T, E>
144    where
145        F: FnOnce(PreparationStorage<'guard>) -> Result<T, E>,
146    {
147        sanitize_bytes(self.target.as_mut_slice());
148        sanitize_bytes(self.body.as_mut_slice());
149        prepare(PreparationStorage::new(
150            self.target.as_mut_slice(),
151            self.body.as_mut_slice(),
152        ))
153    }
154
155    /// Returns capacities without exposing stored request bytes.
156    #[must_use]
157    pub fn capacities(&self) -> (usize, usize) {
158        (self.target.as_slice().len(), self.body.as_slice().len())
159    }
160}
161
162impl fmt::Debug for PreparationStorageGuard<'_> {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        let (target_capacity, body_capacity) = self.capacities();
165        formatter
166            .debug_struct("PreparationStorageGuard")
167            .field("target_capacity", &target_capacity)
168            .field("body_capacity", &body_capacity)
169            .finish_non_exhaustive()
170    }
171}
172
173/// Fallibly allocated preparation buffers cleared in full on drop.
174#[cfg(feature = "alloc")]
175pub struct OwnedPreparationStorage {
176    target: alloc::boxed::Box<[u8]>,
177    body: alloc::boxed::Box<[u8]>,
178}
179
180#[cfg(feature = "alloc")]
181impl OwnedPreparationStorage {
182    /// Allocates exactly one named profile without panicking on allocation
183    /// failure.
184    pub fn try_for_profile(
185        profile: PreparationCapacityProfile,
186    ) -> Result<Self, PreparationCapacityError> {
187        let target = allocate_zeroed(profile.target_bytes)?;
188        let body = allocate_zeroed(profile.body_bytes)?;
189        Ok(Self { target, body })
190    }
191
192    /// Borrows both owned buffers behind a cleanup guard.
193    pub fn guard(&mut self) -> PreparationStorageGuard<'_> {
194        PreparationStorageGuard::new(&mut self.target, &mut self.body)
195    }
196
197    /// Returns capacities without exposing stored request bytes.
198    #[must_use]
199    pub fn capacities(&self) -> (usize, usize) {
200        (self.target.len(), self.body.len())
201    }
202}
203
204#[cfg(feature = "alloc")]
205impl Drop for OwnedPreparationStorage {
206    fn drop(&mut self) {
207        cloud_sdk_sanitization::sanitize_bytes(&mut self.target);
208        cloud_sdk_sanitization::sanitize_bytes(&mut self.body);
209    }
210}
211
212#[cfg(feature = "alloc")]
213impl fmt::Debug for OwnedPreparationStorage {
214    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
215        let (target_capacity, body_capacity) = self.capacities();
216        formatter
217            .debug_struct("OwnedPreparationStorage")
218            .field("target_capacity", &target_capacity)
219            .field("body_capacity", &body_capacity)
220            .finish_non_exhaustive()
221    }
222}
223
224#[cfg(feature = "alloc")]
225fn allocate_zeroed(len: usize) -> Result<alloc::boxed::Box<[u8]>, PreparationCapacityError> {
226    let mut bytes = alloc::vec::Vec::new();
227    bytes
228        .try_reserve_exact(len)
229        .map_err(|_| PreparationCapacityError::AllocationFailed)?;
230    bytes.resize(len, 0);
231    Ok(bytes.into_boxed_slice())
232}
233
234#[cfg(test)]
235mod tests {
236    use crate::operation::{PreparationStorage, PrepareOperation, PreparedRequest};
237
238    use super::{PreparationCapacityError, PreparationCapacityProfile, PreparationStorageGuard};
239
240    struct ContaminateStorage;
241
242    impl PrepareOperation for ContaminateStorage {
243        type Error = ();
244
245        fn prepare<'storage>(
246            &self,
247            storage: PreparationStorage<'storage>,
248        ) -> Result<PreparedRequest<'storage>, Self::Error> {
249            let (target, body) = storage.into_parts();
250            target.fill(0xA5);
251            body.fill(0x5A);
252            Err(())
253        }
254    }
255
256    struct AssertCleared;
257
258    impl PrepareOperation for AssertCleared {
259        type Error = ();
260
261        fn prepare<'storage>(
262            &self,
263            storage: PreparationStorage<'storage>,
264        ) -> Result<PreparedRequest<'storage>, Self::Error> {
265            let (target, body) = storage.into_parts();
266            assert!(target.iter().all(|byte| *byte == 0));
267            assert!(body.iter().all(|byte| *byte == 0));
268            Err(())
269        }
270    }
271
272    #[test]
273    fn profiles_are_bounded_and_validate_both_regions() {
274        assert_eq!(
275            PreparationCapacityProfile::DEFAULT.target_bytes(),
276            crate::transport::MAX_REQUEST_TARGET_BYTES
277        );
278        assert_eq!(
279            PreparationCapacityProfile::DEFAULT.validate(8191, usize::MAX),
280            Err(PreparationCapacityError::TargetTooSmall)
281        );
282        assert_eq!(
283            PreparationCapacityProfile::DEFAULT.validate(8192, 1024),
284            Err(PreparationCapacityError::BodyTooSmall)
285        );
286    }
287
288    #[test]
289    fn borrowed_guard_clears_both_complete_buffers() {
290        let mut target = [0xA5_u8; 8];
291        let mut body = [0x5A_u8; 16];
292        {
293            let guard = PreparationStorageGuard::new(&mut target, &mut body);
294            assert_eq!(guard.capacities(), (8, 16));
295        }
296        assert_eq!(target, [0; 8]);
297        assert_eq!(body, [0; 16]);
298    }
299
300    #[test]
301    fn every_preparation_attempt_clears_complete_reused_storage_first() {
302        let mut target = [0_u8; 8];
303        let mut body = [0_u8; 16];
304        let mut guard = PreparationStorageGuard::new(&mut target, &mut body);
305
306        assert!(matches!(guard.prepare(&ContaminateStorage), Err(())));
307        assert!(matches!(guard.prepare(&AssertCleared), Err(())));
308    }
309
310    #[test]
311    fn provider_specific_preparation_retains_cleanup_ownership() {
312        let mut target = [0xA5_u8; 8];
313        let mut body = [0x5A_u8; 16];
314        {
315            let mut guard = PreparationStorageGuard::new(&mut target, &mut body);
316            let result: Result<(), ()> = guard.prepare_with(|storage| {
317                let (target, body) = storage.into_parts();
318                assert!(target.iter().all(|byte| *byte == 0));
319                assert!(body.iter().all(|byte| *byte == 0));
320                target.fill(0x11);
321                body.fill(0x22);
322                Err(())
323            });
324            assert_eq!(result, Err(()));
325        }
326        assert_eq!(target, [0; 8]);
327        assert_eq!(body, [0; 16]);
328    }
329
330    #[test]
331    fn profile_validation_failures_clear_both_complete_buffers() {
332        let mut short_target = [0xA5_u8; 8];
333        let mut target_failure_body = [0x5A_u8; 16];
334        assert!(matches!(
335            PreparationStorageGuard::for_profile(
336                &mut short_target,
337                &mut target_failure_body,
338                PreparationCapacityProfile::DEFAULT,
339            ),
340            Err(PreparationCapacityError::TargetTooSmall)
341        ));
342        assert_eq!(short_target, [0; 8]);
343        assert_eq!(target_failure_body, [0; 16]);
344
345        let mut target = [0xA5_u8; crate::transport::MAX_REQUEST_TARGET_BYTES];
346        let mut short_body = [0x5A_u8; 16];
347        assert!(matches!(
348            PreparationStorageGuard::for_profile(
349                &mut target,
350                &mut short_body,
351                PreparationCapacityProfile::DEFAULT,
352            ),
353            Err(PreparationCapacityError::BodyTooSmall)
354        ));
355        assert!(target.iter().all(|byte| *byte == 0));
356        assert_eq!(short_body, [0; 16]);
357    }
358
359    #[cfg(feature = "alloc")]
360    #[test]
361    fn owned_profiles_allocate_exact_bounded_regions() {
362        let storage =
363            super::OwnedPreparationStorage::try_for_profile(PreparationCapacityProfile::EMBEDDED);
364        assert!(storage.is_ok());
365        assert_eq!(
366            storage.map(|storage| storage.capacities()),
367            Ok((1024, super::EMBEDDED_BODY_BYTES))
368        );
369    }
370
371    #[cfg(feature = "alloc")]
372    #[test]
373    fn impossible_allocation_fails_without_panicking() {
374        assert_eq!(
375            super::allocate_zeroed(usize::MAX),
376            Err(PreparationCapacityError::AllocationFailed)
377        );
378    }
379}