Skip to main content

cloud_sdk/
buffer.rs

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