pointbreak 0.5.0

Durable terminal code review for changes humans and coding agents collaborate on together
Documentation
use serde::Serialize;
use sha2::{Digest, Sha256};

use crate::error::Result;

pub(crate) fn sha256_json_hex<T>(value: &T) -> Result<String>
where
    T: Serialize,
{
    let value = serde_json::to_value(value)?;
    let bytes = canonical_json_bytes(&value)?;
    Ok(sha256_bytes_hex(&bytes))
}

pub(crate) fn sha256_json_prefixed(value: &serde_json::Value) -> Result<String> {
    let bytes = canonical_json_bytes(value)?;
    Ok(format!("sha256:{}", sha256_bytes_hex(&bytes)))
}

pub(crate) fn sha256_bytes_hex(bytes: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    hex_lower(hasher.finalize().as_slice())
}

pub(crate) fn canonical_json_bytes(value: &serde_json::Value) -> Result<Vec<u8>> {
    let canonical = canonical_json_value(value);
    Ok(serde_json::to_vec(&canonical)?)
}

fn canonical_json_value(value: &serde_json::Value) -> serde_json::Value {
    match value {
        serde_json::Value::Array(values) => {
            serde_json::Value::Array(values.iter().map(canonical_json_value).collect())
        }
        serde_json::Value::Object(object) => {
            let mut keys = object.keys().collect::<Vec<_>>();
            keys.sort_unstable();

            let mut canonical = serde_json::Map::new();
            for key in keys {
                let value = object
                    .get(key)
                    .expect("key collected from object remains present");
                canonical.insert(key.clone(), canonical_json_value(value));
            }

            serde_json::Value::Object(canonical)
        }
        _ => value.clone(),
    }
}

fn hex_lower(bytes: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut output = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        output.push(HEX[(byte >> 4) as usize] as char);
        output.push(HEX[(byte & 0x0f) as usize] as char);
    }
    output
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    #[test]
    fn canonical_json_hash_sorts_object_keys_recursively() {
        let value = json!({
            "b": 2,
            "arr": [{ "z": 1, "y": 2 }],
            "a": { "d": 4, "c": 3 }
        });

        let bytes = canonical_json_bytes(&value).expect("canonical json bytes");

        assert_eq!(
            String::from_utf8(bytes).expect("canonical json is utf-8"),
            r#"{"a":{"c":3,"d":4},"arr":[{"y":2,"z":1}],"b":2}"#
        );
        assert_eq!(
            sha256_json_prefixed(&value).expect("canonical hash"),
            "sha256:b0d4c7b49807651a24243b0ebe264541166d8aa9fd31719c6e461a0118e4dd2f"
        );
    }
}