use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::io::Write as _;
use serde::Serialize;
use sha2::{Digest, Sha256};
use crate::document::{CadIr, SortedModel, SourceMeta};
use crate::native::{Native, NativeNamespace, NativeRecord};
use crate::units::{Tolerances, Units};
pub fn sha256_hex(bytes: &[u8]) -> String {
encode_hex(&Sha256::digest(bytes))
}
pub fn canonical_json_sha256<T: Serialize>(value: &T) -> String {
let mut hasher = Sha256::new();
let mut writer = std::io::BufWriter::new(DigestWriter(&mut hasher));
serde_json::to_writer_pretty(&mut writer, value).expect("canonical JSON serialization");
writer.flush().expect("a digest accepts every byte");
drop(writer);
encode_hex(&hasher.finalize())
}
pub fn semantic_document_hash(ir: &CadIr, format: &str, source_image_id: &str) -> String {
let unknowns = reduced_unknowns(ir, format, source_image_id);
canonical_json_sha256(&NormalizedDocument {
ir_version: &ir.ir_version,
source: ir.source.as_ref().map(|source| {
let mut source = source.clone();
source.attributes.remove("semantic_sha256");
source
}),
units: &ir.units,
tolerances: &ir.tolerances,
model: ir.model.sorted(),
native: normalized_native(&ir.native, format, &unknowns),
})
}
fn reduced_unknowns(ir: &CadIr, format: &str, source_image_id: &str) -> Vec<NativeRecord> {
let mut unreadable = false;
let mut projected = NativeNamespace::default();
projected
.set_arena_from(
"unknowns",
ir.native_unknowns_iter(format)
.map_while(|record| record.inspect_err(|_| unreadable = true).ok())
.filter(|record| record.id.0 != source_image_id),
)
.expect("unknown records serialize");
if unreadable {
return Vec::new();
}
projected
.arenas
.remove("unknowns")
.expect("the unknown arena was just set")
}
#[derive(Serialize)]
struct NormalizedDocument<'a> {
ir_version: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<SourceMeta>,
units: &'a Units,
tolerances: &'a Tolerances,
model: SortedModel<'a>,
native: NormalizedNative<'a>,
}
#[derive(Serialize)]
#[serde(transparent)]
struct NormalizedNative<'a> {
namespaces: BTreeMap<&'a str, NormalizedNamespace<'a>>,
}
#[derive(Serialize)]
struct NormalizedNamespace<'a> {
version: u32,
arenas: BTreeMap<&'a str, Vec<&'a NativeRecord>>,
}
fn normalized_native<'a>(
native: &'a Native,
format: &'a str,
unknowns: &'a [NativeRecord],
) -> NormalizedNative<'a> {
let mut namespaces = native
.0
.iter()
.map(|(name, namespace)| {
let arenas = namespace
.arenas
.iter()
.map(|(arena, records)| (arena.as_str(), sorted_records(records)))
.collect();
(
name.as_str(),
NormalizedNamespace {
version: namespace.version,
arenas,
},
)
})
.collect::<BTreeMap<_, _>>();
let namespace = namespaces
.entry(format)
.or_insert_with(|| NormalizedNamespace {
version: 0,
arenas: BTreeMap::new(),
});
if namespace.version == 0 {
namespace.version = 1;
}
namespace
.arenas
.insert("unknowns", unknowns.iter().collect());
NormalizedNative { namespaces }
}
fn sorted_records(records: &[NativeRecord]) -> Vec<&NativeRecord> {
let mut refs = records.iter().collect::<Vec<_>>();
refs.sort_by(|left, right| left.id().cmp(right.id()));
refs
}
struct DigestWriter<'a>(&'a mut Sha256);
impl std::io::Write for DigestWriter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.update(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn encode_hex(digest: &[u8]) -> String {
let mut encoded = String::with_capacity(digest.len() * 2);
for byte in digest {
write!(encoded, "{byte:02x}").expect("writing to a String cannot fail");
}
encoded
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::{canonical_json_sha256, semantic_document_hash, sha256_hex};
use crate::document::CadIr;
use crate::native::{Native, NativeRecord};
use crate::units::Units;
#[test]
fn encodes_sha256_as_lowercase_hexadecimal() {
assert_eq!(
sha256_hex(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
fn pinned_record() -> NativeRecord {
let serde_json::Value::Object(fields) = serde_json::json!({
"zeta": null,
"alpha": [-1, 0, 1.5, 2.0, 1e10, 1e-7],
"beta": {
"nested": {"deep": [true, false]},
"empty_array": [],
"empty_object": {}
},
"escaped": "quote\" backslash\\ slash/ newline\n tab\t bell\u{7} accent é",
"gamma": 9_007_199_254_740_993_u64,
"delta": -9_007_199_254_740_993_i64
}) else {
panic!("the pinned record literal is a JSON object");
};
NativeRecord::new("pin:record#0", fields)
}
fn pinned_native() -> Native {
let mut native = Native::default();
let namespace = native.namespace_mut("pin");
namespace.version = 3;
namespace
.arenas
.insert("records".into(), vec![pinned_record()]);
native
}
#[test]
fn pins_pretty_printed_native_record_bytes() {
let expected = r#"{
"id": "pin:record#0",
"alpha": [
-1,
0,
1.5,
2.0,
10000000000.0,
1e-7
],
"beta": {
"empty_array": [],
"empty_object": {},
"nested": {
"deep": [
true,
false
]
}
},
"delta": -9007199254740993,
"escaped": "quote\" backslash\\ slash/ newline\n tab\t bell\u0007 accent é",
"gamma": 9007199254740993,
"zeta": null
}"#;
assert_eq!(
serde_json::to_string_pretty(&pinned_record()).unwrap(),
expected
);
}
#[test]
fn pins_pretty_printed_native_arena_bytes() {
let expected = r#"{
"pin": {
"version": 3,
"arenas": {
"records": [
{
"id": "pin:record#0",
"alpha": [
-1,
0,
1.5,
2.0,
10000000000.0,
1e-7
],
"beta": {
"empty_array": [],
"empty_object": {},
"nested": {
"deep": [
true,
false
]
}
},
"delta": -9007199254740993,
"escaped": "quote\" backslash\\ slash/ newline\n tab\t bell\u0007 accent é",
"gamma": 9007199254740993,
"zeta": null
}
]
}
}
}"#;
assert_eq!(
serde_json::to_string_pretty(&pinned_native()).unwrap(),
expected
);
}
#[test]
fn pins_native_arena_digest() {
assert_eq!(
canonical_json_sha256(&pinned_native()),
"acc1d88751dcb143ca47618c3f7a8ce14865edff0a26ab95748d2a0314ee8df0"
);
}
fn pinned_unknown(id: &str, links: &[&str]) -> NativeRecord {
let serde_json::Value::Object(fields) = serde_json::json!({
"links": links,
"offset": 4096,
"byte_len": 12,
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"data": "AQID"
}) else {
panic!("the pinned unknown literal is a JSON object");
};
NativeRecord::new(id, fields)
}
fn pinned_document() -> CadIr {
let mut ir = CadIr::empty(Units::default());
ir.native = pinned_native();
let namespace = ir.native.namespace_mut("pin");
namespace.arenas.insert(
"unknowns".into(),
vec![
pinned_unknown("pin:source-image#0", &[]),
pinned_unknown("pin:unknown#0", &["pin:record#0", "pin:unknown#1"]),
],
);
ir.finalize();
ir
}
#[test]
fn pins_document_digests() {
let ir = pinned_document();
assert_eq!(
canonical_json_sha256(&ir),
"00ac254ce62cf446f1d1dcea56ded050bd9a1ef2a53c846a8e7c588cd99bb071"
);
assert_eq!(
semantic_document_hash(&ir, "pin", "pin:source-image#0"),
"83fa753fb39360b9e51859c9c07ddac6ff23ec17b179fa548cf33c4331170180"
);
}
#[test]
fn round_tripped_document_hashes_identically() {
let ir = pinned_document();
let json = ir.to_canonical_json().unwrap();
let mut reparsed = CadIr::from_json(&json).unwrap();
reparsed.finalize();
assert_eq!(canonical_json_sha256(&ir), canonical_json_sha256(&reparsed));
assert_eq!(ir.to_canonical_json().unwrap(), json);
}
}