1mod encoder;
4
5pub use encoder::{
6 SnapshotEncoder, encode_snapshot, encode_snapshot_bounded, measure_snapshot,
7 measure_snapshot_bounded,
8};
9
10#[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
21pub 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
31pub fn json_string_escaped_len<E: Copy>(value: &str, error: E) -> Result<usize, E> {
33 json_escaped_len(value, error)
34}
35
36pub 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
41pub 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
55pub 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
69pub 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
99pub 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
120pub 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
147pub 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
185pub 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
199pub 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
214pub 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}