Skip to main content

cloud_sdk/
buffer.rs

1//! no_std fixed-buffer writing helpers for provider crates.
2
3mod encoder;
4
5pub use cloud_sdk_sanitization::sanitize_bytes;
6pub use encoder::{
7    SnapshotEncoder, encode_snapshot, encode_snapshot_bounded, measure_snapshot,
8    measure_snapshot_bounded,
9};
10
11/// Returns the base-10 encoded length of an unsigned integer.
12#[must_use]
13pub const fn u64_encoded_len(mut value: u64) -> usize {
14    let mut len = 1_usize;
15    while value >= 10 {
16        value /= 10;
17        len = len.saturating_add(1);
18    }
19    len
20}
21
22/// Returns the checked percent-encoded length of one component.
23pub fn percent_encoded_len<E: Copy>(value: &str, error: E) -> Result<usize, E> {
24    let mut len = 0_usize;
25    for byte in value.bytes() {
26        let width = if is_unreserved(byte) { 1 } else { 3 };
27        len = len.checked_add(width).ok_or(error)?;
28    }
29    Ok(len)
30}
31
32/// Returns the checked length of JSON string contents after escaping.
33pub fn json_string_escaped_len<E: Copy>(value: &str, error: E) -> Result<usize, E> {
34    json_escaped_len(value, error)
35}
36
37/// Returns the checked encoded length of a complete JSON string.
38pub fn json_string_len<E: Copy>(value: &str, error: E) -> Result<usize, E> {
39    json_escaped_len(value, error)?.checked_add(2).ok_or(error)
40}
41
42/// Writes one byte into a caller-owned buffer.
43pub fn write_byte<E: Copy>(
44    output: &mut [u8],
45    len: &mut usize,
46    byte: u8,
47    error: E,
48) -> Result<(), E> {
49    let next = len.checked_add(1).ok_or(error)?;
50    let slot = output.get_mut(*len).ok_or(error)?;
51    *slot = byte;
52    *len = next;
53    Ok(())
54}
55
56/// Writes a string into a caller-owned buffer without escaping.
57pub fn write_str<E: Copy>(
58    output: &mut [u8],
59    len: &mut usize,
60    value: &str,
61    error: E,
62) -> Result<(), E> {
63    let end = len.checked_add(value.len()).ok_or(error)?;
64    let target = output.get_mut(*len..end).ok_or(error)?;
65    target.copy_from_slice(value.as_bytes());
66    *len = end;
67    Ok(())
68}
69
70/// Writes a base-10 unsigned integer into a caller-owned buffer.
71pub fn write_u64<E: Copy>(
72    output: &mut [u8],
73    len: &mut usize,
74    mut value: u64,
75    error: E,
76) -> Result<(), E> {
77    let required = u64_encoded_len(value);
78    ensure_capacity(output, *len, required, error)?;
79    if value == 0 {
80        return write_byte(output, len, b'0', error);
81    }
82
83    let mut digits = [0u8; 20];
84    let mut cursor = digits.len();
85    while value != 0 {
86        cursor = cursor.checked_sub(1).ok_or(error)?;
87        let digit = u8::try_from(value % 10).map_err(|_| error)?;
88        let slot = digits.get_mut(cursor).ok_or(error)?;
89        *slot = b'0'.checked_add(digit).ok_or(error)?;
90        value /= 10;
91    }
92
93    let encoded = digits.get(cursor..).ok_or(error)?;
94    for byte in encoded {
95        write_byte(output, len, *byte, error)?;
96    }
97    Ok(())
98}
99
100/// Writes a percent-encoded query component into a caller-owned buffer.
101pub fn write_percent_encoded<E: Copy>(
102    output: &mut [u8],
103    len: &mut usize,
104    value: &str,
105    error: E,
106) -> Result<(), E> {
107    let required = percent_encoded_len(value, error)?;
108    ensure_capacity(output, *len, required, error)?;
109    for byte in value.bytes() {
110        if is_unreserved(byte) {
111            write_byte(output, len, byte, error)?;
112        } else {
113            write_byte(output, len, b'%', error)?;
114            write_byte(output, len, hex_digit(byte >> 4), error)?;
115            write_byte(output, len, hex_digit(byte & 0x0f), error)?;
116        }
117    }
118    Ok(())
119}
120
121/// Writes JSON-string contents with required escaping, without surrounding quotes.
122pub fn write_json_string_escaped<E: Copy>(
123    output: &mut [u8],
124    len: &mut usize,
125    value: &str,
126    error: E,
127) -> Result<(), E> {
128    let required = json_escaped_len(value, error)?;
129    ensure_capacity(output, *len, required, error)?;
130    for byte in value.bytes() {
131        match byte {
132            b'"' => write_str(output, len, "\\\"", error)?,
133            b'\\' => write_str(output, len, "\\\\", error)?,
134            b'\n' => write_str(output, len, "\\n", error)?,
135            b'\r' => write_str(output, len, "\\r", error)?,
136            b'\t' => write_str(output, len, "\\t", error)?,
137            0x00..=0x1f => {
138                write_str(output, len, "\\u00", error)?;
139                write_byte(output, len, hex_digit(byte >> 4), error)?;
140                write_byte(output, len, hex_digit(byte & 0x0f), error)?;
141            }
142            _ => write_byte(output, len, byte, error)?,
143        }
144    }
145    Ok(())
146}
147
148/// Writes a complete JSON string with surrounding quotes.
149pub fn write_json_string<E: Copy>(
150    output: &mut [u8],
151    len: &mut usize,
152    value: &str,
153    error: E,
154) -> Result<(), E> {
155    let required = json_string_len(value, error)?;
156    ensure_capacity(output, *len, required, error)?;
157    write_byte(output, len, b'"', error)?;
158    write_json_string_escaped(output, len, value, error)?;
159    write_byte(output, len, b'"', error)
160}
161
162fn json_escaped_len<E: Copy>(value: &str, error: E) -> Result<usize, E> {
163    let mut len = 0_usize;
164    for byte in value.bytes() {
165        let encoded = match byte {
166            b'"' | b'\\' | b'\n' | b'\r' | b'\t' => 2,
167            0x00..=0x1f => 6,
168            _ => 1,
169        };
170        len = len.checked_add(encoded).ok_or(error)?;
171    }
172    Ok(len)
173}
174
175fn ensure_capacity<E: Copy>(
176    output: &[u8],
177    len: usize,
178    additional: usize,
179    error: E,
180) -> Result<(), E> {
181    let end = len.checked_add(additional).ok_or(error)?;
182    output.get(len..end).ok_or(error)?;
183    Ok(())
184}
185
186/// Writes `&` unless this is the first query pair.
187pub fn write_query_separator<E: Copy>(
188    output: &mut [u8],
189    len: &mut usize,
190    first: &mut bool,
191    error: E,
192) -> Result<(), E> {
193    if *first {
194        *first = false;
195        return Ok(());
196    }
197    write_byte(output, len, b'&', error)
198}
199
200/// Writes a percent-encoded query key/value pair.
201pub fn write_query_pair<E: Copy>(
202    output: &mut [u8],
203    len: &mut usize,
204    first: &mut bool,
205    key: &str,
206    value: &str,
207    error: E,
208) -> Result<(), E> {
209    write_query_separator(output, len, first, error)?;
210    write_percent_encoded(output, len, key, error)?;
211    write_byte(output, len, b'=', error)?;
212    write_percent_encoded(output, len, value, error)
213}
214
215/// Writes a percent-encoded query key and base-10 integer value.
216pub fn write_query_u64<E: Copy>(
217    output: &mut [u8],
218    len: &mut usize,
219    first: &mut bool,
220    key: &str,
221    value: u64,
222    error: E,
223) -> Result<(), E> {
224    write_query_separator(output, len, first, error)?;
225    write_percent_encoded(output, len, key, error)?;
226    write_byte(output, len, b'=', error)?;
227    write_u64(output, len, value, error)
228}
229
230pub(super) const fn is_unreserved(byte: u8) -> bool {
231    matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')
232}
233
234pub(super) const fn hex_digit(nibble: u8) -> u8 {
235    match nibble {
236        0 => b'0',
237        1 => b'1',
238        2 => b'2',
239        3 => b'3',
240        4 => b'4',
241        5 => b'5',
242        6 => b'6',
243        7 => b'7',
244        8 => b'8',
245        9 => b'9',
246        10 => b'A',
247        11 => b'B',
248        12 => b'C',
249        13 => b'D',
250        14 => b'E',
251        _ => b'F',
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::{
258        write_json_string, write_percent_encoded, write_query_pair, write_query_u64, write_str,
259        write_u64,
260    };
261
262    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
263    enum TestError {
264        TooSmall,
265    }
266
267    #[test]
268    fn writes_decimal_numbers_including_zero() {
269        let mut output = [0u8; 24];
270        let mut len = 0;
271        assert_eq!(
272            write_u64(&mut output, &mut len, 0, TestError::TooSmall),
273            Ok(())
274        );
275        assert_eq!(
276            write_str(&mut output, &mut len, ",", TestError::TooSmall),
277            Ok(())
278        );
279        assert_eq!(
280            write_u64(&mut output, &mut len, u64::MAX, TestError::TooSmall),
281            Ok(())
282        );
283        let written = output
284            .get(..len)
285            .and_then(|bytes| core::str::from_utf8(bytes).ok());
286        assert_eq!(written, Some("0,18446744073709551615"));
287    }
288
289    #[test]
290    fn reports_too_small_buffers() {
291        let mut output = [0u8; 2];
292        let mut len = 0;
293        assert_eq!(
294            write_u64(&mut output, &mut len, 100, TestError::TooSmall),
295            Err(TestError::TooSmall)
296        );
297    }
298
299    #[test]
300    fn writes_percent_encoded_components_and_pairs() {
301        let mut output = [0u8; 64];
302        let mut len = 0;
303        assert_eq!(
304            write_percent_encoded(&mut output, &mut len, "env=prod", TestError::TooSmall),
305            Ok(())
306        );
307        let encoded = output
308            .get(..len)
309            .and_then(|bytes| core::str::from_utf8(bytes).ok());
310        assert_eq!(encoded, Some("env%3Dprod"));
311
312        let mut output = [0u8; 64];
313        let mut len = 0;
314        let mut first = true;
315        assert_eq!(
316            write_query_pair(
317                &mut output,
318                &mut len,
319                &mut first,
320                "label_selector",
321                "env=prod",
322                TestError::TooSmall,
323            ),
324            Ok(())
325        );
326        assert_eq!(
327            write_query_u64(
328                &mut output,
329                &mut len,
330                &mut first,
331                "page",
332                0,
333                TestError::TooSmall
334            ),
335            Ok(())
336        );
337        let query = output
338            .get(..len)
339            .and_then(|bytes| core::str::from_utf8(bytes).ok());
340        assert_eq!(query, Some("label_selector=env%3Dprod&page=0"));
341    }
342
343    #[test]
344    fn writes_json_strings_with_required_escaping() {
345        let mut output = [0u8; 96];
346        let mut len = 0;
347        assert_eq!(
348            write_json_string(
349                &mut output,
350                &mut len,
351                "line\n\"quoted\"\\slash\t\u{001f}",
352                TestError::TooSmall,
353            ),
354            Ok(())
355        );
356        let json = output
357            .get(..len)
358            .and_then(|bytes| core::str::from_utf8(bytes).ok());
359        assert_eq!(json, Some("\"line\\n\\\"quoted\\\"\\\\slash\\t\\u001F\""));
360    }
361
362    #[test]
363    fn json_writes_are_atomic_when_capacity_is_insufficient() {
364        let mut output = [0xa5_u8; 7];
365        let original = output;
366        let mut len = 2;
367        assert_eq!(
368            write_json_string(
369                &mut output,
370                &mut len,
371                "token=classified",
372                TestError::TooSmall,
373            ),
374            Err(TestError::TooSmall)
375        );
376        assert_eq!(len, 2);
377        assert_eq!(output, original);
378    }
379
380    #[test]
381    fn json_writes_are_atomic_at_every_undersized_capacity() {
382        let value = "line\n\"quoted\"\\slash\t\u{001f}";
383        let mut complete = [0_u8; 96];
384        let mut complete_len = 0;
385        assert_eq!(
386            write_json_string(&mut complete, &mut complete_len, value, TestError::TooSmall,),
387            Ok(())
388        );
389
390        for capacity in 0..complete_len {
391            let mut output = [0xa5_u8; 96];
392            let original = output;
393            let mut len = 0;
394            assert_eq!(
395                write_json_string(
396                    output.get_mut(..capacity).unwrap_or_default(),
397                    &mut len,
398                    value,
399                    TestError::TooSmall,
400                ),
401                Err(TestError::TooSmall)
402            );
403            assert_eq!(len, 0);
404            assert_eq!(output, original);
405        }
406    }
407}