amiss-wire 0.1.0

Canonical JSON, digest domains, and the machine contracts of Amiss
Documentation
use core::fmt;

use sha2::{Digest as _, Sha256};

use crate::json::{Sink, Value, stream};

/// The domain for a digest over exact raw bytes taken as evidence: a resolved
/// target's blob, or one build lockfile as the release manifest records it.
pub const RAW_EVIDENCE_DOMAIN: &str = "amiss/raw-evidence/v1";

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Digest([u8; 32]);

impl Digest {
    #[must_use]
    pub const fn as_bytes(&self) -> &[u8; 32] {
        &self.0
    }

    /// Parses the `sha256:` wire form with exactly 64 lowercase hex digits.
    #[must_use]
    pub fn from_wire(raw: &str) -> Option<Self> {
        let hex = raw.strip_prefix("sha256:")?;
        if hex.len() != 64 {
            return None;
        }
        let mut out = [0_u8; 32];
        for (slot, pair) in out.iter_mut().zip(hex.as_bytes().chunks_exact(2)) {
            let [high, low] = pair else { return None };
            *slot = hex_value(*high)?.wrapping_shl(4) | hex_value(*low)?;
        }
        Some(Self(out))
    }
}

fn hex_value(byte: u8) -> Option<u8> {
    match byte {
        b'0'..=b'9' => Some(byte.wrapping_sub(b'0')),
        b'a'..=b'f' => Some(byte.wrapping_sub(b'a').wrapping_add(10)),
        _ => None,
    }
}

impl fmt::Display for Digest {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("sha256:")?;
        for byte in self.0 {
            write!(f, "{byte:02x}")?;
        }
        Ok(())
    }
}

impl fmt::Debug for Digest {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(self, f)
    }
}

/// The plain SHA-256 of exact bytes, with no domain separation: the
/// manifest's file and binary checksums are ordinary content digests, not
/// domain-separated identities.
#[must_use]
pub fn sha256(bytes: &[u8]) -> Digest {
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    Digest(hasher.finalize().into())
}

#[must_use]
pub fn hb(domain: &str, bytes: &[u8]) -> Digest {
    let mut hasher = with_domain(domain);
    hasher.update(bytes);
    Digest(hasher.finalize().into())
}

#[must_use]
pub fn hj(domain: &str, value: &Value) -> Digest {
    let mut sink = HashSink(with_domain(domain));
    stream(value, &mut sink);
    Digest(sink.0.finalize().into())
}

struct HashSink(Sha256);

impl Sink for HashSink {
    fn write(&mut self, piece: &str) {
        self.0.update(piece.as_bytes());
    }
}

fn with_domain(domain: &str) -> Sha256 {
    let mut hasher = Sha256::new();
    hasher.update(domain.as_bytes());
    hasher.update([0_u8]);
    hasher
}