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 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
101pub 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
113pub 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
127pub 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
142pub 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}