use std::fmt::Write as _;
pub fn push_string(out: &mut String, s: &str) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
}
pub fn hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(out, "{b:02x}");
}
out
}
pub struct Obj<'a> {
out: &'a mut String,
empty: bool,
}
impl<'a> Obj<'a> {
pub fn new(out: &'a mut String) -> Self {
out.push('{');
Self { out, empty: true }
}
fn key(&mut self, key: &str) {
if !self.empty {
self.out.push(',');
}
self.empty = false;
push_string(self.out, key);
self.out.push(':');
}
pub fn str(&mut self, key: &str, value: &str) {
self.key(key);
push_string(self.out, value);
}
pub fn u64(&mut self, key: &str, value: u64) {
self.key(key);
let _ = write!(self.out, "{value}");
}
pub fn i64(&mut self, key: &str, value: i64) {
self.key(key);
let _ = write!(self.out, "{value}");
}
pub fn bool(&mut self, key: &str, value: bool) {
self.key(key);
self.out.push_str(if value { "true" } else { "false" });
}
pub fn raw(&mut self, key: &str, value: &str) {
self.key(key);
self.out.push_str(value);
}
pub fn bytes(&mut self, key: &str, value: &[u8]) {
let shown = String::from_utf8_lossy(value);
self.str(key, &shown);
if shown.as_bytes() != value {
self.str(&format!("{key}_hex"), &hex(value));
}
}
pub fn strings(&mut self, key: &str, values: &[&str]) {
self.key(key);
self.out.push('[');
for (i, v) in values.iter().enumerate() {
if i > 0 {
self.out.push(',');
}
push_string(self.out, v);
}
self.out.push(']');
}
pub fn obj(&mut self, key: &str) -> Obj<'_> {
self.key(key);
Obj::new(self.out)
}
pub fn arr(&mut self, key: &str) -> Arr<'_> {
self.key(key);
Arr::new(self.out)
}
pub fn end(self) {
self.out.push('}');
}
}
pub struct Arr<'a> {
out: &'a mut String,
empty: bool,
}
impl<'a> Arr<'a> {
fn new(out: &'a mut String) -> Self {
out.push('[');
Self { out, empty: true }
}
pub fn obj(&mut self) -> Obj<'_> {
if !self.empty {
self.out.push(',');
}
self.empty = false;
Obj::new(self.out)
}
pub fn end(self) {
self.out.push(']');
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strings_escape_what_the_grammar_requires() {
let mut out = String::new();
push_string(&mut out, "a\"b\\c\nd\te\u{1}f");
assert_eq!(out, r#""a\"b\\c\nd\te\u0001f""#);
}
#[test]
fn an_object_carries_the_value_kinds_this_tool_emits() {
let mut out = String::new();
let mut o = Obj::new(&mut out);
o.u64("blocks", 16384);
o.i64("created", -1);
o.bool("clean", true);
o.str("uuid", "f0e1");
o.strings("features", &["extent", "64bit"]);
o.raw("scan", "{\"clean\":true}");
o.end();
assert_eq!(
out,
r#"{"blocks":16384,"created":-1,"clean":true,"uuid":"f0e1","features":["extent","64bit"],"scan":{"clean":true}}"#
);
}
#[test]
fn a_name_that_is_not_text_carries_its_bytes_as_well() {
let mut out = String::new();
let mut o = Obj::new(&mut out);
o.bytes("name", b"/etc/hostname");
o.end();
assert_eq!(
out, r#"{"name":"/etc/hostname"}"#,
"a name that is text says so once"
);
let mut out = String::new();
let mut o = Obj::new(&mut out);
o.bytes("name", b"/od\xffd");
o.end();
assert_eq!(
out,
r#"{"name":"/od\u{fffd}d","name_hex":"2f6f64ff64"}"#.replace("\\u{fffd}", "\u{fffd}")
);
}
#[test]
fn nested_objects_and_arrays() {
let mut out = String::new();
let mut o = Obj::new(&mut out);
let mut inner = o.obj("features");
inner.u64("unknown", 0);
inner.end();
let mut groups = o.arr("groups");
let mut g = groups.obj();
g.u64("group", 0);
g.end();
let mut g = groups.obj();
g.u64("group", 1);
g.end();
groups.end();
o.end();
assert_eq!(
out,
r#"{"features":{"unknown":0},"groups":[{"group":0},{"group":1}]}"#
);
}
}