Skip to main content

cloud_sdk/client/
profile.rs

1//! Named capacities for complete client request workspaces.
2
3#[cfg(feature = "alloc")]
4use core::fmt;
5
6use super::ClientWorkspace;
7use crate::operation::{DEFAULT_BODY_BYTES, EMBEDDED_BODY_BYTES, LARGE_BODY_BYTES};
8use crate::transport::{MAX_REQUEST_TARGET_BYTES, MAX_RESPONSE_HEADER_BYTES};
9
10/// Embedded response-body capacity in bytes.
11pub const EMBEDDED_RESPONSE_BYTES: usize = 64 * 1024;
12/// Default response-body capacity in bytes.
13pub const DEFAULT_RESPONSE_BYTES: usize = 1024 * 1024;
14/// Large response-body capacity in bytes.
15pub const LARGE_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
16
17/// Named capacities for all storage used by one complete client execution.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct ClientCapacityProfile {
20    target_bytes: usize,
21    request_body_bytes: usize,
22    response_body_bytes: usize,
23    response_header_bytes: usize,
24}
25
26impl ClientCapacityProfile {
27    /// Small profile for constrained clients and ordinary JSON requests.
28    pub const EMBEDDED: Self = Self::new(
29        1024,
30        EMBEDDED_BODY_BYTES,
31        EMBEDDED_RESPONSE_BYTES,
32        MAX_RESPONSE_HEADER_BYTES,
33    );
34    /// General profile supporting the complete request-target limit.
35    pub const DEFAULT: Self = Self::new(
36        MAX_REQUEST_TARGET_BYTES,
37        DEFAULT_BODY_BYTES,
38        DEFAULT_RESPONSE_BYTES,
39        MAX_RESPONSE_HEADER_BYTES,
40    );
41    /// Explicit large-payload profile bounded at eight MiB per body.
42    pub const LARGE: Self = Self::new(
43        MAX_REQUEST_TARGET_BYTES,
44        LARGE_BODY_BYTES,
45        LARGE_RESPONSE_BYTES,
46        MAX_RESPONSE_HEADER_BYTES,
47    );
48
49    const fn new(
50        target_bytes: usize,
51        request_body_bytes: usize,
52        response_body_bytes: usize,
53        response_header_bytes: usize,
54    ) -> Self {
55        Self {
56            target_bytes,
57            request_body_bytes,
58            response_body_bytes,
59            response_header_bytes,
60        }
61    }
62
63    /// Returns the required request-target capacity.
64    #[must_use]
65    pub const fn target_bytes(self) -> usize {
66        self.target_bytes
67    }
68
69    /// Returns the required request-body capacity.
70    #[must_use]
71    pub const fn request_body_bytes(self) -> usize {
72        self.request_body_bytes
73    }
74
75    /// Returns the required response-body capacity.
76    #[must_use]
77    pub const fn response_body_bytes(self) -> usize {
78        self.response_body_bytes
79    }
80
81    /// Returns the required response-header capacity.
82    #[must_use]
83    pub const fn response_header_bytes(self) -> usize {
84        self.response_header_bytes
85    }
86
87    /// Checks whether all four independent regions satisfy this profile.
88    pub const fn validate(
89        self,
90        target_bytes: usize,
91        request_body_bytes: usize,
92        response_body_bytes: usize,
93        response_header_bytes: usize,
94    ) -> Result<(), ClientCapacityError> {
95        if target_bytes < self.target_bytes {
96            return Err(ClientCapacityError::TargetTooSmall);
97        }
98        if request_body_bytes < self.request_body_bytes {
99            return Err(ClientCapacityError::RequestBodyTooSmall);
100        }
101        if response_body_bytes < self.response_body_bytes {
102            return Err(ClientCapacityError::ResponseBodyTooSmall);
103        }
104        if response_header_bytes < self.response_header_bytes {
105            return Err(ClientCapacityError::ResponseHeadersTooSmall);
106        }
107        Ok(())
108    }
109}
110
111/// Failure while admitting or allocating a complete client workspace.
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub enum ClientCapacityError {
114    /// Request-target storage does not satisfy the selected profile.
115    TargetTooSmall,
116    /// Request-body storage does not satisfy the selected profile.
117    RequestBodyTooSmall,
118    /// Response-body storage does not satisfy the selected profile.
119    ResponseBodyTooSmall,
120    /// Response-header storage does not satisfy the selected profile.
121    ResponseHeadersTooSmall,
122    /// The allocator rejected the requested bounded profile.
123    AllocationFailed,
124}
125
126impl_static_error!(ClientCapacityError,
127    Self::TargetTooSmall => "client target storage is too small",
128    Self::RequestBodyTooSmall => "client request-body storage is too small",
129    Self::ResponseBodyTooSmall => "client response-body storage is too small",
130    Self::ResponseHeadersTooSmall => "client response-header storage is too small",
131    Self::AllocationFailed => "client workspace allocation failed",
132);
133
134impl<'storage> ClientWorkspace<'storage> {
135    /// Clears and admits four caller-owned regions under one named profile.
136    pub fn for_profile(
137        target: &'storage mut [u8],
138        request_body: &'storage mut [u8],
139        response_body: &'storage mut [u8],
140        response_headers: &'storage mut [u8],
141        profile: ClientCapacityProfile,
142    ) -> Result<Self, ClientCapacityError> {
143        let workspace = Self::new(target, request_body, response_body, response_headers);
144        let (target, request, response, headers) = workspace.capacities();
145        profile.validate(target, request, response, headers)?;
146        Ok(workspace)
147    }
148}
149
150/// Fallibly allocated complete workspace cleared in full on drop.
151#[cfg(feature = "alloc")]
152pub struct OwnedClientWorkspace {
153    target: alloc::boxed::Box<[u8]>,
154    request_body: alloc::boxed::Box<[u8]>,
155    response_body: alloc::boxed::Box<[u8]>,
156    response_headers: alloc::boxed::Box<[u8]>,
157}
158
159#[cfg(feature = "alloc")]
160impl OwnedClientWorkspace {
161    /// Allocates exactly one named profile without panicking on allocation failure.
162    pub fn try_for_profile(profile: ClientCapacityProfile) -> Result<Self, ClientCapacityError> {
163        Ok(Self {
164            target: allocate_zeroed(profile.target_bytes)?,
165            request_body: allocate_zeroed(profile.request_body_bytes)?,
166            response_body: allocate_zeroed(profile.response_body_bytes)?,
167            response_headers: allocate_zeroed(profile.response_header_bytes)?,
168        })
169    }
170
171    /// Borrows all four allocations as one cleanup-owning workspace.
172    pub fn workspace(&mut self) -> ClientWorkspace<'_> {
173        ClientWorkspace::new(
174            &mut self.target,
175            &mut self.request_body,
176            &mut self.response_body,
177            &mut self.response_headers,
178        )
179    }
180
181    /// Returns capacities without exposing stored bytes.
182    #[must_use]
183    pub fn capacities(&self) -> (usize, usize, usize, usize) {
184        (
185            self.target.len(),
186            self.request_body.len(),
187            self.response_body.len(),
188            self.response_headers.len(),
189        )
190    }
191}
192
193#[cfg(feature = "alloc")]
194impl Drop for OwnedClientWorkspace {
195    fn drop(&mut self) {
196        cloud_sdk_sanitization::sanitize_bytes(&mut self.target);
197        cloud_sdk_sanitization::sanitize_bytes(&mut self.request_body);
198        cloud_sdk_sanitization::sanitize_bytes(&mut self.response_body);
199        cloud_sdk_sanitization::sanitize_bytes(&mut self.response_headers);
200    }
201}
202
203#[cfg(feature = "alloc")]
204impl fmt::Debug for OwnedClientWorkspace {
205    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206        formatter
207            .debug_struct("OwnedClientWorkspace")
208            .field("capacities", &self.capacities())
209            .finish_non_exhaustive()
210    }
211}
212
213#[cfg(feature = "alloc")]
214fn allocate_zeroed(len: usize) -> Result<alloc::boxed::Box<[u8]>, ClientCapacityError> {
215    let mut bytes = alloc::vec::Vec::new();
216    bytes
217        .try_reserve_exact(len)
218        .map_err(|_| ClientCapacityError::AllocationFailed)?;
219    bytes.resize(len, 0);
220    Ok(bytes.into_boxed_slice())
221}
222
223#[cfg(test)]
224mod tests {
225    use super::{ClientCapacityError, ClientCapacityProfile};
226    use crate::client::ClientWorkspace;
227
228    #[test]
229    fn profiles_validate_every_region_at_exact_bounds() {
230        let profile = ClientCapacityProfile::DEFAULT;
231        assert_eq!(
232            profile.validate(
233                profile.target_bytes(),
234                profile.request_body_bytes(),
235                profile.response_body_bytes(),
236                profile.response_header_bytes(),
237            ),
238            Ok(())
239        );
240        assert_eq!(
241            profile.validate(
242                profile.target_bytes(),
243                profile.request_body_bytes(),
244                profile.response_body_bytes() - 1,
245                usize::MAX,
246            ),
247            Err(ClientCapacityError::ResponseBodyTooSmall)
248        );
249        assert_eq!(
250            profile.validate(usize::MAX, usize::MAX, usize::MAX, 0),
251            Err(ClientCapacityError::ResponseHeadersTooSmall)
252        );
253    }
254
255    #[test]
256    fn rejected_profile_clears_all_borrowed_regions() {
257        let mut target = [0x11; 8];
258        let mut request = [0x22; 8];
259        let mut response = [0x33; 8];
260        let mut headers = [0x44; 8];
261        assert!(matches!(
262            ClientWorkspace::for_profile(
263                &mut target,
264                &mut request,
265                &mut response,
266                &mut headers,
267                ClientCapacityProfile::DEFAULT,
268            ),
269            Err(ClientCapacityError::TargetTooSmall)
270        ));
271        assert_eq!(target, [0; 8]);
272        assert_eq!(request, [0; 8]);
273        assert_eq!(response, [0; 8]);
274        assert_eq!(headers, [0; 8]);
275    }
276
277    #[cfg(feature = "alloc")]
278    #[test]
279    fn owned_workspace_allocates_the_exact_selected_profile() {
280        let profile = ClientCapacityProfile::EMBEDDED;
281        let workspace = super::OwnedClientWorkspace::try_for_profile(profile);
282        assert!(workspace.is_ok());
283        let Ok(workspace) = workspace else {
284            unreachable!("bounded client workspace allocation failed")
285        };
286        assert_eq!(
287            workspace.capacities(),
288            (
289                profile.target_bytes(),
290                profile.request_body_bytes(),
291                profile.response_body_bytes(),
292                profile.response_header_bytes(),
293            )
294        );
295    }
296}