byteor_pipeline_spec/
canonical.rs

1//! Canonical spec normalization and hashing helpers.
2
3use alloc::string::String;
4
5use sha2::{Digest, Sha256};
6
7use crate::{
8    decode_kv_v1, encode_kv_v1, validate_v1, EndpointCfgV1, LaneCfgV1, LaneGraphV1, PipelineSpecV1,
9    RoleCfgV1, SingleRingV1, SpecError, StageV1,
10};
11
12/// Return a canonicalized v1 spec with deterministic ordering for semantically unordered fields.
13pub fn canonicalize_v1(spec: &PipelineSpecV1) -> PipelineSpecV1 {
14    match spec {
15        PipelineSpecV1::LaneGraph(lg) => PipelineSpecV1::LaneGraph(canonicalize_lane_graph_v1(lg)),
16        PipelineSpecV1::SingleRing(ring) => {
17            PipelineSpecV1::SingleRing(canonicalize_single_ring_v1(ring))
18        }
19    }
20}
21
22/// Canonicalize a v1 `spec.kv` blob as `decode_kv_v1()` → `validate_v1()` → `encode_kv_v1()`.
23pub fn canonicalize_spec_kv_v1(input: &str) -> Result<String, SpecError> {
24    let spec = decode_kv_v1(input)?;
25    validate_v1(&spec)?;
26    Ok(encode_kv_v1(&spec))
27}
28
29/// Encode a spec using the canonical v1 ordering contract.
30pub fn canonical_encode_kv_v1(spec: &PipelineSpecV1) -> Result<String, SpecError> {
31    validate_v1(spec)?;
32    Ok(encode_kv_v1(spec))
33}
34
35/// Return the SHA-256 hex digest of canonical v1 spec bytes.
36pub fn canonical_spec_sha256_hex_v1(spec: &PipelineSpecV1) -> Result<String, SpecError> {
37    let canonical = canonical_encode_kv_v1(spec)?;
38    Ok(sha256_hex(canonical.as_bytes()))
39}
40
41fn canonicalize_lane_graph_v1(lg: &LaneGraphV1) -> LaneGraphV1 {
42    let mut endpoints: alloc::vec::Vec<EndpointCfgV1> = lg.endpoints.clone();
43    endpoints.sort_by(|a, b| a.name.cmp(&b.name));
44
45    let mut lanes: alloc::vec::Vec<LaneCfgV1> = lg.lanes.clone();
46    lanes.sort_by(|a, b| a.name.cmp(&b.name));
47
48    let mut roles: alloc::vec::Vec<RoleCfgV1> = lg.roles.clone();
49    roles.sort_by(|a, b| a.name().cmp(b.name()));
50
51    LaneGraphV1 {
52        endpoints,
53        lanes,
54        roles,
55        on_full: lg.on_full,
56    }
57}
58
59fn canonicalize_single_ring_v1(ring: &SingleRingV1) -> SingleRingV1 {
60    let mut stages: alloc::vec::Vec<StageV1> = ring.stages.clone();
61    for stage in &mut stages {
62        stage.depends_on.sort_unstable();
63        stage.depends_on.dedup();
64    }
65
66    SingleRingV1 {
67        shards: ring.shards,
68        ordering: ring.ordering,
69        producer: ring.producer,
70        scheduling: ring.scheduling,
71        shard_key: ring.shard_key,
72        stages,
73    }
74}
75
76fn sha256_hex(bytes: &[u8]) -> String {
77    let mut hasher = Sha256::new();
78    hasher.update(bytes);
79    let digest = hasher.finalize();
80    let mut out = String::with_capacity(digest.len() * 2);
81    for byte in digest {
82        push_hex_byte(&mut out, byte);
83    }
84    out
85}
86
87fn push_hex_byte(out: &mut String, byte: u8) {
88    const HEX: &[u8; 16] = b"0123456789abcdef";
89    out.push(HEX[(byte >> 4) as usize] as char);
90    out.push(HEX[(byte & 0x0f) as usize] as char);
91}