Skip to main content

cloud_sdk/
buffer.rs

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