pub fn write_byte<E: Copy>(
output: &mut [u8],
len: &mut usize,
byte: u8,
error: E,
) -> Result<(), E> {
let slot = output.get_mut(*len).ok_or(error)?;
*slot = byte;
*len = len.checked_add(1).ok_or(error)?;
Ok(())
}
pub fn write_str<E: Copy>(
output: &mut [u8],
len: &mut usize,
value: &str,
error: E,
) -> Result<(), E> {
for byte in value.bytes() {
write_byte(output, len, byte, error)?;
}
Ok(())
}
pub fn write_u64<E: Copy>(
output: &mut [u8],
len: &mut usize,
mut value: u64,
error: E,
) -> Result<(), E> {
if value == 0 {
return write_byte(output, len, b'0', error);
}
let mut digits = [0u8; 20];
let mut cursor = digits.len();
while value != 0 {
cursor = cursor.checked_sub(1).ok_or(error)?;
let digit = u8::try_from(value % 10).map_err(|_| error)?;
let slot = digits.get_mut(cursor).ok_or(error)?;
*slot = b'0'.checked_add(digit).ok_or(error)?;
value /= 10;
}
let encoded = digits.get(cursor..).ok_or(error)?;
for byte in encoded {
write_byte(output, len, *byte, error)?;
}
Ok(())
}
pub fn write_percent_encoded<E: Copy>(
output: &mut [u8],
len: &mut usize,
value: &str,
error: E,
) -> Result<(), E> {
for byte in value.bytes() {
if is_unreserved(byte) {
write_byte(output, len, byte, error)?;
} else {
write_byte(output, len, b'%', error)?;
write_byte(output, len, hex_digit(byte >> 4), error)?;
write_byte(output, len, hex_digit(byte & 0x0f), error)?;
}
}
Ok(())
}
pub fn write_json_string_escaped<E: Copy>(
output: &mut [u8],
len: &mut usize,
value: &str,
error: E,
) -> Result<(), E> {
for byte in value.bytes() {
match byte {
b'"' => write_str(output, len, "\\\"", error)?,
b'\\' => write_str(output, len, "\\\\", error)?,
b'\n' => write_str(output, len, "\\n", error)?,
b'\r' => write_str(output, len, "\\r", error)?,
b'\t' => write_str(output, len, "\\t", error)?,
0x00..=0x1f => {
write_str(output, len, "\\u00", error)?;
write_byte(output, len, hex_digit(byte >> 4), error)?;
write_byte(output, len, hex_digit(byte & 0x0f), error)?;
}
_ => write_byte(output, len, byte, error)?,
}
}
Ok(())
}
pub fn write_json_string<E: Copy>(
output: &mut [u8],
len: &mut usize,
value: &str,
error: E,
) -> Result<(), E> {
write_byte(output, len, b'"', error)?;
write_json_string_escaped(output, len, value, error)?;
write_byte(output, len, b'"', error)
}
pub fn write_query_separator<E: Copy>(
output: &mut [u8],
len: &mut usize,
first: &mut bool,
error: E,
) -> Result<(), E> {
if *first {
*first = false;
return Ok(());
}
write_byte(output, len, b'&', error)
}
pub fn write_query_pair<E: Copy>(
output: &mut [u8],
len: &mut usize,
first: &mut bool,
key: &str,
value: &str,
error: E,
) -> Result<(), E> {
write_query_separator(output, len, first, error)?;
write_percent_encoded(output, len, key, error)?;
write_byte(output, len, b'=', error)?;
write_percent_encoded(output, len, value, error)
}
pub fn write_query_u64<E: Copy>(
output: &mut [u8],
len: &mut usize,
first: &mut bool,
key: &str,
value: u64,
error: E,
) -> Result<(), E> {
write_query_separator(output, len, first, error)?;
write_percent_encoded(output, len, key, error)?;
write_byte(output, len, b'=', error)?;
write_u64(output, len, value, error)
}
const fn is_unreserved(byte: u8) -> bool {
matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')
}
const fn hex_digit(nibble: u8) -> u8 {
match nibble {
0 => b'0',
1 => b'1',
2 => b'2',
3 => b'3',
4 => b'4',
5 => b'5',
6 => b'6',
7 => b'7',
8 => b'8',
9 => b'9',
10 => b'A',
11 => b'B',
12 => b'C',
13 => b'D',
14 => b'E',
_ => b'F',
}
}
#[cfg(test)]
mod tests {
use super::{
write_json_string, write_percent_encoded, write_query_pair, write_query_u64, write_str,
write_u64,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TestError {
TooSmall,
}
#[test]
fn writes_decimal_numbers_including_zero() {
let mut output = [0u8; 24];
let mut len = 0;
assert_eq!(
write_u64(&mut output, &mut len, 0, TestError::TooSmall),
Ok(())
);
assert_eq!(
write_str(&mut output, &mut len, ",", TestError::TooSmall),
Ok(())
);
assert_eq!(
write_u64(&mut output, &mut len, u64::MAX, TestError::TooSmall),
Ok(())
);
let written = output
.get(..len)
.and_then(|bytes| core::str::from_utf8(bytes).ok());
assert_eq!(written, Some("0,18446744073709551615"));
}
#[test]
fn reports_too_small_buffers() {
let mut output = [0u8; 2];
let mut len = 0;
assert_eq!(
write_u64(&mut output, &mut len, 100, TestError::TooSmall),
Err(TestError::TooSmall)
);
}
#[test]
fn writes_percent_encoded_components_and_pairs() {
let mut output = [0u8; 64];
let mut len = 0;
assert_eq!(
write_percent_encoded(&mut output, &mut len, "env=prod", TestError::TooSmall),
Ok(())
);
let encoded = output
.get(..len)
.and_then(|bytes| core::str::from_utf8(bytes).ok());
assert_eq!(encoded, Some("env%3Dprod"));
let mut output = [0u8; 64];
let mut len = 0;
let mut first = true;
assert_eq!(
write_query_pair(
&mut output,
&mut len,
&mut first,
"label_selector",
"env=prod",
TestError::TooSmall,
),
Ok(())
);
assert_eq!(
write_query_u64(
&mut output,
&mut len,
&mut first,
"page",
0,
TestError::TooSmall
),
Ok(())
);
let query = output
.get(..len)
.and_then(|bytes| core::str::from_utf8(bytes).ok());
assert_eq!(query, Some("label_selector=env%3Dprod&page=0"));
}
#[test]
fn writes_json_strings_with_required_escaping() {
let mut output = [0u8; 96];
let mut len = 0;
assert_eq!(
write_json_string(
&mut output,
&mut len,
"line\n\"quoted\"\\slash\t\u{001f}",
TestError::TooSmall,
),
Ok(())
);
let json = output
.get(..len)
.and_then(|bytes| core::str::from_utf8(bytes).ok());
assert_eq!(json, Some("\"line\\n\\\"quoted\\\"\\\\slash\\t\\u001F\""));
}
}