Skip to main content

golem_schema/schema/
fingerprint.rs

1// Copyright 2024-2026 Golem Cloud
2//
3// Licensed under the Golem Source License v1.1 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://license.golem.cloud/LICENSE
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::schema::graph::{SchemaGraph, reachable_defs};
16use crate::schema::metadata::{MetadataEnvelope, Role};
17use crate::schema::schema_type::{
18    BinaryRestrictions, DiscriminatorRule, NumericBound, NumericRestrictions, PathDirection,
19    PathKind, QuantityValue, SchemaType, TextRestrictions, UrlRestrictions,
20};
21use crate::schema::validation::validate_graph;
22use crate::schema::{FromSchema, FromSchemaError, IntoSchema, SchemaBuilder, SchemaValue, TypeId};
23use serde::{Deserialize, Serialize};
24use std::collections::HashSet;
25
26const FINGERPRINT_DOMAIN: &str = "golem-schema-fingerprint";
27const FORMAT_VERSION: u64 = 1;
28
29/// BLAKE3-256 digest of the v1 deterministic-CBOR encoding of a stream element schema closure.
30#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
31#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
32#[cfg_attr(feature = "full", desert(transparent))]
33#[serde(transparent)]
34pub struct SchemaFingerprintV1(pub [u8; 32]);
35
36impl SchemaFingerprintV1 {
37    pub const FORMAT_VERSION: u8 = 1;
38
39    pub fn as_bytes(&self) -> &[u8; 32] {
40        &self.0
41    }
42
43    pub fn to_hex(self) -> String {
44        blake3::Hash::from_bytes(self.0).to_hex().to_string()
45    }
46}
47
48impl std::fmt::Display for SchemaFingerprintV1 {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.write_str(&blake3::Hash::from_bytes(self.0).to_hex())
51    }
52}
53
54impl IntoSchema for SchemaFingerprintV1 {
55    fn type_id() -> TypeId {
56        TypeId::new("golem_schema.SchemaFingerprintV1")
57    }
58
59    fn register_in(builder: &mut SchemaBuilder) -> SchemaType {
60        <Vec<u8> as IntoSchema>::register_in(builder)
61    }
62
63    fn to_value(&self) -> SchemaValue {
64        self.0.to_vec().to_value()
65    }
66}
67
68impl FromSchema for SchemaFingerprintV1 {
69    fn from_value(value: &SchemaValue) -> Result<Self, FromSchemaError> {
70        let bytes = Vec::<u8>::from_value(value)?;
71        let bytes: [u8; 32] = bytes.try_into().map_err(|bytes: Vec<u8>| {
72            FromSchemaError::custom(format!(
73                "schema fingerprint must have 32 bytes, got {}",
74                bytes.len()
75            ))
76        })?;
77        Ok(Self(bytes))
78    }
79}
80
81#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
82pub enum SchemaFingerprintError {
83    #[error("invalid stream element schema: {0}")]
84    InvalidSchema(String),
85    #[error("duplicate value `{value}` in set-valued schema field `{field}`")]
86    DuplicateSetValue { field: &'static str, value: String },
87}
88
89/// Computes the v1 stream element schema fingerprint.
90///
91/// `None` represents the Component Model stream unit element. The input graph may be the
92/// multi-root definition registry of an agent schema; only definitions reachable from `element`
93/// are encoded.
94pub fn schema_fingerprint_v1(
95    graph: &SchemaGraph,
96    element: Option<&SchemaType>,
97) -> Result<SchemaFingerprintV1, SchemaFingerprintError> {
98    let bytes = canonical_schema_bytes_v1(graph, element)?;
99    Ok(SchemaFingerprintV1(*blake3::hash(&bytes).as_bytes()))
100}
101
102/// Resolves a stream element fingerprint against a pinned multi-root schema registry.
103///
104/// The returned graph is self-contained: its root is the matching element type and its
105/// definitions are the transitive closure reachable from that root. A unit stream element is
106/// represented by an empty tuple root.
107pub fn resolve_stream_element_schema_v1(
108    graph: &SchemaGraph,
109    fingerprint: SchemaFingerprintV1,
110) -> Result<Option<SchemaGraph>, SchemaFingerprintError> {
111    let mut elements = Vec::new();
112    collect_stream_elements(&graph.root, &mut elements);
113    for definition in &graph.defs {
114        collect_stream_elements(&definition.body, &mut elements);
115    }
116    for element in elements {
117        if schema_fingerprint_v1(graph, element)? == fingerprint {
118            let root = element.cloned().unwrap_or_else(synthetic_unit);
119            return Ok(Some(SchemaGraph {
120                defs: reachable_defs(graph, &root),
121                root,
122            }));
123        }
124    }
125    Ok(None)
126}
127
128fn collect_stream_elements<'a>(ty: &'a SchemaType, elements: &mut Vec<Option<&'a SchemaType>>) {
129    match ty {
130        SchemaType::Record { fields, .. } => {
131            for field in fields {
132                collect_stream_elements(&field.body, elements);
133            }
134        }
135        SchemaType::Variant { cases, .. } => {
136            for case in cases {
137                if let Some(payload) = &case.payload {
138                    collect_stream_elements(payload, elements);
139                }
140            }
141        }
142        SchemaType::Tuple {
143            elements: tuple_elements,
144            ..
145        } => {
146            for element in tuple_elements {
147                collect_stream_elements(element, elements);
148            }
149        }
150        SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
151            collect_stream_elements(element, elements);
152        }
153        SchemaType::Map { key, value, .. } => {
154            collect_stream_elements(key, elements);
155            collect_stream_elements(value, elements);
156        }
157        SchemaType::Option { inner, .. } => collect_stream_elements(inner, elements),
158        SchemaType::Result { spec, .. } => {
159            if let Some(ok) = &spec.ok {
160                collect_stream_elements(ok, elements);
161            }
162            if let Some(err) = &spec.err {
163                collect_stream_elements(err, elements);
164            }
165        }
166        SchemaType::Union { spec, .. } => {
167            for branch in &spec.branches {
168                collect_stream_elements(&branch.body, elements);
169            }
170        }
171        SchemaType::Future { inner, .. } => {
172            if let Some(inner) = inner {
173                collect_stream_elements(inner, elements);
174            }
175        }
176        SchemaType::Stream { inner, .. } => {
177            elements.push(inner.as_deref());
178            if let Some(inner) = inner {
179                collect_stream_elements(inner, elements);
180            }
181        }
182        SchemaType::Secret { spec, .. } => collect_stream_elements(&spec.inner, elements),
183        SchemaType::Ref { .. }
184        | SchemaType::Bool { .. }
185        | SchemaType::S8 { .. }
186        | SchemaType::S16 { .. }
187        | SchemaType::S32 { .. }
188        | SchemaType::S64 { .. }
189        | SchemaType::U8 { .. }
190        | SchemaType::U16 { .. }
191        | SchemaType::U32 { .. }
192        | SchemaType::U64 { .. }
193        | SchemaType::F32 { .. }
194        | SchemaType::F64 { .. }
195        | SchemaType::Char { .. }
196        | SchemaType::String { .. }
197        | SchemaType::Enum { .. }
198        | SchemaType::Flags { .. }
199        | SchemaType::Text { .. }
200        | SchemaType::Binary { .. }
201        | SchemaType::Path { .. }
202        | SchemaType::Url { .. }
203        | SchemaType::Uuid { .. }
204        | SchemaType::Datetime { .. }
205        | SchemaType::Duration { .. }
206        | SchemaType::Quantity { .. }
207        | SchemaType::QuotaToken { .. }
208        | SchemaType::PermissionCard { .. } => {}
209    }
210}
211
212fn canonical_schema_bytes_v1(
213    graph: &SchemaGraph,
214    element: Option<&SchemaType>,
215) -> Result<Vec<u8>, SchemaFingerprintError> {
216    let root = element.cloned().unwrap_or_else(synthetic_unit);
217    let is_synthetic_unit = element.is_none();
218
219    let mut seen = HashSet::new();
220    for def in &graph.defs {
221        if !seen.insert(def.id.as_str()) {
222            return Err(SchemaFingerprintError::InvalidSchema(format!(
223                "duplicate type id `{}`",
224                def.id
225            )));
226        }
227    }
228
229    let mut defs = reachable_defs(graph, &root);
230    defs.sort_by(|left, right| {
231        left.id
232            .as_str()
233            .as_bytes()
234            .cmp(right.id.as_str().as_bytes())
235    });
236    let projected = SchemaGraph {
237        defs: defs.clone(),
238        root: root.clone(),
239    };
240    if let Err(errors) = validate_graph(&projected) {
241        return Err(SchemaFingerprintError::InvalidSchema(
242            errors
243                .into_iter()
244                .map(|error| error.to_string())
245                .collect::<Vec<_>>()
246                .join("; "),
247        ));
248    }
249
250    let mut encoder = CborEncoder::default();
251    encoder.array(4);
252    encoder.text(FINGERPRINT_DOMAIN);
253    encoder.unsigned(FORMAT_VERSION);
254    if is_synthetic_unit {
255        encoder.array(2);
256        encoder.unsigned(0);
257        encode_metadata(&mut encoder, &MetadataEnvelope::default())?;
258    } else {
259        encode_type(&mut encoder, &root)?;
260    }
261    encoder.array(defs.len());
262    for def in defs {
263        encoder.array(3);
264        encoder.text(def.id.as_str());
265        encoder.optional_text(def.name.as_deref());
266        encode_type(&mut encoder, &def.body)?;
267    }
268
269    Ok(encoder.bytes)
270}
271
272fn synthetic_unit() -> SchemaType {
273    SchemaType::Tuple {
274        elements: Vec::new(),
275        metadata: MetadataEnvelope::default(),
276    }
277}
278
279fn encode_type(
280    encoder: &mut CborEncoder,
281    schema: &SchemaType,
282) -> Result<(), SchemaFingerprintError> {
283    match schema {
284        SchemaType::Ref { id, metadata } => {
285            encoder.array(3);
286            encoder.unsigned(1);
287            encoder.text(id.as_str());
288            encode_metadata(encoder, metadata)?;
289        }
290        SchemaType::Bool { metadata } => encode_leaf(encoder, 2, metadata)?,
291        SchemaType::S8 {
292            restrictions,
293            metadata,
294        } => encode_numeric(encoder, 3, restrictions.as_ref(), metadata)?,
295        SchemaType::S16 {
296            restrictions,
297            metadata,
298        } => encode_numeric(encoder, 4, restrictions.as_ref(), metadata)?,
299        SchemaType::S32 {
300            restrictions,
301            metadata,
302        } => encode_numeric(encoder, 5, restrictions.as_ref(), metadata)?,
303        SchemaType::S64 {
304            restrictions,
305            metadata,
306        } => encode_numeric(encoder, 6, restrictions.as_ref(), metadata)?,
307        SchemaType::U8 {
308            restrictions,
309            metadata,
310        } => encode_numeric(encoder, 7, restrictions.as_ref(), metadata)?,
311        SchemaType::U16 {
312            restrictions,
313            metadata,
314        } => encode_numeric(encoder, 8, restrictions.as_ref(), metadata)?,
315        SchemaType::U32 {
316            restrictions,
317            metadata,
318        } => encode_numeric(encoder, 9, restrictions.as_ref(), metadata)?,
319        SchemaType::U64 {
320            restrictions,
321            metadata,
322        } => encode_numeric(encoder, 10, restrictions.as_ref(), metadata)?,
323        SchemaType::F32 {
324            restrictions,
325            metadata,
326        } => encode_numeric(encoder, 11, restrictions.as_ref(), metadata)?,
327        SchemaType::F64 {
328            restrictions,
329            metadata,
330        } => encode_numeric(encoder, 12, restrictions.as_ref(), metadata)?,
331        SchemaType::Char { metadata } => encode_leaf(encoder, 13, metadata)?,
332        SchemaType::String { metadata } => encode_leaf(encoder, 14, metadata)?,
333        SchemaType::Record { fields, metadata } => {
334            encoder.array(3);
335            encoder.unsigned(15);
336            encoder.array(fields.len());
337            for field in fields {
338                encoder.array(3);
339                encoder.text(&field.name);
340                encode_type(encoder, &field.body)?;
341                encode_metadata(encoder, &field.metadata)?;
342            }
343            encode_metadata(encoder, metadata)?;
344        }
345        SchemaType::Variant { cases, metadata } => {
346            encoder.array(3);
347            encoder.unsigned(16);
348            encoder.array(cases.len());
349            for case in cases {
350                encoder.array(3);
351                encoder.text(&case.name);
352                encode_optional_type(encoder, case.payload.as_ref())?;
353                encode_metadata(encoder, &case.metadata)?;
354            }
355            encode_metadata(encoder, metadata)?;
356        }
357        SchemaType::Enum { cases, metadata } => {
358            encode_names(encoder, 17, cases, metadata)?;
359        }
360        SchemaType::Flags { flags, metadata } => {
361            encode_names(encoder, 18, flags, metadata)?;
362        }
363        SchemaType::Tuple { elements, metadata } => {
364            encoder.array(3);
365            encoder.unsigned(19);
366            encoder.array(elements.len());
367            for element in elements {
368                encode_type(encoder, element)?;
369            }
370            encode_metadata(encoder, metadata)?;
371        }
372        SchemaType::List { element, metadata } => {
373            encode_unary(encoder, 20, element, metadata)?;
374        }
375        SchemaType::FixedList {
376            element,
377            length,
378            metadata,
379        } => {
380            encoder.array(4);
381            encoder.unsigned(21);
382            encode_type(encoder, element)?;
383            encoder.unsigned(u64::from(*length));
384            encode_metadata(encoder, metadata)?;
385        }
386        SchemaType::Map {
387            key,
388            value,
389            metadata,
390        } => {
391            encoder.array(4);
392            encoder.unsigned(22);
393            encode_type(encoder, key)?;
394            encode_type(encoder, value)?;
395            encode_metadata(encoder, metadata)?;
396        }
397        SchemaType::Option { inner, metadata } => {
398            encode_unary(encoder, 23, inner, metadata)?;
399        }
400        SchemaType::Result { spec, metadata } => {
401            encoder.array(4);
402            encoder.unsigned(24);
403            encode_optional_type(encoder, spec.ok.as_deref())?;
404            encode_optional_type(encoder, spec.err.as_deref())?;
405            encode_metadata(encoder, metadata)?;
406        }
407        SchemaType::Text {
408            restrictions,
409            metadata,
410        } => encode_text(encoder, restrictions, metadata)?,
411        SchemaType::Binary {
412            restrictions,
413            metadata,
414        } => encode_binary(encoder, restrictions, metadata)?,
415        SchemaType::Path { spec, metadata } => {
416            encoder.array(6);
417            encoder.unsigned(27);
418            encoder.unsigned(match spec.direction {
419                PathDirection::Input => 0,
420                PathDirection::Output => 1,
421                PathDirection::InOut => 2,
422            });
423            encoder.unsigned(match spec.kind {
424                PathKind::File => 0,
425                PathKind::Directory => 1,
426                PathKind::Any => 2,
427            });
428            encode_optional_set(
429                encoder,
430                "path.allowed_mime_types",
431                spec.allowed_mime_types.as_ref(),
432            )?;
433            encode_optional_set(
434                encoder,
435                "path.allowed_extensions",
436                spec.allowed_extensions.as_ref(),
437            )?;
438            encode_metadata(encoder, metadata)?;
439        }
440        SchemaType::Url {
441            restrictions,
442            metadata,
443        } => encode_url(encoder, restrictions, metadata)?,
444        SchemaType::Uuid { metadata } => encode_leaf(encoder, 38, metadata)?,
445        SchemaType::Datetime { metadata } => encode_leaf(encoder, 29, metadata)?,
446        SchemaType::Duration { metadata } => encode_leaf(encoder, 30, metadata)?,
447        SchemaType::Quantity { spec, metadata } => {
448            encoder.array(6);
449            encoder.unsigned(31);
450            encoder.text(&spec.base_unit);
451            encoder.array(spec.allowed_suffixes.len());
452            for suffix in &spec.allowed_suffixes {
453                encoder.text(suffix);
454            }
455            encode_optional_quantity(encoder, spec.min.as_ref());
456            encode_optional_quantity(encoder, spec.max.as_ref());
457            encode_metadata(encoder, metadata)?;
458        }
459        SchemaType::Union { spec, metadata } => {
460            encoder.array(3);
461            encoder.unsigned(32);
462            encoder.array(spec.branches.len());
463            for branch in &spec.branches {
464                encoder.array(4);
465                encoder.text(&branch.tag);
466                encode_type(encoder, &branch.body)?;
467                encode_discriminator(encoder, &branch.discriminator);
468                encode_metadata(encoder, &branch.metadata)?;
469            }
470            encode_metadata(encoder, metadata)?;
471        }
472        SchemaType::Secret { spec, metadata } => {
473            encoder.array(4);
474            encoder.unsigned(33);
475            encode_type(encoder, &spec.inner)?;
476            encoder.optional_text(spec.category.as_deref());
477            encode_metadata(encoder, metadata)?;
478        }
479        SchemaType::QuotaToken { spec, metadata } => {
480            encoder.array(3);
481            encoder.unsigned(34);
482            encoder.optional_text(spec.resource_name.as_deref());
483            encode_metadata(encoder, metadata)?;
484        }
485        SchemaType::Future { inner, metadata } => {
486            encoder.array(3);
487            encoder.unsigned(35);
488            encode_optional_type(encoder, inner.as_deref())?;
489            encode_metadata(encoder, metadata)?;
490        }
491        SchemaType::Stream { inner, metadata } => {
492            encoder.array(3);
493            encoder.unsigned(36);
494            encode_optional_type(encoder, inner.as_deref())?;
495            encode_metadata(encoder, metadata)?;
496        }
497        SchemaType::PermissionCard { spec, metadata } => {
498            encoder.array(3);
499            encoder.unsigned(37);
500            encoder.boolean(spec.polymorphic);
501            encode_metadata(encoder, metadata)?;
502        }
503    }
504    Ok(())
505}
506
507fn encode_leaf(
508    encoder: &mut CborEncoder,
509    tag: u64,
510    metadata: &MetadataEnvelope,
511) -> Result<(), SchemaFingerprintError> {
512    encoder.array(2);
513    encoder.unsigned(tag);
514    encode_metadata(encoder, metadata)
515}
516
517fn encode_numeric(
518    encoder: &mut CborEncoder,
519    tag: u64,
520    restrictions: Option<&NumericRestrictions>,
521    metadata: &MetadataEnvelope,
522) -> Result<(), SchemaFingerprintError> {
523    encoder.array(3);
524    encoder.unsigned(tag);
525    match restrictions {
526        None => encoder.null(),
527        Some(restrictions) => {
528            encoder.array(3);
529            encode_optional_bound(encoder, restrictions.min);
530            encode_optional_bound(encoder, restrictions.max);
531            encoder.optional_text(restrictions.unit.as_deref().filter(|unit| !unit.is_empty()));
532        }
533    }
534    encode_metadata(encoder, metadata)
535}
536
537fn encode_optional_bound(encoder: &mut CborEncoder, bound: Option<NumericBound>) {
538    match bound {
539        None => encoder.null(),
540        Some(NumericBound::Signed(value)) => {
541            encoder.array(2);
542            encoder.unsigned(0);
543            encoder.signed(value);
544        }
545        Some(NumericBound::Unsigned(value)) => {
546            encoder.array(2);
547            encoder.unsigned(1);
548            encoder.unsigned(value);
549        }
550        Some(NumericBound::FloatBits(bits)) => {
551            encoder.array(2);
552            encoder.unsigned(2);
553            encoder.unsigned(if f64::from_bits(bits) == 0.0 { 0 } else { bits });
554        }
555    }
556}
557
558fn encode_names(
559    encoder: &mut CborEncoder,
560    tag: u64,
561    names: &[String],
562    metadata: &MetadataEnvelope,
563) -> Result<(), SchemaFingerprintError> {
564    encoder.array(3);
565    encoder.unsigned(tag);
566    encoder.array(names.len());
567    for name in names {
568        encoder.text(name);
569    }
570    encode_metadata(encoder, metadata)
571}
572
573fn encode_unary(
574    encoder: &mut CborEncoder,
575    tag: u64,
576    inner: &SchemaType,
577    metadata: &MetadataEnvelope,
578) -> Result<(), SchemaFingerprintError> {
579    encoder.array(3);
580    encoder.unsigned(tag);
581    encode_type(encoder, inner)?;
582    encode_metadata(encoder, metadata)
583}
584
585fn encode_optional_type(
586    encoder: &mut CborEncoder,
587    schema: Option<&SchemaType>,
588) -> Result<(), SchemaFingerprintError> {
589    match schema {
590        Some(schema) => encode_type(encoder, schema),
591        None => {
592            encoder.null();
593            Ok(())
594        }
595    }
596}
597
598fn encode_text(
599    encoder: &mut CborEncoder,
600    restrictions: &TextRestrictions,
601    metadata: &MetadataEnvelope,
602) -> Result<(), SchemaFingerprintError> {
603    encoder.array(6);
604    encoder.unsigned(25);
605    encode_optional_set(encoder, "text.languages", restrictions.languages.as_ref())?;
606    encoder.optional_u32(restrictions.min_length);
607    encoder.optional_u32(restrictions.max_length);
608    encoder.optional_text(restrictions.regex.as_deref());
609    encode_metadata(encoder, metadata)
610}
611
612fn encode_binary(
613    encoder: &mut CborEncoder,
614    restrictions: &BinaryRestrictions,
615    metadata: &MetadataEnvelope,
616) -> Result<(), SchemaFingerprintError> {
617    encoder.array(5);
618    encoder.unsigned(26);
619    encode_optional_set(
620        encoder,
621        "binary.mime_types",
622        restrictions.mime_types.as_ref(),
623    )?;
624    encoder.optional_u32(restrictions.min_bytes);
625    encoder.optional_u32(restrictions.max_bytes);
626    encode_metadata(encoder, metadata)
627}
628
629fn encode_url(
630    encoder: &mut CborEncoder,
631    restrictions: &UrlRestrictions,
632    metadata: &MetadataEnvelope,
633) -> Result<(), SchemaFingerprintError> {
634    encoder.array(4);
635    encoder.unsigned(28);
636    encode_optional_set(
637        encoder,
638        "url.allowed_schemes",
639        restrictions.allowed_schemes.as_ref(),
640    )?;
641    encode_optional_set(
642        encoder,
643        "url.allowed_hosts",
644        restrictions.allowed_hosts.as_ref(),
645    )?;
646    encode_metadata(encoder, metadata)
647}
648
649fn encode_optional_set(
650    encoder: &mut CborEncoder,
651    field: &'static str,
652    values: Option<&Vec<String>>,
653) -> Result<(), SchemaFingerprintError> {
654    let Some(values) = values else {
655        encoder.null();
656        return Ok(());
657    };
658    let mut values: Vec<&str> = values.iter().map(String::as_str).collect();
659    values.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
660    for duplicate in values.windows(2) {
661        if duplicate[0] == duplicate[1] {
662            return Err(SchemaFingerprintError::DuplicateSetValue {
663                field,
664                value: duplicate[0].to_string(),
665            });
666        }
667    }
668    encoder.array(values.len());
669    for value in values {
670        encoder.text(value);
671    }
672    Ok(())
673}
674
675fn encode_optional_quantity(encoder: &mut CborEncoder, value: Option<&QuantityValue>) {
676    match value {
677        None => encoder.null(),
678        Some(value) => {
679            encoder.array(3);
680            encoder.signed(value.mantissa);
681            encoder.signed(i64::from(value.scale));
682            encoder.text(&value.unit);
683        }
684    }
685}
686
687fn encode_discriminator(encoder: &mut CborEncoder, discriminator: &DiscriminatorRule) {
688    match discriminator {
689        DiscriminatorRule::Prefix { prefix } => {
690            encoder.array(2);
691            encoder.unsigned(0);
692            encoder.text(prefix);
693        }
694        DiscriminatorRule::Suffix { suffix } => {
695            encoder.array(2);
696            encoder.unsigned(1);
697            encoder.text(suffix);
698        }
699        DiscriminatorRule::Contains { substring } => {
700            encoder.array(2);
701            encoder.unsigned(2);
702            encoder.text(substring);
703        }
704        DiscriminatorRule::Regex { regex } => {
705            encoder.array(2);
706            encoder.unsigned(3);
707            encoder.text(regex);
708        }
709        DiscriminatorRule::FieldEquals(field) => {
710            encoder.array(3);
711            encoder.unsigned(4);
712            encoder.text(&field.field_name);
713            encoder.optional_text(field.literal.as_deref());
714        }
715        DiscriminatorRule::FieldAbsent { field_name } => {
716            encoder.array(2);
717            encoder.unsigned(5);
718            encoder.text(field_name);
719        }
720    }
721}
722
723fn encode_metadata(
724    encoder: &mut CborEncoder,
725    metadata: &MetadataEnvelope,
726) -> Result<(), SchemaFingerprintError> {
727    encoder.array(5);
728    encoder.optional_text(metadata.doc.as_deref());
729    encode_set(encoder, "metadata.aliases", &metadata.aliases)?;
730    encoder.array(metadata.examples.len());
731    for example in &metadata.examples {
732        encoder.text(example);
733    }
734    encoder.optional_text(metadata.deprecated.as_deref());
735    match &metadata.role {
736        None => encoder.null(),
737        Some(Role::Multimodal) => {
738            encoder.array(1);
739            encoder.unsigned(0);
740        }
741        Some(Role::UnstructuredText) => {
742            encoder.array(1);
743            encoder.unsigned(1);
744        }
745        Some(Role::UnstructuredBinary) => {
746            encoder.array(1);
747            encoder.unsigned(2);
748        }
749        Some(Role::Other(role)) => {
750            encoder.array(2);
751            encoder.unsigned(3);
752            encoder.text(role);
753        }
754    }
755    Ok(())
756}
757
758fn encode_set(
759    encoder: &mut CborEncoder,
760    field: &'static str,
761    values: &[String],
762) -> Result<(), SchemaFingerprintError> {
763    let mut values: Vec<&str> = values.iter().map(String::as_str).collect();
764    values.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
765    for duplicate in values.windows(2) {
766        if duplicate[0] == duplicate[1] {
767            return Err(SchemaFingerprintError::DuplicateSetValue {
768                field,
769                value: duplicate[0].to_string(),
770            });
771        }
772    }
773    encoder.array(values.len());
774    for value in values {
775        encoder.text(value);
776    }
777    Ok(())
778}
779
780#[derive(Default)]
781struct CborEncoder {
782    bytes: Vec<u8>,
783}
784
785impl CborEncoder {
786    fn unsigned(&mut self, value: u64) {
787        self.major(0, value);
788    }
789
790    fn signed(&mut self, value: i64) {
791        if value >= 0 {
792            self.unsigned(value as u64);
793        } else {
794            self.major(1, (-1i128 - i128::from(value)) as u64);
795        }
796    }
797
798    fn array(&mut self, length: usize) {
799        self.major(4, length as u64);
800    }
801
802    fn text(&mut self, value: &str) {
803        self.major(3, value.len() as u64);
804        self.bytes.extend_from_slice(value.as_bytes());
805    }
806
807    fn optional_text(&mut self, value: Option<&str>) {
808        match value {
809            Some(value) => self.text(value),
810            None => self.null(),
811        }
812    }
813
814    fn optional_u32(&mut self, value: Option<u32>) {
815        match value {
816            Some(value) => self.unsigned(u64::from(value)),
817            None => self.null(),
818        }
819    }
820
821    fn boolean(&mut self, value: bool) {
822        self.bytes.push(if value { 0xf5 } else { 0xf4 });
823    }
824
825    fn null(&mut self) {
826        self.bytes.push(0xf6);
827    }
828
829    fn major(&mut self, major: u8, value: u64) {
830        let prefix = major << 5;
831        match value {
832            0..=23 => self.bytes.push(prefix | value as u8),
833            24..=0xff => self.bytes.extend_from_slice(&[prefix | 24, value as u8]),
834            0x100..=0xffff => {
835                self.bytes.push(prefix | 25);
836                self.bytes.extend_from_slice(&(value as u16).to_be_bytes());
837            }
838            0x1_0000..=0xffff_ffff => {
839                self.bytes.push(prefix | 26);
840                self.bytes.extend_from_slice(&(value as u32).to_be_bytes());
841            }
842            _ => {
843                self.bytes.push(prefix | 27);
844                self.bytes.extend_from_slice(&value.to_be_bytes());
845            }
846        }
847    }
848}
849
850#[cfg(test)]
851mod tests {
852    use super::{
853        SchemaFingerprintError, SchemaFingerprintV1, canonical_schema_bytes_v1,
854        resolve_stream_element_schema_v1, schema_fingerprint_v1,
855    };
856    use crate::schema::{
857        MetadataEnvelope, NamedFieldType, PermissionCardSpec, SchemaGraph, SchemaType,
858        SchemaTypeDef, TypeId,
859    };
860    #[cfg(feature = "regex")]
861    use crate::schema::{Role, TextRestrictions};
862    use test_r::test;
863
864    #[test]
865    fn v1_golden_vectors() {
866        let graph = SchemaGraph::empty();
867        assert_eq!(canonical_schema_bytes_v1(&graph, None).unwrap().len(), 37);
868        assert_eq!(
869            schema_fingerprint_v1(&graph, None).unwrap().to_hex(),
870            "b50494cf0f33961c703d5f6e6af3d3159e528c4d09c1d801172cdf8f022dcafa"
871        );
872        assert_eq!(
873            canonical_schema_bytes_v1(&graph, Some(&SchemaType::string()))
874                .unwrap()
875                .len(),
876            37
877        );
878        assert_eq!(
879            schema_fingerprint_v1(&graph, Some(&SchemaType::string()))
880                .unwrap()
881                .to_hex(),
882            "61c50c0a3c6ffd63529621ada78afc0d4d8e5fe691f8b0993035f847c660a307"
883        );
884        assert_eq!(
885            canonical_schema_bytes_v1(&graph, Some(&SchemaType::list(SchemaType::string())))
886                .unwrap()
887                .len(),
888            45
889        );
890        assert_eq!(
891            schema_fingerprint_v1(&graph, Some(&SchemaType::list(SchemaType::string())))
892                .unwrap()
893                .to_hex(),
894            "4939707f8ef97e9d4b31b568332eaf5a3011f2be7c358f7546966fadfb9416d4"
895        );
896
897        let node_id = TypeId::new("example.node");
898        let recursive = SchemaGraph {
899            root: SchemaType::ref_to(node_id.clone()),
900            defs: vec![SchemaTypeDef {
901                id: node_id.clone(),
902                name: Some("Node".to_string()),
903                body: SchemaType::record(vec![
904                    NamedFieldType {
905                        name: "value".to_string(),
906                        body: SchemaType::string(),
907                        metadata: MetadataEnvelope::default(),
908                    },
909                    NamedFieldType {
910                        name: "next".to_string(),
911                        body: SchemaType::option(SchemaType::ref_to(node_id)),
912                        metadata: MetadataEnvelope::default(),
913                    },
914                ]),
915            }],
916        };
917        assert_eq!(
918            canonical_schema_bytes_v1(&recursive, Some(&recursive.root))
919                .unwrap()
920                .len(),
921            140
922        );
923        assert_eq!(
924            schema_fingerprint_v1(&recursive, Some(&recursive.root))
925                .unwrap()
926                .to_hex(),
927            "3931585d2d02a2b7d5c99e3da1082ac8fe904c535e2700bd45e29a95ff2399fa"
928        );
929    }
930
931    #[test]
932    #[cfg(feature = "regex")]
933    fn v1_regex_golden_vector() {
934        let constrained = SchemaType::Text {
935            restrictions: TextRestrictions {
936                languages: Some(vec!["fr".to_string(), "en".to_string()]),
937                min_length: Some(1),
938                max_length: Some(64),
939                regex: Some("^[a-z]+$".to_string()),
940            },
941            metadata: MetadataEnvelope {
942                doc: Some("text".to_string()),
943                aliases: vec!["z".to_string(), "a".to_string()],
944                examples: vec!["\"hello\"".to_string()],
945                deprecated: Some("use-v2".to_string()),
946                role: Some(Role::Other("prompt".to_string())),
947            },
948        };
949        let constrained_bytes =
950            canonical_schema_bytes_v1(&SchemaGraph::empty(), Some(&constrained)).unwrap();
951        let constrained_hex = constrained_bytes
952            .iter()
953            .map(|byte| format!("{byte:02x}"))
954            .collect::<String>();
955        assert_eq!(
956            constrained_hex,
957            "847818676f6c656d2d736368656d612d66696e6765727072696e74018618198262656e626672011840685e5b612d7a5d2b24856474657874826161617a81672268656c6c6f22667573652d763282036670726f6d707480"
958        );
959        assert_eq!(constrained_bytes.len(), 87);
960        assert_eq!(
961            blake3::hash(&constrained_bytes).to_hex().as_str(),
962            "b985cdb5445862be90e8dca06bbfa9c46b50cf40edc84ed34205bb3a214c5bb0"
963        );
964    }
965
966    #[test]
967    fn v1_permission_card_golden_vector() {
968        let permission_card = SchemaType::permission_card(PermissionCardSpec { polymorphic: true });
969        let permission_card_bytes =
970            canonical_schema_bytes_v1(&SchemaGraph::empty(), Some(&permission_card)).unwrap();
971        assert_eq!(
972            permission_card_bytes
973                .iter()
974                .map(|byte| format!("{byte:02x}"))
975                .collect::<String>(),
976            "847818676f6c656d2d736368656d612d66696e6765727072696e7401831825f585f68080f6f680"
977        );
978        assert_eq!(permission_card_bytes.len(), 39);
979        assert_eq!(
980            blake3::hash(&permission_card_bytes).to_hex().as_str(),
981            "b7d3c09af5db4e56b527051f561689f451dfdb21213cd70aabd11618e244da8b"
982        );
983    }
984
985    #[test]
986    fn sorts_set_values_and_rejects_duplicates() {
987        let graph = SchemaGraph::empty();
988        let left = SchemaType::Text {
989            restrictions: crate::schema::TextRestrictions {
990                languages: Some(vec!["fr".into(), "en".into()]),
991                ..Default::default()
992            },
993            metadata: MetadataEnvelope::default(),
994        };
995        let right = SchemaType::Text {
996            restrictions: crate::schema::TextRestrictions {
997                languages: Some(vec!["en".into(), "fr".into()]),
998                ..Default::default()
999            },
1000            metadata: MetadataEnvelope::default(),
1001        };
1002        assert_eq!(
1003            schema_fingerprint_v1(&graph, Some(&left)).unwrap(),
1004            schema_fingerprint_v1(&graph, Some(&right)).unwrap()
1005        );
1006
1007        let duplicate = SchemaType::Text {
1008            restrictions: crate::schema::TextRestrictions {
1009                languages: Some(vec!["en".into(), "en".into()]),
1010                ..Default::default()
1011            },
1012            metadata: MetadataEnvelope::default(),
1013        };
1014        assert!(matches!(
1015            schema_fingerprint_v1(&graph, Some(&duplicate)),
1016            Err(SchemaFingerprintError::DuplicateSetValue { .. })
1017        ));
1018    }
1019
1020    #[test]
1021    fn resolves_stream_element_fingerprints_to_self_contained_schema_graphs() {
1022        let node_id = TypeId::new("example.node");
1023        let node_ref = SchemaType::ref_to(node_id.clone());
1024        let graph = SchemaGraph {
1025            root: SchemaType::record(vec![
1026                NamedFieldType {
1027                    name: "nodes".to_string(),
1028                    body: SchemaType::stream(Some(node_ref.clone())),
1029                    metadata: MetadataEnvelope::default(),
1030                },
1031                NamedFieldType {
1032                    name: "signals".to_string(),
1033                    body: SchemaType::stream(None),
1034                    metadata: MetadataEnvelope::default(),
1035                },
1036            ]),
1037            defs: vec![SchemaTypeDef {
1038                id: node_id.clone(),
1039                name: Some("Node".to_string()),
1040                body: SchemaType::record(vec![NamedFieldType {
1041                    name: "next".to_string(),
1042                    body: SchemaType::option(SchemaType::ref_to(node_id)),
1043                    metadata: MetadataEnvelope::default(),
1044                }]),
1045            }],
1046        };
1047
1048        let node_fingerprint = schema_fingerprint_v1(&graph, Some(&node_ref)).unwrap();
1049        let resolved = resolve_stream_element_schema_v1(&graph, node_fingerprint)
1050            .unwrap()
1051            .unwrap();
1052        assert_eq!(resolved.root, node_ref);
1053        assert_eq!(resolved.defs, graph.defs);
1054
1055        let unit_fingerprint = schema_fingerprint_v1(&graph, None).unwrap();
1056        let resolved_unit = resolve_stream_element_schema_v1(&graph, unit_fingerprint)
1057            .unwrap()
1058            .unwrap();
1059        assert_eq!(
1060            resolved_unit,
1061            SchemaGraph::anonymous(SchemaType::tuple(Vec::new()))
1062        );
1063
1064        assert!(
1065            resolve_stream_element_schema_v1(&graph, SchemaFingerprintV1([0xff; 32]))
1066                .unwrap()
1067                .is_none()
1068        );
1069    }
1070}