use std::io::{self, Write};
use serde::Serialize;
use sha2::{Digest, Sha256};
pub fn sha256(content: &[u8]) -> String {
lower_hex(Sha256::digest(content).as_ref())
}
pub(crate) fn lower_hex(bytes: &[u8]) -> String {
let mut encoded = String::with_capacity(bytes.len() * 2);
push_lower_hex(&mut encoded, bytes);
encoded
}
fn push_lower_hex(encoded: &mut String, bytes: &[u8]) {
use std::fmt::Write as _;
for byte in bytes {
write!(encoded, "{byte:02x}").expect("writing to String cannot fail");
}
}
pub(crate) struct FramedSha256 {
digest: Sha256,
}
impl FramedSha256 {
pub(crate) fn new(domain: &[u8]) -> Self {
let mut digest = Sha256::new();
digest.update(domain);
digest.update([0]);
Self { digest }
}
pub(crate) fn update_bytes(&mut self, value: &[u8]) {
update_length_prefixed(&mut self.digest, value);
}
fn update_preframed_bytes(&mut self, value: &[u8]) {
self.digest.update(value);
}
pub(crate) fn update_json(&mut self, value: &impl Serialize) -> serde_json::Result<()> {
let mut counter = ByteCounter::default();
serde_json::to_writer(&mut counter, value)?;
self.update_u64(counter.bytes);
serde_json::to_writer(PreframedDigestWriter(self), value)
}
pub(crate) fn update_u8(&mut self, value: u8) {
self.digest.update([value]);
}
pub(crate) fn update_u64(&mut self, value: u64) {
self.digest.update(value.to_be_bytes());
}
pub(crate) fn update_u32(&mut self, value: u32) {
self.digest.update(value.to_be_bytes());
}
pub(crate) fn update_i64(&mut self, value: i64) {
self.digest.update(value.to_be_bytes());
}
pub(crate) fn finish(self) -> String {
let bytes = self.digest.finalize();
let mut encoded = String::with_capacity("sha256:".len() + bytes.len() * 2);
encoded.push_str("sha256:");
push_lower_hex(&mut encoded, bytes.as_ref());
encoded
}
pub(crate) fn finish_i64_words(self) -> [i64; 4] {
let bytes = self.digest.finalize();
std::array::from_fn(|word| {
let start = word * std::mem::size_of::<i64>();
i64::from_be_bytes(
bytes[start..start + std::mem::size_of::<i64>()]
.try_into()
.expect("SHA-256 always contains four i64 words"),
)
})
}
}
#[derive(Default)]
struct ByteCounter {
bytes: u64,
}
impl Write for ByteCounter {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
self.bytes = self
.bytes
.checked_add(buffer.len() as u64)
.ok_or_else(|| io::Error::other("serialized digest value exceeds u64 bytes"))?;
Ok(buffer.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
struct PreframedDigestWriter<'digest>(&'digest mut FramedSha256);
impl Write for PreframedDigestWriter<'_> {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
self.0.update_preframed_bytes(buffer);
Ok(buffer.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn update_length_prefixed(digest: &mut Sha256, value: &[u8]) {
digest.update((value.len() as u64).to_be_bytes());
digest.update(value);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digest_is_stable_lowercase_hex_and_byte_sensitive() {
assert_eq!(
sha256(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(
sha256(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert_ne!(
sha256(b"abc"),
sha256(b"abc\n"),
"trailing bytes must count"
);
}
#[test]
fn fixed_width_words_preserve_every_digest_byte_without_allocation() {
let mut digest = FramedSha256::new(b"aise-word-test-v1");
digest.update_bytes(b"same input");
let words = digest.finish_i64_words();
let mut repeated = FramedSha256::new(b"aise-word-test-v1");
repeated.update_bytes(b"same input");
assert_eq!(words, repeated.finish_i64_words());
let mut changed = FramedSha256::new(b"aise-word-test-v1");
changed.update_bytes(b"different input");
assert_ne!(words, changed.finish_i64_words());
}
#[test]
fn framed_digest_preserves_domain_value_boundaries_and_order() {
let mut first = FramedSha256::new(b"aise-test-digest-v1");
first.update_bytes(b"ab");
first.update_bytes(b"c");
let mut different_boundaries = FramedSha256::new(b"aise-test-digest-v1");
different_boundaries.update_bytes(b"a");
different_boundaries.update_bytes(b"bc");
let mut different_order = FramedSha256::new(b"aise-test-digest-v1");
different_order.update_bytes(b"c");
different_order.update_bytes(b"ab");
let mut different_domain = FramedSha256::new(b"aise-other-digest-v1");
different_domain.update_bytes(b"ab");
different_domain.update_bytes(b"c");
let expected = first.finish();
assert!(expected.starts_with("sha256:"));
assert_eq!(expected.len(), "sha256:".len() + 64);
assert_ne!(expected, different_boundaries.finish());
assert_ne!(expected, different_order.finish());
assert_ne!(expected, different_domain.finish());
}
#[test]
fn streamed_json_matches_one_length_framed_compact_value() {
let value = serde_json::json!({"ordered": [1, 2, 3], "text": "value"});
let bytes = serde_json::to_vec(&value).unwrap();
let mut streamed = FramedSha256::new(b"aise-json-digest-v1");
streamed.update_json(&value).unwrap();
let mut buffered = FramedSha256::new(b"aise-json-digest-v1");
buffered.update_bytes(&bytes);
assert_eq!(streamed.finish(), buffered.finish());
}
}