1use crate::schema::graph::{GraphIndex, SchemaGraph};
19use crate::schema::metadata::TypeId;
20use crate::schema::schema_type::{
21 BinaryRestrictions, DiscriminatorRule, NumericBound, NumericRepr, NumericRestrictionError,
22 NumericRestrictions, PathSpec, QuantitySpec, QuantityValue, SchemaType, TextRestrictions,
23 UnionBranch, UrlRestrictions, numeric_bound_cmp,
24};
25#[cfg(not(all(feature = "guest", not(feature = "host"))))]
26use crate::schema::schema_value::SecretValuePayload;
27use crate::schema::schema_value::{
28 BinaryValuePayload, ResultValuePayload, SchemaValue, TextValuePayload,
29};
30#[cfg(not(all(feature = "guest", not(feature = "host"))))]
33use crate::schema::schema_type::{PermissionCardSpec, QuotaTokenSpec, SecretSpec};
34#[cfg(not(all(feature = "guest", not(feature = "host"))))]
35use crate::schema::schema_value::{PermissionCardValuePayload, QuotaTokenValuePayload};
36use std::cmp::Ordering;
37use std::error::Error;
38use std::fmt::{self, Display, Formatter, Write};
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum ValuePathSegment {
43 Field(String),
44 Index(usize),
45 VariantPayload,
46 OptionInner,
47 ResultOk,
48 ResultErr,
49 UnionBody,
50 MapKey(usize),
51 MapValue(usize),
52}
53
54impl Display for ValuePathSegment {
55 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
56 match self {
57 ValuePathSegment::Field(n) => write!(f, ".field({n:?})"),
58 ValuePathSegment::Index(i) => write!(f, ".index({i})"),
59 ValuePathSegment::VariantPayload => f.write_str(".variant_payload"),
60 ValuePathSegment::OptionInner => f.write_str(".option_inner"),
61 ValuePathSegment::ResultOk => f.write_str(".ok"),
62 ValuePathSegment::ResultErr => f.write_str(".err"),
63 ValuePathSegment::UnionBody => f.write_str(".union_body"),
64 ValuePathSegment::MapKey(i) => write!(f, ".map_key({i})"),
65 ValuePathSegment::MapValue(i) => write!(f, ".map_value({i})"),
66 }
67 }
68}
69
70#[derive(Clone, Debug, Default, PartialEq, Eq)]
72pub struct ValuePath {
73 segments: Vec<ValuePathSegment>,
74}
75
76impl ValuePath {
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn segments(&self) -> &[ValuePathSegment] {
82 &self.segments
83 }
84
85 fn push(&mut self, segment: ValuePathSegment) {
86 self.segments.push(segment);
87 }
88
89 fn pop(&mut self) {
90 self.segments.pop();
91 }
92
93 fn snapshot(&self) -> Self {
94 Self {
95 segments: self.segments.clone(),
96 }
97 }
98}
99
100impl Display for ValuePath {
101 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
102 for s in &self.segments {
103 write!(f, "{s}")?;
104 }
105 Ok(())
106 }
107}
108
109#[derive(Clone, Debug, PartialEq)]
111pub enum ValueError {
112 UnsupportedFeature {
114 path: ValuePath,
115 feature: &'static str,
116 },
117 ShapeMismatch {
118 path: ValuePath,
119 expected: String,
120 found: String,
121 },
122 VariantCaseOutOfRange {
123 path: ValuePath,
124 case: u32,
125 case_count: usize,
126 },
127 EnumCaseOutOfRange {
128 path: ValuePath,
129 case: u32,
130 case_count: usize,
131 },
132 RecordArityMismatch {
133 path: ValuePath,
134 expected: usize,
135 found: usize,
136 },
137 TupleArityMismatch {
138 path: ValuePath,
139 expected: usize,
140 found: usize,
141 },
142 FlagsArityMismatch {
143 path: ValuePath,
144 expected: usize,
145 found: usize,
146 },
147 FixedListLengthMismatch {
148 path: ValuePath,
149 expected: u32,
150 found: usize,
151 },
152 DanglingRef {
153 path: ValuePath,
154 type_id: TypeId,
155 },
156 RecursiveRef {
161 path: ValuePath,
162 type_id: TypeId,
163 },
164 UnionUnknownTag {
165 path: ValuePath,
166 tag: String,
167 },
168 UnionDiscriminatorMismatch {
169 path: ValuePath,
170 tag: String,
171 },
172 VariantPayloadPresenceMismatch {
173 path: ValuePath,
174 expected_some: bool,
175 },
176 ResultPayloadPresenceMismatch {
177 path: ValuePath,
178 expected_some: bool,
179 side: ResultSide,
180 },
181 OptionInnerPresenceMismatch {
182 path: ValuePath,
183 },
184 TextLanguageNotAllowed {
185 path: ValuePath,
186 language: String,
187 },
188 TextTooShort {
189 path: ValuePath,
190 min: u32,
191 found: usize,
192 },
193 TextTooLong {
194 path: ValuePath,
195 max: u32,
196 found: usize,
197 },
198 TextRegexMismatch {
199 path: ValuePath,
200 regex: String,
201 },
202 BinaryMimeNotAllowed {
203 path: ValuePath,
204 mime_type: String,
205 },
206 BinaryTooSmall {
207 path: ValuePath,
208 min: u32,
209 found: usize,
210 },
211 BinaryTooLarge {
212 path: ValuePath,
213 max: u32,
214 found: usize,
215 },
216 PathEmpty {
217 path: ValuePath,
218 },
219 PathExtensionNotAllowed {
220 path: ValuePath,
221 extension: String,
222 },
223 UrlEmpty {
224 path: ValuePath,
225 },
226 UrlInvalid {
227 path: ValuePath,
228 url: String,
229 reason: String,
230 },
231 UrlSchemeNotAllowed {
232 path: ValuePath,
233 scheme: String,
234 },
235 UrlHostNotAllowed {
236 path: ValuePath,
237 host: String,
238 },
239 UrlHostMissing {
240 path: ValuePath,
241 },
242 QuantityUnitNotAllowed {
243 path: ValuePath,
244 unit: String,
245 },
246 QuantityOutOfRange {
247 path: ValuePath,
248 reason: String,
249 },
250 NumericOutOfRange {
251 path: ValuePath,
252 reason: String,
253 },
254 SecretCategoryMismatch {
255 path: ValuePath,
256 expected: String,
257 found: Option<String>,
258 },
259 QuotaTokenResourceMismatch {
260 path: ValuePath,
261 expected: String,
262 found: String,
263 },
264 PermissionCardPolymorphicMismatch {
265 path: ValuePath,
266 expected: bool,
267 found: bool,
268 },
269}
270
271#[derive(Copy, Clone, Debug, PartialEq, Eq)]
272pub enum ResultSide {
273 Ok,
274 Err,
275}
276
277impl Display for ValueError {
278 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
279 match self {
280 ValueError::UnsupportedFeature { path, feature } => {
281 write!(f, "validation at {path} requires feature `{feature}`")
282 }
283 ValueError::ShapeMismatch {
284 path,
285 expected,
286 found,
287 } => write!(
288 f,
289 "shape mismatch at {path}: expected {expected}, found {found}"
290 ),
291 ValueError::VariantCaseOutOfRange {
292 path,
293 case,
294 case_count,
295 } => write!(
296 f,
297 "variant case index {case} at {path} is out of range (case count: {case_count})"
298 ),
299 ValueError::EnumCaseOutOfRange {
300 path,
301 case,
302 case_count,
303 } => write!(
304 f,
305 "enum case index {case} at {path} is out of range (case count: {case_count})"
306 ),
307 ValueError::RecordArityMismatch {
308 path,
309 expected,
310 found,
311 } => write!(
312 f,
313 "record at {path} has {found} field(s), expected {expected}"
314 ),
315 ValueError::TupleArityMismatch {
316 path,
317 expected,
318 found,
319 } => write!(
320 f,
321 "tuple at {path} has {found} element(s), expected {expected}"
322 ),
323 ValueError::FlagsArityMismatch {
324 path,
325 expected,
326 found,
327 } => write!(
328 f,
329 "flags value at {path} has {found} bit(s), expected {expected}"
330 ),
331 ValueError::FixedListLengthMismatch {
332 path,
333 expected,
334 found,
335 } => write!(
336 f,
337 "fixed-list at {path} has {found} element(s), expected {expected}"
338 ),
339 ValueError::DanglingRef { path, type_id } => write!(
340 f,
341 "dangling ref `{type_id}` at {path} (no such named definition)"
342 ),
343 ValueError::RecursiveRef { path, type_id } => write!(
344 f,
345 "ref chain at {path} loops back to `{type_id}` without reaching a structural shape"
346 ),
347 ValueError::UnionUnknownTag { path, tag } => write!(
348 f,
349 "union value at {path} carries tag `{tag}` that does not match any branch"
350 ),
351 ValueError::UnionDiscriminatorMismatch { path, tag } => write!(
352 f,
353 "union value at {path} does not satisfy branch `{tag}` discriminator"
354 ),
355 ValueError::VariantPayloadPresenceMismatch {
356 path,
357 expected_some,
358 } => {
359 let mut s = String::new();
360 let _ = write!(s, "variant payload presence mismatch at {path}: ");
361 if *expected_some {
362 let _ = write!(s, "schema expects a payload, value carries none");
363 } else {
364 let _ = write!(s, "schema expects no payload, value carries one");
365 }
366 f.write_str(&s)
367 }
368 ValueError::ResultPayloadPresenceMismatch {
369 path,
370 expected_some,
371 side,
372 } => {
373 let side = match side {
374 ResultSide::Ok => "ok",
375 ResultSide::Err => "err",
376 };
377 let mut s = String::new();
378 let _ = write!(s, "result {side} payload presence mismatch at {path}: ");
379 if *expected_some {
380 let _ = write!(s, "schema expects a payload, value carries none");
381 } else {
382 let _ = write!(s, "schema expects no payload, value carries one");
383 }
384 f.write_str(&s)
385 }
386 ValueError::OptionInnerPresenceMismatch { path } => {
387 write!(f, "option-value presence inconsistent at {path}")
388 }
389 ValueError::TextLanguageNotAllowed { path, language } => write!(
390 f,
391 "text value at {path} carries language `{language}` not in the allow-list"
392 ),
393 ValueError::TextTooShort { path, min, found } => write!(
394 f,
395 "text value at {path} has {found} char(s), below min-length {min}"
396 ),
397 ValueError::TextTooLong { path, max, found } => write!(
398 f,
399 "text value at {path} has {found} char(s), above max-length {max}"
400 ),
401 ValueError::TextRegexMismatch { path, regex } => write!(
402 f,
403 "text value at {path} does not match required regex `{regex}`"
404 ),
405 ValueError::BinaryMimeNotAllowed { path, mime_type } => write!(
406 f,
407 "binary value at {path} carries mime-type `{mime_type}` not in the allow-list"
408 ),
409 ValueError::BinaryTooSmall { path, min, found } => write!(
410 f,
411 "binary value at {path} has {found} byte(s), below min-bytes {min}"
412 ),
413 ValueError::BinaryTooLarge { path, max, found } => write!(
414 f,
415 "binary value at {path} has {found} byte(s), above max-bytes {max}"
416 ),
417 ValueError::PathEmpty { path } => write!(f, "path value at {path} is empty"),
418 ValueError::PathExtensionNotAllowed { path, extension } => write!(
419 f,
420 "path value at {path} has extension `{extension}` not in the allow-list"
421 ),
422 ValueError::UrlEmpty { path } => write!(f, "url value at {path} is empty"),
423 ValueError::UrlInvalid { path, url, reason } => write!(
424 f,
425 "url value at {path} (`{url}`) is not a valid URL: {reason}"
426 ),
427 ValueError::UrlSchemeNotAllowed { path, scheme } => write!(
428 f,
429 "url value at {path} has scheme `{scheme}` not in the allow-list"
430 ),
431 ValueError::UrlHostNotAllowed { path, host } => write!(
432 f,
433 "url value at {path} has host `{host}` not in the allow-list"
434 ),
435 ValueError::UrlHostMissing { path } => write!(
436 f,
437 "url value at {path} has no host (allow-list requires one)"
438 ),
439 ValueError::QuantityUnitNotAllowed { path, unit } => write!(
440 f,
441 "quantity value at {path} has unit `{unit}` which is not allowed"
442 ),
443 ValueError::QuantityOutOfRange { path, reason } => {
444 write!(f, "quantity value at {path} is out of range ({reason})")
445 }
446 ValueError::NumericOutOfRange { path, reason } => {
447 write!(f, "numeric value at {path} is out of range ({reason})")
448 }
449 ValueError::SecretCategoryMismatch {
450 path,
451 expected,
452 found,
453 } => write!(
454 f,
455 "secret value at {path} expected category `{expected}`, found `{}`",
456 found.as_deref().unwrap_or("<none>")
457 ),
458 ValueError::QuotaTokenResourceMismatch {
459 path,
460 expected,
461 found,
462 } => write!(
463 f,
464 "quota-token value at {path} expected resource `{expected}`, found `{found}`"
465 ),
466 ValueError::PermissionCardPolymorphicMismatch {
467 path,
468 expected,
469 found,
470 } => write!(
471 f,
472 "permission-card value at {path} expected polymorphic `{expected}`, found `{found}`"
473 ),
474 }
475 }
476}
477
478impl Error for ValueError {}
479
480pub fn validate_value(
483 graph: &SchemaGraph,
484 ty: &SchemaType,
485 value: &SchemaValue,
486) -> Result<(), Vec<ValueError>> {
487 let mut errors = Vec::new();
488 let mut path = ValuePath::new();
489 let index = GraphIndex::new(graph);
490 check(&index, ty, value, &mut path, &mut errors);
491 if errors.is_empty() {
492 Ok(())
493 } else {
494 Err(errors)
495 }
496}
497
498pub fn validate_record_fields<'g, 'f, I>(
513 graph: &'g SchemaGraph,
514 fields: I,
515 values: &[SchemaValue],
516) -> Result<(), Vec<ValueError>>
517where
518 I: ExactSizeIterator<Item = (&'f str, &'f SchemaType)>,
519{
520 let mut errors = Vec::new();
521 let mut path = ValuePath::new();
522 let index = GraphIndex::new(graph);
523
524 if fields.len() != values.len() {
525 errors.push(ValueError::RecordArityMismatch {
526 path: path.snapshot(),
527 expected: fields.len(),
528 found: values.len(),
529 });
530 return Err(errors);
531 }
532
533 for ((name, body), v) in fields.zip(values.iter()) {
534 path.push(ValuePathSegment::Field(name.to_string()));
535 check(&index, body, v, &mut path, &mut errors);
536 path.pop();
537 }
538
539 if errors.is_empty() {
540 Ok(())
541 } else {
542 Err(errors)
543 }
544}
545
546fn shape_name(value: &SchemaValue) -> &'static str {
547 match value {
548 SchemaValue::Bool(_) => "bool",
549 SchemaValue::S8(_) => "s8",
550 SchemaValue::S16(_) => "s16",
551 SchemaValue::S32(_) => "s32",
552 SchemaValue::S64(_) => "s64",
553 SchemaValue::U8(_) => "u8",
554 SchemaValue::U16(_) => "u16",
555 SchemaValue::U32(_) => "u32",
556 SchemaValue::U64(_) => "u64",
557 SchemaValue::F32(_) => "f32",
558 SchemaValue::F64(_) => "f64",
559 SchemaValue::Char(_) => "char",
560 SchemaValue::String(_) => "string",
561 SchemaValue::Record { .. } => "record",
562 SchemaValue::Variant(_) => "variant",
563 SchemaValue::Enum { .. } => "enum",
564 SchemaValue::Flags { .. } => "flags",
565 SchemaValue::Tuple { .. } => "tuple",
566 SchemaValue::List { .. } => "list",
567 SchemaValue::FixedList { .. } => "fixed-list",
568 SchemaValue::Map { .. } => "map",
569 SchemaValue::Option { .. } => "option",
570 SchemaValue::Result(_) => "result",
571 SchemaValue::Text(_) => "text",
572 SchemaValue::Binary(_) => "binary",
573 SchemaValue::Path { .. } => "path",
574 SchemaValue::Url { .. } => "url",
575 SchemaValue::Uuid(_) => "uuid",
576 SchemaValue::Datetime { .. } => "datetime",
577 SchemaValue::Duration(_) => "duration",
578 SchemaValue::Quantity(_) => "quantity",
579 SchemaValue::Union(_) => "union",
580 SchemaValue::Secret(_) => "secret",
581 SchemaValue::QuotaToken(_) => "quota-token",
582 SchemaValue::Stream(_) => "stream",
583 SchemaValue::PermissionCard(_) => "permission-card",
584 }
585}
586
587fn type_name(ty: &SchemaType) -> &'static str {
588 match ty {
589 SchemaType::Ref { .. } => "ref",
590 SchemaType::Bool { .. } => "bool",
591 SchemaType::S8 { .. } => "s8",
592 SchemaType::S16 { .. } => "s16",
593 SchemaType::S32 { .. } => "s32",
594 SchemaType::S64 { .. } => "s64",
595 SchemaType::U8 { .. } => "u8",
596 SchemaType::U16 { .. } => "u16",
597 SchemaType::U32 { .. } => "u32",
598 SchemaType::U64 { .. } => "u64",
599 SchemaType::F32 { .. } => "f32",
600 SchemaType::F64 { .. } => "f64",
601 SchemaType::Char { .. } => "char",
602 SchemaType::String { .. } => "string",
603 SchemaType::Record { .. } => "record",
604 SchemaType::Variant { .. } => "variant",
605 SchemaType::Enum { .. } => "enum",
606 SchemaType::Flags { .. } => "flags",
607 SchemaType::Tuple { .. } => "tuple",
608 SchemaType::List { .. } => "list",
609 SchemaType::FixedList { .. } => "fixed-list",
610 SchemaType::Map { .. } => "map",
611 SchemaType::Option { .. } => "option",
612 SchemaType::Result { .. } => "result",
613 SchemaType::Text { .. } => "text",
614 SchemaType::Binary { .. } => "binary",
615 SchemaType::Path { .. } => "path",
616 SchemaType::Url { .. } => "url",
617 SchemaType::Uuid { .. } => "uuid",
618 SchemaType::Datetime { .. } => "datetime",
619 SchemaType::Duration { .. } => "duration",
620 SchemaType::Quantity { .. } => "quantity",
621 SchemaType::Union { .. } => "union",
622 SchemaType::Secret { .. } => "secret",
623 SchemaType::QuotaToken { .. } => "quota-token",
624 SchemaType::PermissionCard { .. } => "permission-card",
625 SchemaType::Future { .. } => "future",
626 SchemaType::Stream { .. } => "stream",
627 }
628}
629
630fn shape_mismatch(
631 path: &ValuePath,
632 ty: &SchemaType,
633 value: &SchemaValue,
634 errors: &mut Vec<ValueError>,
635) {
636 errors.push(ValueError::ShapeMismatch {
637 path: path.snapshot(),
638 expected: type_name(ty).to_string(),
639 found: shape_name(value).to_string(),
640 });
641}
642
643fn resolve_refs_at_value_node<'a>(
655 index: &GraphIndex<'a>,
656 mut ty: &'a SchemaType,
657 path: &ValuePath,
658 errors: &mut Vec<ValueError>,
659) -> Option<&'a SchemaType> {
660 let max_hops = index.defs_len();
666 let mut hops = 0usize;
667 loop {
668 match ty {
669 SchemaType::Ref { id, .. } => {
670 if hops > max_hops {
671 errors.push(ValueError::RecursiveRef {
672 path: path.snapshot(),
673 type_id: id.clone(),
674 });
675 return None;
676 }
677 hops += 1;
678 match index.lookup(id) {
679 Some(def) => {
680 ty = &def.body;
681 }
682 None => {
683 errors.push(ValueError::DanglingRef {
684 path: path.snapshot(),
685 type_id: id.clone(),
686 });
687 return None;
688 }
689 }
690 }
691 other => return Some(other),
692 }
693 }
694}
695
696fn check<'a>(
697 index: &GraphIndex<'a>,
698 ty: &'a SchemaType,
699 value: &SchemaValue,
700 path: &mut ValuePath,
701 errors: &mut Vec<ValueError>,
702) {
703 let Some(ty) = resolve_refs_at_value_node(index, ty, path, errors) else {
709 return;
710 };
711
712 match (ty, value) {
713 (SchemaType::Bool { .. }, SchemaValue::Bool(_)) => {}
714 (SchemaType::S8 { restrictions, .. }, SchemaValue::S8(v)) => {
715 check_numeric(
716 NumericRepr::S8,
717 restrictions,
718 NumericBound::Signed(*v as i64),
719 path,
720 errors,
721 );
722 }
723 (SchemaType::S16 { restrictions, .. }, SchemaValue::S16(v)) => {
724 check_numeric(
725 NumericRepr::S16,
726 restrictions,
727 NumericBound::Signed(*v as i64),
728 path,
729 errors,
730 );
731 }
732 (SchemaType::S32 { restrictions, .. }, SchemaValue::S32(v)) => {
733 check_numeric(
734 NumericRepr::S32,
735 restrictions,
736 NumericBound::Signed(*v as i64),
737 path,
738 errors,
739 );
740 }
741 (SchemaType::S64 { restrictions, .. }, SchemaValue::S64(v)) => {
742 check_numeric(
743 NumericRepr::S64,
744 restrictions,
745 NumericBound::Signed(*v),
746 path,
747 errors,
748 );
749 }
750 (SchemaType::U8 { restrictions, .. }, SchemaValue::U8(v)) => {
751 check_numeric(
752 NumericRepr::U8,
753 restrictions,
754 NumericBound::Unsigned(*v as u64),
755 path,
756 errors,
757 );
758 }
759 (SchemaType::U16 { restrictions, .. }, SchemaValue::U16(v)) => {
760 check_numeric(
761 NumericRepr::U16,
762 restrictions,
763 NumericBound::Unsigned(*v as u64),
764 path,
765 errors,
766 );
767 }
768 (SchemaType::U32 { restrictions, .. }, SchemaValue::U32(v)) => {
769 check_numeric(
770 NumericRepr::U32,
771 restrictions,
772 NumericBound::Unsigned(*v as u64),
773 path,
774 errors,
775 );
776 }
777 (SchemaType::U64 { restrictions, .. }, SchemaValue::U64(v)) => {
778 check_numeric(
779 NumericRepr::U64,
780 restrictions,
781 NumericBound::Unsigned(*v),
782 path,
783 errors,
784 );
785 }
786 (SchemaType::F32 { restrictions, .. }, SchemaValue::F32(v)) => {
787 check_numeric(
788 NumericRepr::F32,
789 restrictions,
790 NumericBound::FloatBits((*v as f64).to_bits()),
791 path,
792 errors,
793 );
794 }
795 (SchemaType::F64 { restrictions, .. }, SchemaValue::F64(v)) => {
796 check_numeric(
797 NumericRepr::F64,
798 restrictions,
799 NumericBound::FloatBits(v.to_bits()),
800 path,
801 errors,
802 );
803 }
804 (SchemaType::Char { .. }, SchemaValue::Char(_)) => {}
805 (SchemaType::String { .. }, SchemaValue::String(_)) => {}
806
807 (SchemaType::Text { restrictions, .. }, SchemaValue::Text(payload)) => {
808 check_text(restrictions, payload, path, errors);
809 }
810 (SchemaType::Binary { restrictions, .. }, SchemaValue::Binary(payload)) => {
811 check_binary(restrictions, payload, path, errors);
812 }
813 (SchemaType::Path { spec, .. }, SchemaValue::Path { path: p }) => {
814 check_path(spec, p.as_str(), path, errors);
815 }
816 (SchemaType::Url { restrictions, .. }, SchemaValue::Url { url }) => {
817 check_url(restrictions, url.as_str(), path, errors);
818 }
819 (SchemaType::Uuid { .. }, SchemaValue::Uuid(_)) => {}
820 (SchemaType::Datetime { .. }, SchemaValue::Datetime { .. }) => {}
821 (SchemaType::Duration { .. }, SchemaValue::Duration(_)) => {}
822 (SchemaType::Quantity { spec, .. }, SchemaValue::Quantity(value)) => {
823 check_quantity(spec, value, path, errors);
824 }
825 (SchemaType::Secret { spec, .. }, SchemaValue::Secret(payload)) => {
826 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
827 check_secret(spec, payload, path, errors);
828 #[cfg(all(feature = "guest", not(feature = "host")))]
832 let _ = (spec, payload);
833 }
834 (SchemaType::QuotaToken { spec, .. }, SchemaValue::QuotaToken(payload)) => {
835 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
836 check_quota_token(spec, payload, path, errors);
837 #[cfg(all(feature = "guest", not(feature = "host")))]
841 let _ = (spec, payload);
842 }
843 (SchemaType::Stream { .. }, SchemaValue::Stream(_)) => {}
844 (SchemaType::PermissionCard { spec, .. }, SchemaValue::PermissionCard(payload)) => {
845 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
846 check_permission_card(spec, payload, path, errors);
847 #[cfg(all(feature = "guest", not(feature = "host")))]
851 let _ = (spec, payload);
852 }
853
854 (SchemaType::Record { fields, .. }, SchemaValue::Record { fields: vs }) => {
855 if fields.len() != vs.len() {
856 errors.push(ValueError::RecordArityMismatch {
857 path: path.snapshot(),
858 expected: fields.len(),
859 found: vs.len(),
860 });
861 return;
862 }
863 for (field, v) in fields.iter().zip(vs.iter()) {
864 path.push(ValuePathSegment::Field(field.name.clone()));
865 check(index, &field.body, v, path, errors);
866 path.pop();
867 }
868 }
869
870 (SchemaType::Variant { cases, .. }, SchemaValue::Variant(vp)) => {
871 let idx = vp.case as usize;
872 if idx >= cases.len() {
873 errors.push(ValueError::VariantCaseOutOfRange {
874 path: path.snapshot(),
875 case: vp.case,
876 case_count: cases.len(),
877 });
878 return;
879 }
880 let case = &cases[idx];
881 match (&case.payload, &vp.payload) {
882 (Some(case_ty), Some(payload)) => {
883 path.push(ValuePathSegment::VariantPayload);
884 check(index, case_ty, payload, path, errors);
885 path.pop();
886 }
887 (None, None) => {}
888 (Some(_), None) => errors.push(ValueError::VariantPayloadPresenceMismatch {
889 path: path.snapshot(),
890 expected_some: true,
891 }),
892 (None, Some(_)) => errors.push(ValueError::VariantPayloadPresenceMismatch {
893 path: path.snapshot(),
894 expected_some: false,
895 }),
896 }
897 }
898
899 (SchemaType::Enum { cases, .. }, SchemaValue::Enum { case }) => {
900 if (*case as usize) >= cases.len() {
901 errors.push(ValueError::EnumCaseOutOfRange {
902 path: path.snapshot(),
903 case: *case,
904 case_count: cases.len(),
905 });
906 }
907 }
908
909 (SchemaType::Flags { flags, .. }, SchemaValue::Flags { bits }) => {
910 if flags.len() != bits.len() {
911 errors.push(ValueError::FlagsArityMismatch {
912 path: path.snapshot(),
913 expected: flags.len(),
914 found: bits.len(),
915 });
916 }
917 }
918
919 (SchemaType::Tuple { elements, .. }, SchemaValue::Tuple { elements: vs }) => {
920 if elements.len() != vs.len() {
921 errors.push(ValueError::TupleArityMismatch {
922 path: path.snapshot(),
923 expected: elements.len(),
924 found: vs.len(),
925 });
926 return;
927 }
928 for (i, (t, v)) in elements.iter().zip(vs.iter()).enumerate() {
929 path.push(ValuePathSegment::Index(i));
930 check(index, t, v, path, errors);
931 path.pop();
932 }
933 }
934
935 (SchemaType::List { element, .. }, SchemaValue::List { elements }) => {
936 for (i, v) in elements.iter().enumerate() {
937 path.push(ValuePathSegment::Index(i));
938 check(index, element, v, path, errors);
939 path.pop();
940 }
941 }
942
943 (
944 SchemaType::FixedList {
945 element, length, ..
946 },
947 SchemaValue::FixedList { elements },
948 ) => {
949 if elements.len() != *length as usize {
950 errors.push(ValueError::FixedListLengthMismatch {
951 path: path.snapshot(),
952 expected: *length,
953 found: elements.len(),
954 });
955 return;
956 }
957 for (i, v) in elements.iter().enumerate() {
958 path.push(ValuePathSegment::Index(i));
959 check(index, element, v, path, errors);
960 path.pop();
961 }
962 }
963
964 (
965 SchemaType::Map {
966 key, value: vty, ..
967 },
968 SchemaValue::Map { entries },
969 ) => {
970 for (i, (k, v)) in entries.iter().enumerate() {
971 path.push(ValuePathSegment::MapKey(i));
972 check(index, key, k, path, errors);
973 path.pop();
974 path.push(ValuePathSegment::MapValue(i));
975 check(index, vty, v, path, errors);
976 path.pop();
977 }
978 }
979
980 (SchemaType::Option { inner, .. }, SchemaValue::Option { inner: v }) => {
981 if let Some(v) = v {
982 path.push(ValuePathSegment::OptionInner);
983 check(index, inner, v, path, errors);
984 path.pop();
985 }
986 }
987
988 (SchemaType::Result { spec, .. }, SchemaValue::Result(vp)) => match vp {
989 ResultValuePayload::Ok { value: v } => match (&spec.ok, v) {
990 (Some(t), Some(v)) => {
991 path.push(ValuePathSegment::ResultOk);
992 check(index, t, v, path, errors);
993 path.pop();
994 }
995 (None, None) => {}
996 (Some(_), None) => errors.push(ValueError::ResultPayloadPresenceMismatch {
997 path: path.snapshot(),
998 expected_some: true,
999 side: ResultSide::Ok,
1000 }),
1001 (None, Some(_)) => errors.push(ValueError::ResultPayloadPresenceMismatch {
1002 path: path.snapshot(),
1003 expected_some: false,
1004 side: ResultSide::Ok,
1005 }),
1006 },
1007 ResultValuePayload::Err { value: v } => match (&spec.err, v) {
1008 (Some(t), Some(v)) => {
1009 path.push(ValuePathSegment::ResultErr);
1010 check(index, t, v, path, errors);
1011 path.pop();
1012 }
1013 (None, None) => {}
1014 (Some(_), None) => errors.push(ValueError::ResultPayloadPresenceMismatch {
1015 path: path.snapshot(),
1016 expected_some: true,
1017 side: ResultSide::Err,
1018 }),
1019 (None, Some(_)) => errors.push(ValueError::ResultPayloadPresenceMismatch {
1020 path: path.snapshot(),
1021 expected_some: false,
1022 side: ResultSide::Err,
1023 }),
1024 },
1025 },
1026
1027 (SchemaType::Union { spec, .. }, SchemaValue::Union(vp)) => {
1028 let branch = spec.branches.iter().find(|b| b.tag == vp.tag);
1029 match branch {
1030 None => errors.push(ValueError::UnionUnknownTag {
1031 path: path.snapshot(),
1032 tag: vp.tag.clone(),
1033 }),
1034 Some(branch) => {
1035 path.push(ValuePathSegment::UnionBody);
1036 #[cfg(not(feature = "regex"))]
1037 if matches!(branch.discriminator, DiscriminatorRule::Regex { .. }) {
1038 errors.push(ValueError::UnsupportedFeature {
1039 path: path.snapshot(),
1040 feature: "regex",
1041 });
1042 path.pop();
1043 return;
1044 }
1045 let mut sub_errors = Vec::new();
1046 check(index, &branch.body, &vp.body, path, &mut sub_errors);
1047 errors.extend(sub_errors);
1048 if !discriminator_matches(index, branch, &vp.body) {
1049 errors.push(ValueError::UnionDiscriminatorMismatch {
1050 path: path.snapshot(),
1051 tag: vp.tag.clone(),
1052 });
1053 }
1054 path.pop();
1055 }
1056 }
1057 }
1058
1059 (ty, v) => shape_mismatch(path, ty, v, errors),
1060 }
1061}
1062
1063fn check_numeric(
1072 repr: NumericRepr,
1073 restrictions: &Option<NumericRestrictions>,
1074 value: NumericBound,
1075 path: &mut ValuePath,
1076 errors: &mut Vec<ValueError>,
1077) {
1078 let Some(restrictions) = restrictions else {
1079 return;
1080 };
1081 if let Err(err) = restrictions.validate_for_repr(repr)
1087 && !matches!(err, NumericRestrictionError::MinGreaterThanMax)
1088 {
1089 return;
1090 }
1091 if let Some(min) = restrictions.min
1092 && !matches!(
1093 numeric_bound_cmp(value, min),
1094 Some(Ordering::Greater | Ordering::Equal)
1095 )
1096 {
1097 errors.push(ValueError::NumericOutOfRange {
1098 path: path.snapshot(),
1099 reason: format!("below minimum {}", describe_numeric_bound(min)),
1100 });
1101 }
1102 if let Some(max) = restrictions.max
1103 && !matches!(
1104 numeric_bound_cmp(value, max),
1105 Some(Ordering::Less | Ordering::Equal)
1106 )
1107 {
1108 errors.push(ValueError::NumericOutOfRange {
1109 path: path.snapshot(),
1110 reason: format!("above maximum {}", describe_numeric_bound(max)),
1111 });
1112 }
1113}
1114
1115fn describe_numeric_bound(bound: NumericBound) -> String {
1116 match bound {
1117 NumericBound::Signed(v) => v.to_string(),
1118 NumericBound::Unsigned(v) => v.to_string(),
1119 NumericBound::FloatBits(bits) => f64::from_bits(bits).to_string(),
1120 }
1121}
1122
1123fn check_text(
1124 restrictions: &TextRestrictions,
1125 payload: &TextValuePayload,
1126 path: &mut ValuePath,
1127 errors: &mut Vec<ValueError>,
1128) {
1129 if let (Some(allowed), Some(lang)) = (&restrictions.languages, &payload.language)
1130 && !allowed.iter().any(|a| a == lang)
1131 {
1132 errors.push(ValueError::TextLanguageNotAllowed {
1133 path: path.snapshot(),
1134 language: lang.clone(),
1135 });
1136 }
1137 let char_len = payload.text.chars().count();
1138 if let Some(min) = restrictions.min_length
1139 && (char_len as u64) < (min as u64)
1140 {
1141 errors.push(ValueError::TextTooShort {
1142 path: path.snapshot(),
1143 min,
1144 found: char_len,
1145 });
1146 }
1147 if let Some(max) = restrictions.max_length
1148 && (char_len as u64) > (max as u64)
1149 {
1150 errors.push(ValueError::TextTooLong {
1151 path: path.snapshot(),
1152 max,
1153 found: char_len,
1154 });
1155 }
1156 #[cfg(not(feature = "regex"))]
1157 if restrictions.regex.is_some() {
1158 errors.push(ValueError::UnsupportedFeature {
1159 path: path.snapshot(),
1160 feature: "regex",
1161 });
1162 }
1163 #[cfg(feature = "regex")]
1164 if let Some(regex) = &restrictions.regex
1165 && let Ok(compiled) = regex::Regex::new(regex.as_str())
1166 && !compiled.is_match(payload.text.as_str())
1167 {
1168 errors.push(ValueError::TextRegexMismatch {
1169 path: path.snapshot(),
1170 regex: regex.clone(),
1171 });
1172 }
1173}
1174
1175fn check_binary(
1176 restrictions: &BinaryRestrictions,
1177 payload: &BinaryValuePayload,
1178 path: &mut ValuePath,
1179 errors: &mut Vec<ValueError>,
1180) {
1181 if let (Some(allowed), Some(mime)) = (&restrictions.mime_types, &payload.mime_type)
1182 && !allowed.iter().any(|a| a == mime)
1183 {
1184 errors.push(ValueError::BinaryMimeNotAllowed {
1185 path: path.snapshot(),
1186 mime_type: mime.clone(),
1187 });
1188 }
1189 let len = payload.bytes.len();
1190 if let Some(min) = restrictions.min_bytes
1191 && (len as u64) < (min as u64)
1192 {
1193 errors.push(ValueError::BinaryTooSmall {
1194 path: path.snapshot(),
1195 min,
1196 found: len,
1197 });
1198 }
1199 if let Some(max) = restrictions.max_bytes
1200 && (len as u64) > (max as u64)
1201 {
1202 errors.push(ValueError::BinaryTooLarge {
1203 path: path.snapshot(),
1204 max,
1205 found: len,
1206 });
1207 }
1208}
1209
1210fn check_path(spec: &PathSpec, p: &str, path: &mut ValuePath, errors: &mut Vec<ValueError>) {
1211 if p.is_empty() {
1212 errors.push(ValueError::PathEmpty {
1213 path: path.snapshot(),
1214 });
1215 return;
1216 }
1217 if let Some(allowed_exts) = &spec.allowed_extensions
1218 && let Some(ext) = file_extension(p)
1219 && !allowed_exts.iter().any(|a| a == ext)
1220 {
1221 errors.push(ValueError::PathExtensionNotAllowed {
1222 path: path.snapshot(),
1223 extension: ext.to_string(),
1224 });
1225 }
1226 }
1233
1234#[cfg(not(feature = "url"))]
1235fn check_url(
1236 _spec: &UrlRestrictions,
1237 _url: &str,
1238 path: &mut ValuePath,
1239 errors: &mut Vec<ValueError>,
1240) {
1241 errors.push(ValueError::UnsupportedFeature {
1242 path: path.snapshot(),
1243 feature: "url",
1244 });
1245}
1246
1247#[cfg(feature = "url")]
1248fn check_url(
1249 spec: &UrlRestrictions,
1250 url: &str,
1251 path: &mut ValuePath,
1252 errors: &mut Vec<ValueError>,
1253) {
1254 if url.is_empty() {
1255 errors.push(ValueError::UrlEmpty {
1256 path: path.snapshot(),
1257 });
1258 return;
1259 }
1260 let parsed = match url::Url::parse(url) {
1261 Ok(parsed) => parsed,
1262 Err(err) => {
1263 errors.push(ValueError::UrlInvalid {
1264 path: path.snapshot(),
1265 url: url.to_string(),
1266 reason: err.to_string(),
1267 });
1268 return;
1269 }
1270 };
1271 if let Some(allowed) = &spec.allowed_schemes
1272 && !allowed
1273 .iter()
1274 .any(|scheme| scheme.eq_ignore_ascii_case(parsed.scheme()))
1275 {
1276 errors.push(ValueError::UrlSchemeNotAllowed {
1277 path: path.snapshot(),
1278 scheme: parsed.scheme().to_string(),
1279 });
1280 }
1281 if let Some(allowed) = &spec.allowed_hosts {
1282 match parsed.host_str() {
1283 Some(host)
1284 if allowed
1285 .iter()
1286 .any(|allowed_host| allowed_host.eq_ignore_ascii_case(host)) => {}
1287 Some(host) => errors.push(ValueError::UrlHostNotAllowed {
1288 path: path.snapshot(),
1289 host: host.to_string(),
1290 }),
1291 None => errors.push(ValueError::UrlHostMissing {
1292 path: path.snapshot(),
1293 }),
1294 }
1295 }
1296}
1297
1298fn check_quantity(
1299 spec: &QuantitySpec,
1300 value: &QuantityValue,
1301 path: &mut ValuePath,
1302 errors: &mut Vec<ValueError>,
1303) {
1304 let unit_ok = if spec.allowed_suffixes.is_empty() {
1305 value.unit == spec.base_unit
1306 } else {
1307 spec.allowed_suffixes.iter().any(|s| s == &value.unit)
1308 };
1309 if !unit_ok {
1310 errors.push(ValueError::QuantityUnitNotAllowed {
1311 path: path.snapshot(),
1312 unit: value.unit.clone(),
1313 });
1314 return;
1317 }
1318
1319 if let Some(min) = &spec.min {
1320 match quantity_le_checked(min, value) {
1321 Some(true) => {}
1322 Some(false) => errors.push(ValueError::QuantityOutOfRange {
1323 path: path.snapshot(),
1324 reason: format!("value is below min `{}`", render_quantity(min)),
1325 }),
1326 None => errors.push(ValueError::QuantityOutOfRange {
1327 path: path.snapshot(),
1328 reason: "overflow".to_string(),
1329 }),
1330 }
1331 }
1332 if let Some(max) = &spec.max {
1333 match quantity_le_checked(value, max) {
1334 Some(true) => {}
1335 Some(false) => errors.push(ValueError::QuantityOutOfRange {
1336 path: path.snapshot(),
1337 reason: format!("value is above max `{}`", render_quantity(max)),
1338 }),
1339 None => errors.push(ValueError::QuantityOutOfRange {
1340 path: path.snapshot(),
1341 reason: "overflow".to_string(),
1342 }),
1343 }
1344 }
1345}
1346
1347#[cfg(not(all(feature = "guest", not(feature = "host"))))]
1348fn check_secret(
1349 spec: &SecretSpec,
1350 payload: &SecretValuePayload,
1351 path: &mut ValuePath,
1352 errors: &mut Vec<ValueError>,
1353) {
1354 if let Some(expected) = &spec.category
1355 && payload.category.as_ref() != Some(expected)
1356 {
1357 errors.push(ValueError::SecretCategoryMismatch {
1358 path: path.snapshot(),
1359 expected: expected.clone(),
1360 found: payload.category.clone(),
1361 });
1362 }
1363}
1364
1365#[cfg(not(all(feature = "guest", not(feature = "host"))))]
1366fn check_quota_token(
1367 spec: &QuotaTokenSpec,
1368 payload: &QuotaTokenValuePayload,
1369 path: &mut ValuePath,
1370 errors: &mut Vec<ValueError>,
1371) {
1372 if let Some(expected) = &spec.resource_name
1373 && expected != &payload.resource_name
1374 {
1375 errors.push(ValueError::QuotaTokenResourceMismatch {
1376 path: path.snapshot(),
1377 expected: expected.clone(),
1378 found: payload.resource_name.clone(),
1379 });
1380 }
1381}
1382
1383#[cfg(not(all(feature = "guest", not(feature = "host"))))]
1384fn check_permission_card(
1385 spec: &PermissionCardSpec,
1386 payload: &PermissionCardValuePayload,
1387 path: &mut ValuePath,
1388 errors: &mut Vec<ValueError>,
1389) {
1390 if spec.polymorphic != payload.polymorphic {
1391 errors.push(ValueError::PermissionCardPolymorphicMismatch {
1392 path: path.snapshot(),
1393 expected: spec.polymorphic,
1394 found: payload.polymorphic,
1395 });
1396 }
1397}
1398
1399fn discriminator_matches(index: &GraphIndex, branch: &UnionBranch, body: &SchemaValue) -> bool {
1400 match &branch.discriminator {
1401 DiscriminatorRule::Prefix { prefix } => string_view(index, &branch.body, body)
1402 .map(|s| s.starts_with(prefix.as_str()))
1403 .unwrap_or(false),
1404 DiscriminatorRule::Suffix { suffix } => string_view(index, &branch.body, body)
1405 .map(|s| s.ends_with(suffix.as_str()))
1406 .unwrap_or(false),
1407 DiscriminatorRule::Contains { substring } => string_view(index, &branch.body, body)
1408 .map(|s| s.contains(substring.as_str()))
1409 .unwrap_or(false),
1410 #[cfg(not(feature = "regex"))]
1411 DiscriminatorRule::Regex { .. } => false,
1412 #[cfg(feature = "regex")]
1413 DiscriminatorRule::Regex { regex } => {
1414 let Some(s) = string_view(index, &branch.body, body) else {
1415 return false;
1416 };
1417 match regex::Regex::new(regex.as_str()) {
1418 Ok(compiled) => compiled.is_match(s),
1419 Err(_) => false,
1420 }
1421 }
1422 DiscriminatorRule::FieldEquals(field_disc) => {
1423 let Some(record) = record_view(index, &branch.body, body) else {
1424 return false;
1425 };
1426 let pos = record
1427 .field_names
1428 .iter()
1429 .position(|n| n == &field_disc.field_name);
1430 let Some(pos) = pos else {
1431 return false;
1432 };
1433 match &field_disc.literal {
1434 None => true,
1435 Some(lit) => {
1436 let v = &record.values[pos];
1437 matches!(v, SchemaValue::String(s) if s == lit)
1438 || matches!(v, SchemaValue::Text(t) if &t.text == lit)
1439 || matches!(v, SchemaValue::Url { url } if url == lit)
1440 || matches!(v, SchemaValue::Path { path } if path == lit)
1441 }
1442 }
1443 }
1444 DiscriminatorRule::FieldAbsent { field_name } => {
1445 let Some(record) = record_view(index, &branch.body, body) else {
1446 return false;
1447 };
1448 !record.field_names.iter().any(|n| n == field_name)
1449 }
1450 }
1451}
1452
1453fn string_view<'a>(index: &GraphIndex, ty: &SchemaType, value: &'a SchemaValue) -> Option<&'a str> {
1454 match (resolve(index, ty), value) {
1455 (Some(_), SchemaValue::String(s)) => Some(s.as_str()),
1456 (Some(_), SchemaValue::Text(t)) => Some(t.text.as_str()),
1457 (Some(_), SchemaValue::Url { url }) => Some(url.as_str()),
1458 (Some(_), SchemaValue::Path { path }) => Some(path.as_str()),
1459 _ => None,
1460 }
1461}
1462
1463struct RecordView<'a> {
1464 field_names: Vec<String>,
1465 values: &'a [SchemaValue],
1466}
1467
1468fn record_view<'a>(
1469 index: &GraphIndex,
1470 ty: &SchemaType,
1471 value: &'a SchemaValue,
1472) -> Option<RecordView<'a>> {
1473 let resolved = resolve_record(index, ty)?;
1474 match value {
1475 SchemaValue::Record { fields } if fields.len() == resolved.len() => Some(RecordView {
1476 field_names: resolved,
1477 values: fields.as_slice(),
1478 }),
1479 _ => None,
1480 }
1481}
1482
1483fn resolve<'a>(index: &GraphIndex<'a>, ty: &'a SchemaType) -> Option<&'a SchemaType> {
1484 let mut current = ty;
1485 let max_hops = index.defs_len();
1489 let mut hops = 0usize;
1490 loop {
1491 match current {
1492 SchemaType::Ref { id, .. } => {
1493 if hops > max_hops {
1494 return None;
1495 }
1496 hops += 1;
1497 {
1498 let def = index.lookup(id)?;
1499 current = &def.body
1500 }
1501 }
1502 other => return Some(other),
1503 }
1504 }
1505}
1506
1507fn resolve_record(index: &GraphIndex, ty: &SchemaType) -> Option<Vec<String>> {
1508 let resolved = resolve(index, ty)?;
1509 match resolved {
1510 SchemaType::Record { fields, .. } => Some(fields.iter().map(|f| f.name.clone()).collect()),
1511 _ => None,
1512 }
1513}
1514
1515fn file_extension(p: &str) -> Option<&str> {
1516 let name = p.rsplit('/').next()?;
1517 let (_, ext) = name.rsplit_once('.')?;
1518 if ext.is_empty() { None } else { Some(ext) }
1519}
1520
1521fn render_quantity(q: &QuantityValue) -> String {
1522 format!("{}*10^(-{}) {}", q.mantissa, q.scale, q.unit)
1523}
1524
1525fn quantity_le_checked(a: &QuantityValue, b: &QuantityValue) -> Option<bool> {
1526 let common = a.scale.max(b.scale);
1527 let a_shift = (common - a.scale).max(0) as u32;
1528 let b_shift = (common - b.scale).max(0) as u32;
1529 let ten: i128 = 10;
1530 let a_factor = ten.checked_pow(a_shift)?;
1531 let b_factor = ten.checked_pow(b_shift)?;
1532 let a_canon = (a.mantissa as i128).checked_mul(a_factor)?;
1533 let b_canon = (b.mantissa as i128).checked_mul(b_factor)?;
1534 Some(a_canon <= b_canon)
1535}