1pub 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
16pub 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
29pub 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
57pub 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
76pub 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
103pub 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
142pub 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
156pub 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
171pub 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}