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    for byte in value.bytes() {
84        match byte {
85            b'"' => write_str(output, len, "\\\"", error)?,
86            b'\\' => write_str(output, len, "\\\\", error)?,
87            b'\n' => write_str(output, len, "\\n", error)?,
88            b'\r' => write_str(output, len, "\\r", error)?,
89            b'\t' => write_str(output, len, "\\t", error)?,
90            0x00..=0x1f => {
91                write_str(output, len, "\\u00", error)?;
92                write_byte(output, len, hex_digit(byte >> 4), error)?;
93                write_byte(output, len, hex_digit(byte & 0x0f), error)?;
94            }
95            _ => write_byte(output, len, byte, error)?,
96        }
97    }
98    Ok(())
99}
100
101/// Writes a complete JSON string with surrounding quotes.
102pub fn write_json_string<E: Copy>(
103    output: &mut [u8],
104    len: &mut usize,
105    value: &str,
106    error: E,
107) -> Result<(), E> {
108    write_byte(output, len, b'"', error)?;
109    write_json_string_escaped(output, len, value, error)?;
110    write_byte(output, len, b'"', error)
111}
112
113/// Writes `&` unless this is the first query pair.
114pub fn write_query_separator<E: Copy>(
115    output: &mut [u8],
116    len: &mut usize,
117    first: &mut bool,
118    error: E,
119) -> Result<(), E> {
120    if *first {
121        *first = false;
122        return Ok(());
123    }
124    write_byte(output, len, b'&', error)
125}
126
127/// Writes a percent-encoded query key/value pair.
128pub fn write_query_pair<E: Copy>(
129    output: &mut [u8],
130    len: &mut usize,
131    first: &mut bool,
132    key: &str,
133    value: &str,
134    error: E,
135) -> Result<(), E> {
136    write_query_separator(output, len, first, error)?;
137    write_percent_encoded(output, len, key, error)?;
138    write_byte(output, len, b'=', error)?;
139    write_percent_encoded(output, len, value, error)
140}
141
142/// Writes a percent-encoded query key and base-10 integer value.
143pub fn write_query_u64<E: Copy>(
144    output: &mut [u8],
145    len: &mut usize,
146    first: &mut bool,
147    key: &str,
148    value: u64,
149    error: E,
150) -> Result<(), E> {
151    write_query_separator(output, len, first, error)?;
152    write_percent_encoded(output, len, key, error)?;
153    write_byte(output, len, b'=', error)?;
154    write_u64(output, len, value, error)
155}
156
157const fn is_unreserved(byte: u8) -> bool {
158    matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')
159}
160
161const fn hex_digit(nibble: u8) -> u8 {
162    match nibble {
163        0 => b'0',
164        1 => b'1',
165        2 => b'2',
166        3 => b'3',
167        4 => b'4',
168        5 => b'5',
169        6 => b'6',
170        7 => b'7',
171        8 => b'8',
172        9 => b'9',
173        10 => b'A',
174        11 => b'B',
175        12 => b'C',
176        13 => b'D',
177        14 => b'E',
178        _ => b'F',
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::{
185        write_json_string, write_percent_encoded, write_query_pair, write_query_u64, write_str,
186        write_u64,
187    };
188
189    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
190    enum TestError {
191        TooSmall,
192    }
193
194    #[test]
195    fn writes_decimal_numbers_including_zero() {
196        let mut output = [0u8; 24];
197        let mut len = 0;
198        assert_eq!(
199            write_u64(&mut output, &mut len, 0, TestError::TooSmall),
200            Ok(())
201        );
202        assert_eq!(
203            write_str(&mut output, &mut len, ",", TestError::TooSmall),
204            Ok(())
205        );
206        assert_eq!(
207            write_u64(&mut output, &mut len, u64::MAX, TestError::TooSmall),
208            Ok(())
209        );
210        let written = output
211            .get(..len)
212            .and_then(|bytes| core::str::from_utf8(bytes).ok());
213        assert_eq!(written, Some("0,18446744073709551615"));
214    }
215
216    #[test]
217    fn reports_too_small_buffers() {
218        let mut output = [0u8; 2];
219        let mut len = 0;
220        assert_eq!(
221            write_u64(&mut output, &mut len, 100, TestError::TooSmall),
222            Err(TestError::TooSmall)
223        );
224    }
225
226    #[test]
227    fn writes_percent_encoded_components_and_pairs() {
228        let mut output = [0u8; 64];
229        let mut len = 0;
230        assert_eq!(
231            write_percent_encoded(&mut output, &mut len, "env=prod", TestError::TooSmall),
232            Ok(())
233        );
234        let encoded = output
235            .get(..len)
236            .and_then(|bytes| core::str::from_utf8(bytes).ok());
237        assert_eq!(encoded, Some("env%3Dprod"));
238
239        let mut output = [0u8; 64];
240        let mut len = 0;
241        let mut first = true;
242        assert_eq!(
243            write_query_pair(
244                &mut output,
245                &mut len,
246                &mut first,
247                "label_selector",
248                "env=prod",
249                TestError::TooSmall,
250            ),
251            Ok(())
252        );
253        assert_eq!(
254            write_query_u64(
255                &mut output,
256                &mut len,
257                &mut first,
258                "page",
259                0,
260                TestError::TooSmall
261            ),
262            Ok(())
263        );
264        let query = output
265            .get(..len)
266            .and_then(|bytes| core::str::from_utf8(bytes).ok());
267        assert_eq!(query, Some("label_selector=env%3Dprod&page=0"));
268    }
269
270    #[test]
271    fn writes_json_strings_with_required_escaping() {
272        let mut output = [0u8; 96];
273        let mut len = 0;
274        assert_eq!(
275            write_json_string(
276                &mut output,
277                &mut len,
278                "line\n\"quoted\"\\slash\t\u{001f}",
279                TestError::TooSmall,
280            ),
281            Ok(())
282        );
283        let json = output
284            .get(..len)
285            .and_then(|bytes| core::str::from_utf8(bytes).ok());
286        assert_eq!(json, Some("\"line\\n\\\"quoted\\\"\\\\slash\\t\\u001F\""));
287    }
288}