Skip to main content

weavatrix_refactor_plan/
fingerprint.rs

1use crate::canonical::{CanonicalError, CanonicalSerializer};
2use crate::{
3    Completeness, CompletenessProof, GraphRevision, NotModified, PlanError, PlanErrorCode,
4    RefactorOperation, RefactorPlan, RefactorPlanLimits, StatusCode, UncertainReference,
5    WarningCode,
6};
7use blazingly_json::Value;
8use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
9use sha2::{Digest, Sha256};
10use std::{fmt, io, io::Write as _, str::FromStr};
11
12/// Versioned algorithm identifier included as the fingerprint domain separator.
13pub const FINGERPRINT_ALGORITHM: &str = "weavatrix.refactor-plan.jcs-sha256.v1";
14
15const DOMAIN_SEPARATOR: &[u8] = b"weavatrix.refactor-plan.jcs-sha256.v1\0";
16
17/// A SHA-256 fingerprint over the validated JCS refactor-plan contract.
18#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub struct PlanFingerprint([u8; 32]);
20
21impl PlanFingerprint {
22    #[must_use]
23    pub const fn as_bytes(&self) -> &[u8; 32] {
24        &self.0
25    }
26
27    #[must_use]
28    pub fn to_hex(self) -> String {
29        let mut output = String::with_capacity(64);
30        for byte in self.0 {
31            use fmt::Write as _;
32            write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
33        }
34        output
35    }
36}
37
38impl fmt::Display for PlanFingerprint {
39    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40        formatter.write_str(&self.to_hex())
41    }
42}
43
44/// An invalid encoded plan fingerprint.
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct FingerprintParseError;
47
48impl fmt::Display for FingerprintParseError {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter.write_str("fingerprint must be 64 lowercase hexadecimal characters")
51    }
52}
53
54impl std::error::Error for FingerprintParseError {}
55
56impl FromStr for PlanFingerprint {
57    type Err = FingerprintParseError;
58
59    fn from_str(value: &str) -> Result<Self, Self::Err> {
60        if value.len() != 64
61            || !value
62                .bytes()
63                .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
64        {
65            return Err(FingerprintParseError);
66        }
67        let mut bytes = [0_u8; 32];
68        for (index, output) in bytes.iter_mut().enumerate() {
69            let start = index * 2;
70            *output = u8::from_str_radix(&value[start..start + 2], 16)
71                .map_err(|_| FingerprintParseError)?;
72        }
73        Ok(Self(bytes))
74    }
75}
76
77impl Serialize for PlanFingerprint {
78    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
79    where
80        S: Serializer,
81    {
82        serializer.serialize_str(&self.to_hex())
83    }
84}
85
86impl<'de> Deserialize<'de> for PlanFingerprint {
87    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
88    where
89        D: Deserializer<'de>,
90    {
91        struct FingerprintVisitor;
92
93        impl Visitor<'_> for FingerprintVisitor {
94            type Value = PlanFingerprint;
95
96            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97                formatter.write_str("a 64-character lowercase SHA-256 fingerprint")
98            }
99
100            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
101            where
102                E: serde::de::Error,
103            {
104                value.parse().map_err(E::custom)
105            }
106        }
107
108        deserializer.deserialize_str(FingerprintVisitor)
109    }
110}
111
112/// Validates with default limits and returns JCS bytes excluding top-level `createdAt`.
113pub fn canonical_plan_bytes(plan: &RefactorPlan) -> Result<Vec<u8>, PlanError> {
114    canonical_plan_bytes_with_limits(plan, RefactorPlanLimits::default())
115}
116
117/// Validates with caller limits and returns the canonical fingerprint payload.
118pub fn canonical_plan_bytes_with_limits(
119    plan: &RefactorPlan,
120    limits: RefactorPlanLimits,
121) -> Result<Vec<u8>, PlanError> {
122    crate::validation::validate_for_fingerprint(plan, limits)?;
123    let mut bytes = Vec::with_capacity(256);
124    write_canonical_top_level(&mut bytes, plan)?;
125    Ok(bytes)
126}
127
128/// Computes a validated, domain-separated JCS fingerprint with default limits.
129pub fn fingerprint_plan(plan: &RefactorPlan) -> Result<PlanFingerprint, PlanError> {
130    fingerprint_plan_with_limits(plan, RefactorPlanLimits::default())
131}
132
133/// Computes a validated, domain-separated JCS fingerprint with caller limits.
134pub fn fingerprint_plan_with_limits(
135    plan: &RefactorPlan,
136    limits: RefactorPlanLimits,
137) -> Result<PlanFingerprint, PlanError> {
138    crate::validation::validate_for_fingerprint(plan, limits)?;
139    fingerprint_validated(plan)
140}
141
142pub(crate) fn fingerprint_validated(plan: &RefactorPlan) -> Result<PlanFingerprint, PlanError> {
143    let mut digest = Sha256::new();
144    digest.update(DOMAIN_SEPARATOR);
145    let mut writer = io::BufWriter::with_capacity(8 * 1024, DigestWriter(&mut digest));
146    write_canonical_top_level(&mut writer, plan)?;
147    writer.flush().map_err(|error| io_error(&error))?;
148    drop(writer);
149    Ok(PlanFingerprint(digest.finalize().into()))
150}
151
152fn write_canonical_top_level<W: io::Write>(
153    writer: W,
154    plan: &RefactorPlan,
155) -> Result<(), PlanError> {
156    let mut entries = top_level_entries(plan);
157    entries
158        .sort_unstable_by(|left, right| crate::canonical::compare_utf16_order(left.key, right.key));
159    write_sorted_entries(writer, &entries).map_err(|error| canonical_error(&error))
160}
161
162fn write_sorted_entries<W: io::Write>(
163    writer: W,
164    entries: &[Entry<'_>],
165) -> Result<(), CanonicalError> {
166    let mut serializer = CanonicalSerializer::new(writer);
167    serializer.write_punctuation(b"{")?;
168    for (index, entry) in entries.iter().enumerate() {
169        if index != 0 {
170            serializer.write_punctuation(b",")?;
171        }
172        serializer.write_string(entry.key)?;
173        serializer.write_punctuation(b":")?;
174        serializer.write_value(&entry.value)?;
175    }
176    serializer.write_punctuation(b"}")
177}
178
179fn top_level_entries(plan: &RefactorPlan) -> Vec<Entry<'_>> {
180    let mut entries = vec![
181        Entry::new("schemaVersion", TopValue::String(&plan.schema_version)),
182        Entry::new("operation", TopValue::String(&plan.operation)),
183        Entry::new("operations", TopValue::Operations(&plan.operations)),
184    ];
185    if let Some(value) = &plan.completeness {
186        entries.push(Entry::new("completeness", TopValue::Completeness(value)));
187    }
188    push_evidence_entries(&mut entries, plan);
189    for (key, value) in &plan.evidence.extensions {
190        entries.push(Entry::new(key, TopValue::Extension(value)));
191    }
192    entries
193}
194
195fn push_evidence_entries<'a>(entries: &mut Vec<Entry<'a>>, plan: &'a RefactorPlan) {
196    let evidence = &plan.evidence;
197    if !evidence.graph_revision.is_missing() {
198        entries.push(Entry::new(
199            "graphRevision",
200            TopValue::GraphRevision(&evidence.graph_revision),
201        ));
202    }
203    if let Some(value) = &evidence.completeness_proof {
204        entries.push(Entry::new("completenessProof", TopValue::Proof(value)));
205    }
206    if let Some(value) = &evidence.uncertain_references {
207        entries.push(Entry::new(
208            "uncertainReferences",
209            TopValue::Uncertain(value),
210        ));
211    }
212    if let Some(value) = &evidence.not_modified {
213        entries.push(Entry::new("notModified", TopValue::Omitted(value)));
214    }
215    if let Some(value) = &evidence.warnings {
216        entries.push(Entry::new("warnings", TopValue::Warnings(value)));
217    }
218    if let Some(value) = &evidence.follow_up {
219        entries.push(Entry::new("followUp", TopValue::String(value)));
220    }
221    if let Some(value) = &evidence.syntax_check {
222        entries.push(Entry::new("syntaxCheck", TopValue::Status(value)));
223    }
224}
225
226struct Entry<'a> {
227    key: &'a str,
228    value: TopValue<'a>,
229}
230
231impl<'a> Entry<'a> {
232    const fn new(key: &'a str, value: TopValue<'a>) -> Self {
233        Self { key, value }
234    }
235}
236
237enum TopValue<'a> {
238    String(&'a str),
239    Operations(&'a [RefactorOperation]),
240    Completeness(&'a Completeness),
241    GraphRevision(&'a GraphRevision),
242    Proof(&'a CompletenessProof),
243    Uncertain(&'a [UncertainReference]),
244    Omitted(&'a [NotModified]),
245    Warnings(&'a [WarningCode]),
246    Status(&'a StatusCode),
247    Extension(&'a Value),
248}
249
250impl Serialize for TopValue<'_> {
251    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
252    where
253        S: Serializer,
254    {
255        match self {
256            Self::String(value) => value.serialize(serializer),
257            Self::Operations(value) => value.serialize(serializer),
258            Self::Completeness(value) => value.serialize(serializer),
259            Self::GraphRevision(value) => value.serialize(serializer),
260            Self::Proof(value) => value.serialize(serializer),
261            Self::Uncertain(value) => value.serialize(serializer),
262            Self::Omitted(value) => value.serialize(serializer),
263            Self::Warnings(value) => value.serialize(serializer),
264            Self::Status(value) => value.serialize(serializer),
265            Self::Extension(value) => value.serialize(serializer),
266        }
267    }
268}
269
270struct DigestWriter<'a>(&'a mut Sha256);
271
272impl io::Write for DigestWriter<'_> {
273    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
274        self.0.update(buffer);
275        Ok(buffer.len())
276    }
277
278    fn flush(&mut self) -> io::Result<()> {
279        Ok(())
280    }
281}
282
283fn canonical_error(error: &CanonicalError) -> PlanError {
284    PlanError::new(
285        PlanErrorCode::JsonEncoding,
286        format!("could not encode canonical plan JSON: {error}"),
287    )
288}
289
290fn io_error(error: &io::Error) -> PlanError {
291    PlanError::new(
292        PlanErrorCode::JsonEncoding,
293        format!("could not write canonical plan JSON: {error}"),
294    )
295}