use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
trait Sink {
fn put(&mut self, bytes: &[u8]);
}
impl Sink for Sha256 {
#[inline]
fn put(&mut self, bytes: &[u8]) {
self.update(bytes);
}
}
impl Sink for Vec<u8> {
#[inline]
fn put(&mut self, bytes: &[u8]) {
self.extend_from_slice(bytes);
}
}
const HEX: &[u8; 16] = b"0123456789abcdef";
fn put_json_string<S: Sink>(sink: &mut S, value: &str) {
sink.put(b"\"");
let bytes = value.as_bytes();
let mut start = 0usize;
for i in 0..bytes.len() {
let c = bytes[i];
let escape: &[u8] = match c {
b'"' => b"\\\"",
b'\\' => b"\\\\",
0x08 => b"\\b",
0x09 => b"\\t",
0x0A => b"\\n",
0x0C => b"\\f",
0x0D => b"\\r",
0x00..=0x1F => {
if start < i {
sink.put(&bytes[start..i]);
}
let buf = [
b'\\',
b'u',
b'0',
b'0',
HEX[(c >> 4) as usize],
HEX[(c & 0x0F) as usize],
];
sink.put(&buf);
start = i + 1;
continue;
}
_ => continue,
};
if start < i {
sink.put(&bytes[start..i]);
}
sink.put(escape);
start = i + 1;
}
if start < bytes.len() {
sink.put(&bytes[start..]);
}
sink.put(b"\"");
}
fn put_path<S: Sink>(sink: &mut S, path: &[String]) {
sink.put(b"[");
for (i, seg) in path.iter().enumerate() {
if i > 0 {
sink.put(b",");
}
put_json_string(sink, seg);
}
sink.put(b"]");
}
fn put_content<S: Sink>(sink: &mut S, content: &BTreeMap<String, BTreeSet<String>>) {
sink.put(b"{");
for (i, (key, values)) in content.iter().enumerate() {
if i > 0 {
sink.put(b",");
}
put_json_string(sink, key);
sink.put(b":[");
for (j, v) in values.iter().enumerate() {
if j > 0 {
sink.put(b",");
}
put_json_string(sink, v);
}
sink.put(b"]");
}
sink.put(b"}");
}
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0F) as usize] as char);
}
out
}
pub(crate) fn hash_path(path: &[String]) -> String {
let mut h = Sha256::new();
put_path(&mut h, path);
to_hex(&h.finalize())
}
pub(crate) fn hash_content(content: &BTreeMap<String, BTreeSet<String>>) -> String {
let mut h = Sha256::new();
put_content(&mut h, content);
to_hex(&h.finalize())
}
pub fn canonical_json_string(value: &str) -> String {
let mut buf = Vec::new();
put_json_string(&mut buf, value);
String::from_utf8(buf).expect("canonical bytes are valid UTF-8")
}
pub(crate) fn canonical_path(path: &[String]) -> String {
let mut buf = Vec::new();
put_path(&mut buf, path);
String::from_utf8(buf).expect("canonical bytes are valid UTF-8")
}
pub(crate) fn canonical_content(content: &BTreeMap<String, BTreeSet<String>>) -> String {
let mut buf = Vec::new();
put_content(&mut buf, content);
String::from_utf8(buf).expect("canonical bytes are valid UTF-8")
}
#[cfg(test)]
mod tests {
use super::*;
fn buf_string(value: &str) -> String {
let mut v = Vec::new();
put_json_string(&mut v, value);
String::from_utf8(v).unwrap()
}
#[test]
fn json_string_escaping() {
assert_eq!(buf_string("plain"), r#""plain""#);
assert_eq!(buf_string(r#"a"b"#), r#""a\"b""#);
assert_eq!(buf_string(r"a\b"), r#""a\\b""#);
assert_eq!(buf_string("tab\there"), r#""tab\there""#);
assert_eq!(buf_string("nl\nhere"), r#""nl\nhere""#);
assert_eq!(buf_string("\u{0001}"), "\"\\u0001\"");
assert_eq!(buf_string("acentuação ✓"), "\"acentuação ✓\""); }
#[test]
fn streaming_and_materialised_paths_agree() {
let path = vec!["a".to_string(), "b\"c".to_string()];
let materialised = canonical_path(&path);
assert_eq!(hash_path(&path), to_hex(&Sha256::digest(materialised.as_bytes())));
}
#[test]
fn empty_content_is_empty_object() {
assert_eq!(canonical_content(&BTreeMap::new()), "{}");
}
#[test]
fn public_canonical_json_string_matches_internal_encoding() {
assert_eq!(canonical_json_string(r#"a"b"#), r#""a\"b""#);
assert_eq!(canonical_json_string("\u{0001}"), "\"\\u0001\"");
assert_eq!(canonical_json_string("acentuação ✓"), "\"acentuação ✓\"");
}
}