1mod 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#[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
22pub 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
32pub fn json_string_escaped_len<E: Copy>(value: &str, error: E) -> Result<usize, E> {
34 json_escaped_len(value, error)
35}
36
37pub 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
42pub 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
56pub 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
70pub 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
100pub 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
121pub 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
148pub 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
186pub 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
200pub 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
215pub 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}