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