Skip to main content

arete_hash/
vectors.rs

1//! Language-neutral conformance vector support.
2//!
3//! The shared corpus at `test-vectors/hash-v1.json` describes inputs,
4//! canonical payloads, framed preimages, digests, textual identifiers, and
5//! expected failures. Rust and TypeScript tests execute the same vectors
6//! through this single dispatch so both languages prove byte-level parity
7//! against one implementation of the profile dispatch rules.
8
9use sha2::{Digest, Sha256};
10
11use crate::{
12    artifact_tree_payload, canonicalize_json_bytes, framed_preimage, framed_tuple_payload,
13    identity_metadata, AnyHashId, ArtifactEntryKind, ArtifactTreeEntry, CanonicalizationProfile,
14    HashError, HashKindName, TupleField,
15};
16
17/// Owned form of the language-neutral vector input encodings.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum VectorInput {
20    /// `{"encoding": "utf8", "data": "..."}` and `{"encoding": "hex", ...}`.
21    RawBytes(Vec<u8>),
22    /// `{"encoding": "tuple", "fields": [{"label", "valueUtf8"|"valueHex"}]}`.
23    TupleFields(Vec<(String, Vec<u8>)>),
24    /// `{"encoding": "tree", "entries": [{"path", "bytesHex", "type"}]}`.
25    TreeEntries(Vec<VectorTreeEntry>),
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct VectorTreeEntry {
30    pub path: String,
31    pub bytes: Vec<u8>,
32    pub symlink: bool,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct VectorOutcome {
37    pub canonical_payload: Vec<u8>,
38    pub preimage: Vec<u8>,
39    pub digest: [u8; 32],
40    pub hash_id: AnyHashId,
41}
42
43/// Execute one hash vector: build the canonical payload for the profile,
44/// frame it, and digest it. The declared profile must match the registry
45/// profile for the kind; unknown kinds fail closed during vector parsing.
46pub fn execute_vector(
47    kind: HashKindName,
48    profile: CanonicalizationProfile,
49    input: &VectorInput,
50) -> Result<VectorOutcome, HashError> {
51    let metadata = identity_metadata(kind);
52    if metadata.profile != profile {
53        return Err(HashError::ProfileMismatch {
54            kind: kind.to_string(),
55            expected: metadata.profile.to_string(),
56            actual: profile.to_string(),
57        });
58    }
59
60    let canonical_payload = match (profile, input) {
61        (CanonicalizationProfile::RawBytesV1, VectorInput::RawBytes(bytes)) => bytes.clone(),
62        (CanonicalizationProfile::AreteJcsV1, VectorInput::RawBytes(bytes)) => {
63            canonicalize_json_bytes(bytes)?
64        }
65        (CanonicalizationProfile::FramedTupleV1, VectorInput::TupleFields(fields)) => {
66            let fields: Vec<TupleField<'_>> = fields
67                .iter()
68                .map(|(label, value)| TupleField::new(label, value))
69                .collect();
70            framed_tuple_payload(&fields)?
71        }
72        (CanonicalizationProfile::ArtifactTreeV1, VectorInput::TreeEntries(entries)) => {
73            let entries: Vec<ArtifactTreeEntry<'_>> = entries
74                .iter()
75                .map(|entry| ArtifactTreeEntry {
76                    path: &entry.path,
77                    bytes: &entry.bytes,
78                    kind: if entry.symlink {
79                        ArtifactEntryKind::Symlink
80                    } else {
81                        ArtifactEntryKind::File
82                    },
83                })
84                .collect();
85            artifact_tree_payload(&entries)?
86        }
87        _ => {
88            return Err(HashError::InvalidHashId(
89                "vector input encoding does not match the canonicalization profile",
90            ))
91        }
92    };
93
94    let preimage = framed_preimage(kind, profile, &canonical_payload);
95    let digest: [u8; 32] = Sha256::digest(&preimage).into();
96    Ok(VectorOutcome {
97        canonical_payload,
98        preimage,
99        digest,
100        hash_id: AnyHashId::from_parts(kind, digest),
101    })
102}