1use std::collections::{BTreeMap, BTreeSet};
16
17use omena_cascade::{
18 CascadeKey, CascadeLevel, CascadeValue, DomClassTokenizationV0, LayerOrdinal, Specificity,
19 normalized_layer_rank, resolve_custom_property_env_least_fixed_point, token_support_v0,
20 tokenize_dom_class_attribute_v0,
21};
22use serde::{Deserialize, Serialize};
23
24pub const REWRITE_CERTIFICATE_SCHEMA_VERSION_V0: &str = "0";
25pub const REWRITE_RULE_CATALOG_SCHEMA_VERSION_V0: &str = "0";
26pub const REWRITE_RULE_CATALOG_SCHEMA_ID_V0: &str = "omena-cascade-proof.rewrite-rule-catalog.v0";
27pub const CANONICAL_REWRITE_ASSUMPTIONS_SCHEMA_VERSION_V0: &str = "0";
28pub const REWRITE_CERTIFICATE_MAX_DEPTH_V0: usize = 64;
29pub const REWRITE_CERTIFICATE_MAX_NODES_V0: usize = 4_096;
30pub const REWRITE_RULE_CATALOG_MAX_RULES_V0: usize = 256;
31pub const REWRITE_RULE_CATALOG_MAX_OPERATORS_V0: usize = 256;
32const REWRITE_TERM_MAX_DEPTH_V0: usize = 64;
33const REWRITE_TERM_MAX_NODES_V0: usize = 4_096;
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
36#[non_exhaustive]
37#[serde(
38 tag = "kind",
39 rename_all = "camelCase",
40 rename_all_fields = "camelCase"
41)]
42pub enum RewriteTermV0 {
43 Atom {
44 value: String,
45 },
46 Apply {
47 operator: String,
48 operands: Vec<RewriteTermV0>,
49 },
50}
51
52impl RewriteTermV0 {
53 pub fn atom(value: impl Into<String>) -> Self {
54 Self::Atom {
55 value: value.into(),
56 }
57 }
58
59 pub fn apply(operator: impl Into<String>, operands: Vec<Self>) -> Self {
60 Self::Apply {
61 operator: operator.into(),
62 operands,
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
68#[non_exhaustive]
69#[serde(
70 tag = "kind",
71 rename_all = "camelCase",
72 rename_all_fields = "camelCase"
73)]
74pub enum RewritePatternV0 {
75 Atom {
76 value: String,
77 },
78 Variable {
79 name: String,
80 },
81 Apply {
82 operator: String,
83 operands: Vec<RewritePatternV0>,
84 },
85}
86
87impl RewritePatternV0 {
88 pub fn atom(value: impl Into<String>) -> Self {
89 Self::Atom {
90 value: value.into(),
91 }
92 }
93
94 pub fn variable(name: impl Into<String>) -> Self {
95 Self::Variable { name: name.into() }
96 }
97
98 pub fn apply(operator: impl Into<String>, operands: Vec<Self>) -> Self {
99 Self::Apply {
100 operator: operator.into(),
101 operands,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
107#[non_exhaustive]
108#[serde(rename_all = "camelCase")]
109pub enum CascadeLevelCertV0 {
110 UserAgentNormal,
111 UserNormal,
112 AuthorNormal,
113 InlineNormal,
114 Animation,
115 AuthorImportant,
116 InlineImportant,
117 UserImportant,
118 UserAgentImportant,
119 Transition,
120}
121
122impl CascadeLevelCertV0 {
123 fn to_cascade_level(self) -> CascadeLevel {
124 match self {
125 Self::UserAgentNormal => CascadeLevel::UserAgentNormal,
126 Self::UserNormal => CascadeLevel::UserNormal,
127 Self::AuthorNormal => CascadeLevel::AuthorNormal,
128 Self::InlineNormal => CascadeLevel::InlineNormal,
129 Self::Animation => CascadeLevel::Animation,
130 Self::AuthorImportant => CascadeLevel::AuthorImportant,
131 Self::InlineImportant => CascadeLevel::InlineImportant,
132 Self::UserImportant => CascadeLevel::UserImportant,
133 Self::UserAgentImportant => CascadeLevel::UserAgentImportant,
134 Self::Transition => CascadeLevel::Transition,
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
140#[serde(rename_all = "camelCase")]
141pub struct CascadeWinnerKeyCertV0 {
142 pub level: CascadeLevelCertV0,
143 pub layer_important: bool,
144 pub layer_ordinal: Option<i32>,
145 pub scope_proximity: u32,
146 pub specificity_ids: u32,
147 pub specificity_classes: u32,
148 pub specificity_elements: u32,
149 pub source_order: u32,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
153#[serde(rename_all = "camelCase")]
154pub struct CascadeWinnerEqualityCertV0 {
155 pub before_winner_id: String,
156 pub after_winner_id: String,
157 pub before_key: CascadeWinnerKeyCertV0,
158 pub after_key: CascadeWinnerKeyCertV0,
159 pub before_class_attribute: String,
160 pub after_class_attribute: String,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
164#[non_exhaustive]
165#[serde(
166 tag = "kind",
167 rename_all = "camelCase",
168 rename_all_fields = "camelCase"
169)]
170pub enum ComputedValueTermV0 {
171 Literal {
172 value: String,
173 },
174 Composite {
175 values: Vec<ComputedValueTermV0>,
176 },
177 Variable {
178 name: String,
179 fallback: Option<Box<ComputedValueTermV0>>,
180 },
181 Initial,
182 Inherit,
183 Indeterminate,
184 GuaranteedInvalid,
185 Unset,
186}
187
188impl ComputedValueTermV0 {
189 fn to_cascade_value(&self) -> CascadeValue {
190 match self {
191 Self::Literal { value } => CascadeValue::Literal(value.clone()),
192 Self::Composite { values } => CascadeValue::Composite(
193 values
194 .iter()
195 .map(ComputedValueTermV0::to_cascade_value)
196 .collect(),
197 ),
198 Self::Variable { name, fallback } => CascadeValue::Var {
199 name: name.clone(),
200 fallback: fallback
201 .as_ref()
202 .map(|value| Box::new(value.to_cascade_value())),
203 },
204 Self::Initial => CascadeValue::Initial,
205 Self::Inherit => CascadeValue::Inherit,
206 Self::Indeterminate => CascadeValue::Indeterminate,
207 Self::GuaranteedInvalid => CascadeValue::GuaranteedInvalid,
208 Self::Unset => CascadeValue::Unset,
209 }
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
214#[serde(rename_all = "camelCase")]
215pub struct ComputedValueEnvironmentEntryV0 {
216 pub name: String,
217 pub value: ComputedValueTermV0,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
221#[serde(rename_all = "camelCase")]
222pub struct ComputedValueEqualityCertV0 {
223 pub property: String,
224 pub before_environment: Vec<ComputedValueEnvironmentEntryV0>,
225 pub after_environment: Vec<ComputedValueEnvironmentEntryV0>,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
229#[serde(rename_all = "camelCase")]
230pub struct SourceMapTraceSegmentV0 {
231 pub source_path: String,
232 pub source_digest: String,
233 pub original_start: usize,
234 pub original_end: usize,
235 pub generated_start: usize,
236 pub generated_end: usize,
237 pub pass_id: String,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
241#[serde(rename_all = "camelCase")]
242pub struct SourceMapTraceCertV0 {
243 pub before_segments: Vec<SourceMapTraceSegmentV0>,
244 pub after_segments: Vec<SourceMapTraceSegmentV0>,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
248#[serde(rename_all = "camelCase")]
249pub struct TokenOwnershipCertEntryV0 {
250 pub emitted_token: String,
251 pub module_paths: Vec<String>,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
255#[serde(rename_all = "camelCase")]
256pub struct TokenOwnershipSeparabilityCertV0 {
257 pub complete: bool,
258 pub modeled_preimage_count: usize,
259 pub emitted_token_count: usize,
260 pub ownerships: Vec<TokenOwnershipCertEntryV0>,
261 pub unattributed_emitted_token_count: usize,
262 pub interface_mismatch_count: usize,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
266#[serde(rename_all = "camelCase")]
267pub struct TransformIndependenceObservationCertRowV0 {
268 pub fixture_id: String,
269 pub observer: String,
270 pub left_then_right: String,
271 pub right_then_left: String,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
275#[serde(rename_all = "camelCase")]
276pub struct TransformIndependenceCertV0 {
277 pub left_pass_id: String,
278 pub right_pass_id: String,
279 pub observation_profile_id: String,
280 pub profile_observers: Vec<String>,
281 pub observation_rows: Vec<TransformIndependenceObservationCertRowV0>,
282 pub left_preconditions: Vec<String>,
283 pub right_preconditions: Vec<String>,
284 pub left_preserves_right_preconditions: Vec<String>,
285 pub right_preserves_left_preconditions: Vec<String>,
286 pub disqualifying_descriptor_edges: Vec<String>,
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
290#[non_exhaustive]
291#[serde(rename_all = "camelCase")]
292pub enum RewriteSideConditionKindV0 {
293 NoSideCondition,
294 CascadeWinnerEquality,
295 ComputedValueEquality,
296 SourceMapTrace,
297 TokenOwnershipSeparability,
298 TransformIndependence,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
302#[non_exhaustive]
303#[serde(tag = "kind", rename_all = "camelCase")]
304pub enum SideConditionCertV0 {
305 NoSideCondition,
306 CascadeWinnerEquality {
307 certificate: CascadeWinnerEqualityCertV0,
308 },
309 ComputedValueEquality {
310 certificate: ComputedValueEqualityCertV0,
311 },
312 SourceMapTrace {
313 certificate: SourceMapTraceCertV0,
314 },
315 TokenOwnershipSeparability {
316 certificate: TokenOwnershipSeparabilityCertV0,
317 },
318 TransformIndependence {
319 certificate: Box<TransformIndependenceCertV0>,
320 },
321}
322
323impl SideConditionCertV0 {
324 fn kind(&self) -> RewriteSideConditionKindV0 {
325 match self {
326 Self::NoSideCondition => RewriteSideConditionKindV0::NoSideCondition,
327 Self::CascadeWinnerEquality { .. } => RewriteSideConditionKindV0::CascadeWinnerEquality,
328 Self::ComputedValueEquality { .. } => RewriteSideConditionKindV0::ComputedValueEquality,
329 Self::SourceMapTrace { .. } => RewriteSideConditionKindV0::SourceMapTrace,
330 Self::TokenOwnershipSeparability { .. } => {
331 RewriteSideConditionKindV0::TokenOwnershipSeparability
332 }
333 Self::TransformIndependence { .. } => RewriteSideConditionKindV0::TransformIndependence,
334 }
335 }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
339#[serde(rename_all = "camelCase")]
340pub struct RewriteOperatorV0 {
341 pub operator: String,
342 pub arity: usize,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
346#[serde(rename_all = "camelCase")]
347pub struct RewriteRuleV0 {
348 pub rule_id: String,
349 pub before_pattern: RewritePatternV0,
350 pub after_pattern: RewritePatternV0,
351 pub side_condition_kind: RewriteSideConditionKindV0,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
355#[serde(rename_all = "camelCase")]
356pub struct RewriteRuleCatalogV0 {
357 pub schema_version: String,
358 pub operators: Vec<RewriteOperatorV0>,
359 pub rules: Vec<RewriteRuleV0>,
360}
361
362#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
363#[serde(rename_all = "camelCase")]
364pub struct RewriteSubstitutionEntryV0 {
365 pub variable: String,
366 pub term: RewriteTermV0,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
370#[non_exhaustive]
371#[serde(
372 tag = "kind",
373 rename_all = "camelCase",
374 rename_all_fields = "camelCase"
375)]
376pub enum RewriteCertificateV0 {
377 Refl {
378 term: RewriteTermV0,
379 },
380 Sym {
381 certificate: Box<RewriteCertificateV0>,
382 },
383 Trans {
384 left: Box<RewriteCertificateV0>,
385 right: Box<RewriteCertificateV0>,
386 },
387 Cong {
388 operator: String,
389 certificates: Vec<RewriteCertificateV0>,
390 },
391 Rewrite {
392 rule_id: String,
393 substitution: Vec<RewriteSubstitutionEntryV0>,
394 side_condition: SideConditionCertV0,
395 },
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
399#[serde(rename_all = "camelCase")]
400pub struct RewriteCertificateEnvelopeV0 {
401 pub schema_version: String,
402 pub max_depth: usize,
403 pub max_nodes: usize,
404 pub certificate: RewriteCertificateV0,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
408#[serde(rename_all = "camelCase")]
409pub struct CanonicalRewriteAssumptionV0 {
410 pub name: String,
411 pub value: String,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
415#[serde(rename_all = "camelCase")]
416pub struct CanonicalRewriteAssumptionsV0 {
417 pub schema_version: String,
418 pub entries: Vec<CanonicalRewriteAssumptionV0>,
419}
420
421impl Default for CanonicalRewriteAssumptionsV0 {
422 fn default() -> Self {
423 Self {
424 schema_version: CANONICAL_REWRITE_ASSUMPTIONS_SCHEMA_VERSION_V0.to_owned(),
425 entries: Vec::new(),
426 }
427 }
428}
429
430#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
431#[non_exhaustive]
432#[serde(rename_all = "camelCase")]
433pub enum RewriteCheckInputV0 {
434 BeforeTerm,
435 AfterTerm,
436 RuleCatalog,
437 Certificate,
438 Assumptions,
439 SerializedCertificate,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
443#[serde(rename_all = "camelCase")]
444pub struct RewriteFailureSiteV0 {
445 pub input: RewriteCheckInputV0,
446 pub certificate_path: Vec<usize>,
447 pub term_path: Vec<usize>,
448 pub rule_id: Option<String>,
449}
450
451impl RewriteFailureSiteV0 {
452 fn root(input: RewriteCheckInputV0) -> Self {
453 Self {
454 input,
455 certificate_path: Vec::new(),
456 term_path: Vec::new(),
457 rule_id: None,
458 }
459 }
460
461 fn certificate(certificate_path: &[usize]) -> Self {
462 Self {
463 input: RewriteCheckInputV0::Certificate,
464 certificate_path: certificate_path.to_vec(),
465 term_path: Vec::new(),
466 rule_id: None,
467 }
468 }
469
470 fn rule(rule_id: &str, term_path: Vec<usize>) -> Self {
471 Self {
472 input: RewriteCheckInputV0::RuleCatalog,
473 certificate_path: Vec::new(),
474 term_path,
475 rule_id: Some(rule_id.to_owned()),
476 }
477 }
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
481#[non_exhaustive]
482#[serde(
483 tag = "kind",
484 rename_all = "camelCase",
485 rename_all_fields = "camelCase"
486)]
487pub enum CertificateRejectionKindV0 {
488 SchemaVersionMismatch {
489 expected: String,
490 observed: String,
491 },
492 BoundOutOfRange {
493 bound: String,
494 declared: usize,
495 hard_maximum: usize,
496 },
497 DeclaredBoundExceeded {
498 bound: String,
499 declared: usize,
500 observed: usize,
501 },
502 CatalogLimitExceeded {
503 collection: String,
504 observed: usize,
505 maximum: usize,
506 },
507 DuplicateOperator {
508 operator: String,
509 },
510 DuplicateRule {
511 rule_id: String,
512 },
513 UnknownOperator {
514 operator: String,
515 },
516 OperatorArityMismatch {
517 operator: String,
518 expected: usize,
519 observed: usize,
520 },
521 EmptyVariable,
522 DuplicateAssumption {
523 name: String,
524 },
525 DuplicateSubstitutionVariable {
526 variable: String,
527 },
528 MissingSubstitutionVariable {
529 variable: String,
530 },
531 UnexpectedSubstitutionVariable {
532 variable: String,
533 },
534 UnknownRule {
535 rule_id: String,
536 },
537 SideConditionKindMismatch {
538 expected: RewriteSideConditionKindV0,
539 observed: RewriteSideConditionKindV0,
540 },
541 InvalidLayerOrdinal {
542 observed: i32,
543 },
544 CascadeWinnerEqualityRejected {
545 winner_ids_equal: bool,
546 cascade_keys_equal: bool,
547 token_support_equal: bool,
548 },
549 CascadeTokenizationUnavailable,
550 DuplicateComputedValueEnvironmentEntry {
551 name: String,
552 },
553 ComputedValueEqualityRejected {
554 property: String,
555 before_present: bool,
556 after_present: bool,
557 },
558 EmptySourceMapTrace,
559 SourceMapTraceRejected {
560 segment_index: usize,
561 reason: String,
562 },
563 TokenOwnershipSeparabilityRejected {
564 reason: String,
565 token: Option<String>,
566 },
567 TransformIndependenceRejected {
568 reason: String,
569 left_pass_id: String,
570 right_pass_id: String,
571 },
572 TransitiveMiddleMismatch,
573 EndpointMismatch {
574 endpoint: RewriteCheckInputV0,
575 },
576 MissingCertificate,
577 MalformedCertificate {
578 message: String,
579 },
580 DerivedTermLimitExceeded,
581}
582
583#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
584#[serde(rename_all = "camelCase")]
585pub struct CertificateRejectionV0 {
586 pub site: Box<RewriteFailureSiteV0>,
587 pub rejection: Box<CertificateRejectionKindV0>,
588}
589
590impl CertificateRejectionV0 {
591 fn new(site: RewriteFailureSiteV0, rejection: CertificateRejectionKindV0) -> Self {
592 Self {
593 site: Box::new(site),
594 rejection: Box::new(rejection),
595 }
596 }
597}
598
599#[derive(Debug, Clone, Copy, PartialEq, Eq)]
600struct RewriteIssuanceSealV0(());
601
602#[derive(Debug, Clone, PartialEq, Eq)]
619pub struct RewriteIssuanceTokenV0 {
620 before_digest: [u8; 32],
621 after_digest: [u8; 32],
622 catalog_schema_id: &'static str,
623 catalog_content_digest: [u8; 32],
624 checked_rule_ids: Vec<String>,
625 _seal: RewriteIssuanceSealV0,
626}
627
628impl RewriteIssuanceTokenV0 {
629 fn issue(
630 before: &RewriteTermV0,
631 after: &RewriteTermV0,
632 catalog: &RewriteRuleCatalogV0,
633 checked_rule_ids: Vec<String>,
634 ) -> Self {
635 Self {
636 before_digest: term_digest_v0(before),
637 after_digest: term_digest_v0(after),
638 catalog_schema_id: REWRITE_RULE_CATALOG_SCHEMA_ID_V0,
639 catalog_content_digest: catalog_content_digest_v0(catalog),
640 checked_rule_ids,
641 _seal: RewriteIssuanceSealV0(()),
642 }
643 }
644
645 pub fn before_digest_hex_v0(&self) -> String {
646 digest_hex_v0(&self.before_digest)
647 }
648
649 pub fn after_digest_hex_v0(&self) -> String {
650 digest_hex_v0(&self.after_digest)
651 }
652
653 pub fn checked_rule_ids_v0(&self) -> &[String] {
654 self.checked_rule_ids.as_slice()
655 }
656
657 pub const fn catalog_schema_id_v0(&self) -> &'static str {
658 self.catalog_schema_id
659 }
660
661 pub fn catalog_content_digest_hex_v0(&self) -> String {
662 digest_hex_v0(&self.catalog_content_digest)
663 }
664
665 pub fn matches_endpoints_v0(&self, before: &RewriteTermV0, after: &RewriteTermV0) -> bool {
668 self.before_digest == term_digest_v0(before) && self.after_digest == term_digest_v0(after)
669 }
670
671 pub fn matches_catalog_v0(&self, catalog: &RewriteRuleCatalogV0) -> bool {
675 self.catalog_schema_id == REWRITE_RULE_CATALOG_SCHEMA_ID_V0
676 && self.catalog_content_digest == catalog_content_digest_v0(catalog)
677 }
678}
679
680struct ValidatedCatalogV0<'a> {
681 operators: BTreeMap<&'a str, usize>,
682 rules: BTreeMap<&'a str, &'a RewriteRuleV0>,
683}
684
685struct DerivedRewriteV0 {
686 before: RewriteTermV0,
687 after: RewriteTermV0,
688 checked_rule_ids: Vec<String>,
689}
690
691pub fn check_rewrite_certificate_v0(
692 before: &RewriteTermV0,
693 after: &RewriteTermV0,
694 rule_catalog: &RewriteRuleCatalogV0,
695 certificate: &RewriteCertificateEnvelopeV0,
696 assumptions: &CanonicalRewriteAssumptionsV0,
697) -> Result<RewriteIssuanceTokenV0, CertificateRejectionV0> {
698 validate_schema(
699 &certificate.schema_version,
700 REWRITE_CERTIFICATE_SCHEMA_VERSION_V0,
701 RewriteCheckInputV0::Certificate,
702 )?;
703 validate_certificate_bounds(certificate)?;
704 let catalog = validate_catalog(rule_catalog)?;
705 validate_assumptions(assumptions)?;
706 validate_term(
707 before,
708 RewriteCheckInputV0::BeforeTerm,
709 &[],
710 &catalog.operators,
711 )?;
712 validate_term(
713 after,
714 RewriteCheckInputV0::AfterTerm,
715 &[],
716 &catalog.operators,
717 )?;
718
719 let mut path = Vec::new();
720 let derived = derive_certificate(&certificate.certificate, &catalog, &mut path)?;
721 if derived.before != *before {
722 let term_path = first_term_mismatch_path(before, &derived.before);
723 return Err(CertificateRejectionV0::new(
724 RewriteFailureSiteV0 {
725 input: RewriteCheckInputV0::BeforeTerm,
726 certificate_path: Vec::new(),
727 term_path,
728 rule_id: None,
729 },
730 CertificateRejectionKindV0::EndpointMismatch {
731 endpoint: RewriteCheckInputV0::BeforeTerm,
732 },
733 ));
734 }
735 if derived.after != *after {
736 let term_path = first_term_mismatch_path(after, &derived.after);
737 return Err(CertificateRejectionV0::new(
738 RewriteFailureSiteV0 {
739 input: RewriteCheckInputV0::AfterTerm,
740 certificate_path: Vec::new(),
741 term_path,
742 rule_id: None,
743 },
744 CertificateRejectionKindV0::EndpointMismatch {
745 endpoint: RewriteCheckInputV0::AfterTerm,
746 },
747 ));
748 }
749
750 Ok(RewriteIssuanceTokenV0::issue(
751 before,
752 after,
753 rule_catalog,
754 derived.checked_rule_ids,
755 ))
756}
757
758pub fn check_optional_rewrite_certificate_v0(
759 before: &RewriteTermV0,
760 after: &RewriteTermV0,
761 rule_catalog: &RewriteRuleCatalogV0,
762 certificate: Option<&RewriteCertificateEnvelopeV0>,
763 assumptions: &CanonicalRewriteAssumptionsV0,
764) -> Result<RewriteIssuanceTokenV0, CertificateRejectionV0> {
765 let Some(certificate) = certificate else {
766 return Err(CertificateRejectionV0::new(
767 RewriteFailureSiteV0::root(RewriteCheckInputV0::Certificate),
768 CertificateRejectionKindV0::MissingCertificate,
769 ));
770 };
771 check_rewrite_certificate_v0(before, after, rule_catalog, certificate, assumptions)
772}
773
774pub fn check_serialized_rewrite_certificate_v0(
775 before: &RewriteTermV0,
776 after: &RewriteTermV0,
777 rule_catalog: &RewriteRuleCatalogV0,
778 certificate_json: &str,
779 assumptions: &CanonicalRewriteAssumptionsV0,
780) -> Result<RewriteIssuanceTokenV0, CertificateRejectionV0> {
781 let certificate = serde_json::from_str::<RewriteCertificateEnvelopeV0>(certificate_json)
782 .map_err(|error| {
783 CertificateRejectionV0::new(
784 RewriteFailureSiteV0::root(RewriteCheckInputV0::SerializedCertificate),
785 CertificateRejectionKindV0::MalformedCertificate {
786 message: error.to_string(),
787 },
788 )
789 })?;
790 check_rewrite_certificate_v0(before, after, rule_catalog, &certificate, assumptions)
791}
792
793pub fn selector_rewrite_rule_catalog_v0() -> RewriteRuleCatalogV0 {
794 RewriteRuleCatalogV0 {
795 schema_version: REWRITE_RULE_CATALOG_SCHEMA_VERSION_V0.to_owned(),
796 operators: vec![
797 RewriteOperatorV0 {
798 operator: "selectorConcat".to_owned(),
799 arity: 2,
800 },
801 RewriteOperatorV0 {
802 operator: "selectorIs".to_owned(),
803 arity: 1,
804 },
805 RewriteOperatorV0 {
806 operator: "selectorWhere".to_owned(),
807 arity: 1,
808 },
809 RewriteOperatorV0 {
810 operator: "selectorList2".to_owned(),
811 arity: 2,
812 },
813 ],
814 rules: vec![
815 RewriteRuleV0 {
816 rule_id: "selector-list-deduplicate-v0".to_owned(),
817 before_pattern: RewritePatternV0::apply(
818 "selectorList2",
819 vec![
820 RewritePatternV0::variable("x"),
821 RewritePatternV0::variable("x"),
822 ],
823 ),
824 after_pattern: RewritePatternV0::variable("x"),
825 side_condition_kind: RewriteSideConditionKindV0::NoSideCondition,
826 },
827 RewriteRuleV0 {
828 rule_id: "selector-is-single-v0".to_owned(),
829 before_pattern: RewritePatternV0::apply(
830 "selectorIs",
831 vec![RewritePatternV0::variable("x")],
832 ),
833 after_pattern: RewritePatternV0::variable("x"),
834 side_condition_kind: RewriteSideConditionKindV0::NoSideCondition,
835 },
836 ],
837 }
838}
839
840pub fn selector_rewrite_rule_catalog_with_cascade_winner_equality_v0() -> RewriteRuleCatalogV0 {
841 let mut catalog = selector_rewrite_rule_catalog_v0();
842 for rule in &mut catalog.rules {
843 rule.side_condition_kind = RewriteSideConditionKindV0::CascadeWinnerEquality;
844 }
845 catalog
846}
847
848fn validate_schema(
849 observed: &str,
850 expected: &str,
851 input: RewriteCheckInputV0,
852) -> Result<(), CertificateRejectionV0> {
853 if observed == expected {
854 return Ok(());
855 }
856 Err(CertificateRejectionV0::new(
857 RewriteFailureSiteV0::root(input),
858 CertificateRejectionKindV0::SchemaVersionMismatch {
859 expected: expected.to_owned(),
860 observed: observed.to_owned(),
861 },
862 ))
863}
864
865fn validate_certificate_bounds(
866 envelope: &RewriteCertificateEnvelopeV0,
867) -> Result<(), CertificateRejectionV0> {
868 for (bound, declared, hard_maximum) in [
869 (
870 "depth",
871 envelope.max_depth,
872 REWRITE_CERTIFICATE_MAX_DEPTH_V0,
873 ),
874 (
875 "nodes",
876 envelope.max_nodes,
877 REWRITE_CERTIFICATE_MAX_NODES_V0,
878 ),
879 ] {
880 if declared == 0 || declared > hard_maximum {
881 return Err(CertificateRejectionV0::new(
882 RewriteFailureSiteV0::root(RewriteCheckInputV0::Certificate),
883 CertificateRejectionKindV0::BoundOutOfRange {
884 bound: bound.to_owned(),
885 declared,
886 hard_maximum,
887 },
888 ));
889 }
890 }
891
892 let mut stack = vec![(&envelope.certificate, 1_usize, Vec::<usize>::new())];
893 let mut nodes = 0_usize;
894 while let Some((certificate, depth, path)) = stack.pop() {
895 nodes = nodes.saturating_add(1);
896 if depth > envelope.max_depth {
897 return Err(CertificateRejectionV0::new(
898 RewriteFailureSiteV0::certificate(path.as_slice()),
899 CertificateRejectionKindV0::DeclaredBoundExceeded {
900 bound: "depth".to_owned(),
901 declared: envelope.max_depth,
902 observed: depth,
903 },
904 ));
905 }
906 if nodes > envelope.max_nodes {
907 return Err(CertificateRejectionV0::new(
908 RewriteFailureSiteV0::certificate(path.as_slice()),
909 CertificateRejectionKindV0::DeclaredBoundExceeded {
910 bound: "nodes".to_owned(),
911 declared: envelope.max_nodes,
912 observed: nodes,
913 },
914 ));
915 }
916 match certificate {
917 RewriteCertificateV0::Refl { .. } | RewriteCertificateV0::Rewrite { .. } => {}
918 RewriteCertificateV0::Sym { certificate } => {
919 let mut child_path = path;
920 child_path.push(0);
921 stack.push((certificate, depth.saturating_add(1), child_path));
922 }
923 RewriteCertificateV0::Trans { left, right } => {
924 let mut right_path = path.clone();
925 right_path.push(1);
926 stack.push((right, depth.saturating_add(1), right_path));
927 let mut left_path = path;
928 left_path.push(0);
929 stack.push((left, depth.saturating_add(1), left_path));
930 }
931 RewriteCertificateV0::Cong { certificates, .. } => {
932 for (index, child) in certificates.iter().enumerate().rev() {
933 let mut child_path = path.clone();
934 child_path.push(index);
935 stack.push((child, depth.saturating_add(1), child_path));
936 }
937 }
938 }
939 }
940 Ok(())
941}
942
943fn validate_catalog(
944 catalog: &RewriteRuleCatalogV0,
945) -> Result<ValidatedCatalogV0<'_>, CertificateRejectionV0> {
946 validate_schema(
947 &catalog.schema_version,
948 REWRITE_RULE_CATALOG_SCHEMA_VERSION_V0,
949 RewriteCheckInputV0::RuleCatalog,
950 )?;
951 validate_catalog_limit(
952 "operators",
953 catalog.operators.len(),
954 REWRITE_RULE_CATALOG_MAX_OPERATORS_V0,
955 )?;
956 validate_catalog_limit(
957 "rules",
958 catalog.rules.len(),
959 REWRITE_RULE_CATALOG_MAX_RULES_V0,
960 )?;
961
962 let mut operators = BTreeMap::new();
963 for operator in &catalog.operators {
964 if operators
965 .insert(operator.operator.as_str(), operator.arity)
966 .is_some()
967 {
968 return Err(CertificateRejectionV0::new(
969 RewriteFailureSiteV0::root(RewriteCheckInputV0::RuleCatalog),
970 CertificateRejectionKindV0::DuplicateOperator {
971 operator: operator.operator.clone(),
972 },
973 ));
974 }
975 }
976
977 let mut rules = BTreeMap::new();
978 for rule in &catalog.rules {
979 if rules.insert(rule.rule_id.as_str(), rule).is_some() {
980 return Err(CertificateRejectionV0::new(
981 RewriteFailureSiteV0::rule(&rule.rule_id, Vec::new()),
982 CertificateRejectionKindV0::DuplicateRule {
983 rule_id: rule.rule_id.clone(),
984 },
985 ));
986 }
987 validate_pattern(&rule.before_pattern, &rule.rule_id, &operators)?;
988 validate_pattern(&rule.after_pattern, &rule.rule_id, &operators)?;
989 }
990 Ok(ValidatedCatalogV0 { operators, rules })
991}
992
993fn validate_catalog_limit(
994 collection: &str,
995 observed: usize,
996 maximum: usize,
997) -> Result<(), CertificateRejectionV0> {
998 if observed <= maximum {
999 return Ok(());
1000 }
1001 Err(CertificateRejectionV0::new(
1002 RewriteFailureSiteV0::root(RewriteCheckInputV0::RuleCatalog),
1003 CertificateRejectionKindV0::CatalogLimitExceeded {
1004 collection: collection.to_owned(),
1005 observed,
1006 maximum,
1007 },
1008 ))
1009}
1010
1011fn validate_pattern(
1012 pattern: &RewritePatternV0,
1013 rule_id: &str,
1014 operators: &BTreeMap<&str, usize>,
1015) -> Result<(), CertificateRejectionV0> {
1016 let mut stack = vec![(pattern, 1_usize, Vec::<usize>::new())];
1017 let mut nodes = 0_usize;
1018 while let Some((current, depth, path)) = stack.pop() {
1019 nodes = nodes.saturating_add(1);
1020 if depth > REWRITE_TERM_MAX_DEPTH_V0 || nodes > REWRITE_TERM_MAX_NODES_V0 {
1021 return Err(CertificateRejectionV0::new(
1022 RewriteFailureSiteV0::rule(rule_id, path),
1023 CertificateRejectionKindV0::DerivedTermLimitExceeded,
1024 ));
1025 }
1026 match current {
1027 RewritePatternV0::Atom { .. } => {}
1028 RewritePatternV0::Variable { name } => {
1029 if name.is_empty() {
1030 return Err(CertificateRejectionV0::new(
1031 RewriteFailureSiteV0::rule(rule_id, path),
1032 CertificateRejectionKindV0::EmptyVariable,
1033 ));
1034 }
1035 }
1036 RewritePatternV0::Apply { operator, operands } => {
1037 validate_operator_arity(
1038 operator,
1039 operands.len(),
1040 operators,
1041 RewriteFailureSiteV0::rule(rule_id, path.clone()),
1042 )?;
1043 for (index, child) in operands.iter().enumerate().rev() {
1044 let mut child_path = path.clone();
1045 child_path.push(index);
1046 stack.push((child, depth.saturating_add(1), child_path));
1047 }
1048 }
1049 }
1050 }
1051 Ok(())
1052}
1053
1054fn validate_assumptions(
1055 assumptions: &CanonicalRewriteAssumptionsV0,
1056) -> Result<BTreeMap<&str, &str>, CertificateRejectionV0> {
1057 validate_schema(
1058 &assumptions.schema_version,
1059 CANONICAL_REWRITE_ASSUMPTIONS_SCHEMA_VERSION_V0,
1060 RewriteCheckInputV0::Assumptions,
1061 )?;
1062 let mut canonical = BTreeMap::new();
1063 for assumption in &assumptions.entries {
1064 if canonical
1065 .insert(assumption.name.as_str(), assumption.value.as_str())
1066 .is_some()
1067 {
1068 return Err(CertificateRejectionV0::new(
1069 RewriteFailureSiteV0::root(RewriteCheckInputV0::Assumptions),
1070 CertificateRejectionKindV0::DuplicateAssumption {
1071 name: assumption.name.clone(),
1072 },
1073 ));
1074 }
1075 }
1076 Ok(canonical)
1077}
1078
1079fn validate_term(
1080 term: &RewriteTermV0,
1081 input: RewriteCheckInputV0,
1082 certificate_path: &[usize],
1083 operators: &BTreeMap<&str, usize>,
1084) -> Result<(), CertificateRejectionV0> {
1085 let mut stack = vec![(term, 1_usize, Vec::<usize>::new())];
1086 let mut nodes = 0_usize;
1087 while let Some((current, depth, term_path)) = stack.pop() {
1088 nodes = nodes.saturating_add(1);
1089 if depth > REWRITE_TERM_MAX_DEPTH_V0 || nodes > REWRITE_TERM_MAX_NODES_V0 {
1090 return Err(CertificateRejectionV0::new(
1091 RewriteFailureSiteV0 {
1092 input,
1093 certificate_path: certificate_path.to_vec(),
1094 term_path,
1095 rule_id: None,
1096 },
1097 CertificateRejectionKindV0::DerivedTermLimitExceeded,
1098 ));
1099 }
1100 if let RewriteTermV0::Apply { operator, operands } = current {
1101 validate_operator_arity(
1102 operator,
1103 operands.len(),
1104 operators,
1105 RewriteFailureSiteV0 {
1106 input,
1107 certificate_path: certificate_path.to_vec(),
1108 term_path: term_path.clone(),
1109 rule_id: None,
1110 },
1111 )?;
1112 for (index, child) in operands.iter().enumerate().rev() {
1113 let mut child_path = term_path.clone();
1114 child_path.push(index);
1115 stack.push((child, depth.saturating_add(1), child_path));
1116 }
1117 }
1118 }
1119 Ok(())
1120}
1121
1122fn validate_operator_arity(
1123 operator: &str,
1124 observed: usize,
1125 operators: &BTreeMap<&str, usize>,
1126 site: RewriteFailureSiteV0,
1127) -> Result<(), CertificateRejectionV0> {
1128 let Some(expected) = operators.get(operator).copied() else {
1129 return Err(CertificateRejectionV0::new(
1130 site,
1131 CertificateRejectionKindV0::UnknownOperator {
1132 operator: operator.to_owned(),
1133 },
1134 ));
1135 };
1136 if expected == observed {
1137 return Ok(());
1138 }
1139 Err(CertificateRejectionV0::new(
1140 site,
1141 CertificateRejectionKindV0::OperatorArityMismatch {
1142 operator: operator.to_owned(),
1143 expected,
1144 observed,
1145 },
1146 ))
1147}
1148
1149fn derive_certificate(
1150 certificate: &RewriteCertificateV0,
1151 catalog: &ValidatedCatalogV0<'_>,
1152 path: &mut Vec<usize>,
1153) -> Result<DerivedRewriteV0, CertificateRejectionV0> {
1154 match certificate {
1155 RewriteCertificateV0::Refl { term } => {
1156 validate_term(
1157 term,
1158 RewriteCheckInputV0::Certificate,
1159 path,
1160 &catalog.operators,
1161 )?;
1162 Ok(DerivedRewriteV0 {
1163 before: term.clone(),
1164 after: term.clone(),
1165 checked_rule_ids: Vec::new(),
1166 })
1167 }
1168 RewriteCertificateV0::Sym { certificate } => {
1169 path.push(0);
1170 let child = derive_certificate(certificate, catalog, path);
1171 path.pop();
1172 let child = child?;
1173 Ok(DerivedRewriteV0 {
1174 before: child.after,
1175 after: child.before,
1176 checked_rule_ids: child.checked_rule_ids,
1177 })
1178 }
1179 RewriteCertificateV0::Trans { left, right } => {
1180 path.push(0);
1181 let left_derived = derive_certificate(left, catalog, path);
1182 path.pop();
1183 let left_derived = left_derived?;
1184 path.push(1);
1185 let right_derived = derive_certificate(right, catalog, path);
1186 path.pop();
1187 let right_derived = right_derived?;
1188 if left_derived.after != right_derived.before {
1189 return Err(CertificateRejectionV0::new(
1190 RewriteFailureSiteV0 {
1191 input: RewriteCheckInputV0::Certificate,
1192 certificate_path: path.clone(),
1193 term_path: first_term_mismatch_path(
1194 &left_derived.after,
1195 &right_derived.before,
1196 ),
1197 rule_id: None,
1198 },
1199 CertificateRejectionKindV0::TransitiveMiddleMismatch,
1200 ));
1201 }
1202 let mut checked_rule_ids = left_derived.checked_rule_ids;
1203 checked_rule_ids.extend(right_derived.checked_rule_ids);
1204 Ok(DerivedRewriteV0 {
1205 before: left_derived.before,
1206 after: right_derived.after,
1207 checked_rule_ids,
1208 })
1209 }
1210 RewriteCertificateV0::Cong {
1211 operator,
1212 certificates,
1213 } => {
1214 validate_operator_arity(
1215 operator,
1216 certificates.len(),
1217 &catalog.operators,
1218 RewriteFailureSiteV0::certificate(path.as_slice()),
1219 )?;
1220 let mut before_operands = Vec::with_capacity(certificates.len());
1221 let mut after_operands = Vec::with_capacity(certificates.len());
1222 let mut checked_rule_ids = Vec::new();
1223 for (index, child) in certificates.iter().enumerate() {
1224 path.push(index);
1225 let child_derived = derive_certificate(child, catalog, path);
1226 path.pop();
1227 let child_derived = child_derived?;
1228 before_operands.push(child_derived.before);
1229 after_operands.push(child_derived.after);
1230 checked_rule_ids.extend(child_derived.checked_rule_ids);
1231 }
1232 let before = RewriteTermV0::apply(operator, before_operands);
1233 let after = RewriteTermV0::apply(operator, after_operands);
1234 validate_term(
1235 &before,
1236 RewriteCheckInputV0::Certificate,
1237 path,
1238 &catalog.operators,
1239 )?;
1240 validate_term(
1241 &after,
1242 RewriteCheckInputV0::Certificate,
1243 path,
1244 &catalog.operators,
1245 )?;
1246 Ok(DerivedRewriteV0 {
1247 before,
1248 after,
1249 checked_rule_ids,
1250 })
1251 }
1252 RewriteCertificateV0::Rewrite {
1253 rule_id,
1254 substitution,
1255 side_condition,
1256 } => derive_rule_application(rule_id, substitution, side_condition, catalog, path),
1257 }
1258}
1259
1260fn derive_rule_application(
1261 rule_id: &str,
1262 substitution: &[RewriteSubstitutionEntryV0],
1263 side_condition: &SideConditionCertV0,
1264 catalog: &ValidatedCatalogV0<'_>,
1265 path: &[usize],
1266) -> Result<DerivedRewriteV0, CertificateRejectionV0> {
1267 let Some(rule) = catalog.rules.get(rule_id).copied() else {
1268 return Err(CertificateRejectionV0::new(
1269 RewriteFailureSiteV0 {
1270 input: RewriteCheckInputV0::Certificate,
1271 certificate_path: path.to_vec(),
1272 term_path: Vec::new(),
1273 rule_id: Some(rule_id.to_owned()),
1274 },
1275 CertificateRejectionKindV0::UnknownRule {
1276 rule_id: rule_id.to_owned(),
1277 },
1278 ));
1279 };
1280 let observed_kind = side_condition.kind();
1281 if observed_kind != rule.side_condition_kind {
1282 return Err(CertificateRejectionV0::new(
1283 RewriteFailureSiteV0 {
1284 input: RewriteCheckInputV0::Certificate,
1285 certificate_path: path.to_vec(),
1286 term_path: Vec::new(),
1287 rule_id: Some(rule_id.to_owned()),
1288 },
1289 CertificateRejectionKindV0::SideConditionKindMismatch {
1290 expected: rule.side_condition_kind,
1291 observed: observed_kind,
1292 },
1293 ));
1294 }
1295 check_side_condition_v0(side_condition, path, rule_id)?;
1296
1297 let mut substitutions = BTreeMap::new();
1298 for entry in substitution {
1299 if substitutions
1300 .insert(entry.variable.as_str(), &entry.term)
1301 .is_some()
1302 {
1303 return Err(CertificateRejectionV0::new(
1304 RewriteFailureSiteV0 {
1305 input: RewriteCheckInputV0::Certificate,
1306 certificate_path: path.to_vec(),
1307 term_path: Vec::new(),
1308 rule_id: Some(rule_id.to_owned()),
1309 },
1310 CertificateRejectionKindV0::DuplicateSubstitutionVariable {
1311 variable: entry.variable.clone(),
1312 },
1313 ));
1314 }
1315 validate_term(
1316 &entry.term,
1317 RewriteCheckInputV0::Certificate,
1318 path,
1319 &catalog.operators,
1320 )?;
1321 }
1322
1323 let mut variables = BTreeSet::new();
1324 collect_pattern_variables(&rule.before_pattern, &mut variables);
1325 collect_pattern_variables(&rule.after_pattern, &mut variables);
1326 if let Some(missing) = variables
1327 .iter()
1328 .find(|variable| !substitutions.contains_key(variable.as_str()))
1329 {
1330 return Err(CertificateRejectionV0::new(
1331 RewriteFailureSiteV0 {
1332 input: RewriteCheckInputV0::Certificate,
1333 certificate_path: path.to_vec(),
1334 term_path: Vec::new(),
1335 rule_id: Some(rule_id.to_owned()),
1336 },
1337 CertificateRejectionKindV0::MissingSubstitutionVariable {
1338 variable: (*missing).clone(),
1339 },
1340 ));
1341 }
1342 if let Some(extra) = substitutions
1343 .keys()
1344 .find(|variable| !variables.contains(**variable))
1345 {
1346 return Err(CertificateRejectionV0::new(
1347 RewriteFailureSiteV0 {
1348 input: RewriteCheckInputV0::Certificate,
1349 certificate_path: path.to_vec(),
1350 term_path: Vec::new(),
1351 rule_id: Some(rule_id.to_owned()),
1352 },
1353 CertificateRejectionKindV0::UnexpectedSubstitutionVariable {
1354 variable: (*extra).to_owned(),
1355 },
1356 ));
1357 }
1358
1359 let mut before_nodes = 0_usize;
1360 let before = instantiate_pattern(&rule.before_pattern, &substitutions, &mut before_nodes)
1361 .ok_or_else(|| {
1362 CertificateRejectionV0::new(
1363 RewriteFailureSiteV0 {
1364 input: RewriteCheckInputV0::Certificate,
1365 certificate_path: path.to_vec(),
1366 term_path: Vec::new(),
1367 rule_id: Some(rule_id.to_owned()),
1368 },
1369 CertificateRejectionKindV0::DerivedTermLimitExceeded,
1370 )
1371 })?;
1372 let mut after_nodes = 0_usize;
1373 let after = instantiate_pattern(&rule.after_pattern, &substitutions, &mut after_nodes)
1374 .ok_or_else(|| {
1375 CertificateRejectionV0::new(
1376 RewriteFailureSiteV0 {
1377 input: RewriteCheckInputV0::Certificate,
1378 certificate_path: path.to_vec(),
1379 term_path: Vec::new(),
1380 rule_id: Some(rule_id.to_owned()),
1381 },
1382 CertificateRejectionKindV0::DerivedTermLimitExceeded,
1383 )
1384 })?;
1385 Ok(DerivedRewriteV0 {
1386 before,
1387 after,
1388 checked_rule_ids: vec![rule_id.to_owned()],
1389 })
1390}
1391
1392fn check_side_condition_v0(
1393 side_condition: &SideConditionCertV0,
1394 path: &[usize],
1395 rule_id: &str,
1396) -> Result<(), CertificateRejectionV0> {
1397 match side_condition {
1398 SideConditionCertV0::NoSideCondition => Ok(()),
1399 SideConditionCertV0::CascadeWinnerEquality { certificate } => {
1400 check_cascade_winner_equality_v0(certificate, path, rule_id)
1401 }
1402 SideConditionCertV0::ComputedValueEquality { certificate } => {
1403 check_computed_value_equality_v0(certificate, path, rule_id)
1404 }
1405 SideConditionCertV0::SourceMapTrace { certificate } => {
1406 check_source_map_trace_v0(certificate, path, rule_id)
1407 }
1408 SideConditionCertV0::TokenOwnershipSeparability { certificate } => {
1409 check_token_ownership_separability_v0(certificate, path, rule_id)
1410 }
1411 SideConditionCertV0::TransformIndependence { certificate } => {
1412 check_transform_independence_v0(certificate, path, rule_id)
1413 }
1414 }
1415}
1416
1417fn check_transform_independence_v0(
1418 certificate: &TransformIndependenceCertV0,
1419 path: &[usize],
1420 rule_id: &str,
1421) -> Result<(), CertificateRejectionV0> {
1422 let reject = |reason: &str| {
1423 CertificateRejectionV0::new(
1424 side_condition_site_v0(path, rule_id),
1425 CertificateRejectionKindV0::TransformIndependenceRejected {
1426 reason: reason.to_owned(),
1427 left_pass_id: certificate.left_pass_id.clone(),
1428 right_pass_id: certificate.right_pass_id.clone(),
1429 },
1430 )
1431 };
1432 if certificate.left_pass_id.is_empty()
1433 || certificate.right_pass_id.is_empty()
1434 || certificate.left_pass_id == certificate.right_pass_id
1435 {
1436 return Err(reject("independence pair is empty or reflexive"));
1437 }
1438 if certificate.observation_profile_id.is_empty() || certificate.profile_observers.is_empty() {
1439 return Err(reject("observation profile is empty"));
1440 }
1441 if !certificate.disqualifying_descriptor_edges.is_empty() {
1442 return Err(reject(
1443 "descriptor dependency or conflict disqualifies the pair",
1444 ));
1445 }
1446
1447 let profile_observers = certificate
1448 .profile_observers
1449 .iter()
1450 .map(String::as_str)
1451 .collect::<BTreeSet<_>>();
1452 if profile_observers.len() != certificate.profile_observers.len()
1453 || profile_observers.contains("")
1454 {
1455 return Err(reject(
1456 "observation profile contains an empty or duplicate observer",
1457 ));
1458 }
1459 if certificate.observation_rows.is_empty() {
1460 return Err(reject("observational commutation has no checked rows"));
1461 }
1462 let mut observed_profile_members = BTreeSet::new();
1463 let mut row_keys = BTreeSet::new();
1464 for row in &certificate.observation_rows {
1465 if row.fixture_id.is_empty() || !profile_observers.contains(row.observer.as_str()) {
1466 return Err(reject(
1467 "observation row does not resolve to the named profile",
1468 ));
1469 }
1470 if !row_keys.insert((row.fixture_id.as_str(), row.observer.as_str())) {
1471 return Err(reject("observation row is duplicated"));
1472 }
1473 if row.left_then_right != row.right_then_left {
1474 return Err(reject(
1475 "adjacent transform orders have different observations",
1476 ));
1477 }
1478 observed_profile_members.insert(row.observer.as_str());
1479 }
1480 if observed_profile_members != profile_observers {
1481 return Err(reject("observation rows do not cover the named profile"));
1482 }
1483
1484 let left_preconditions = certificate
1485 .left_preconditions
1486 .iter()
1487 .map(String::as_str)
1488 .collect::<BTreeSet<_>>();
1489 let right_preconditions = certificate
1490 .right_preconditions
1491 .iter()
1492 .map(String::as_str)
1493 .collect::<BTreeSet<_>>();
1494 let left_preserves_right = certificate
1495 .left_preserves_right_preconditions
1496 .iter()
1497 .map(String::as_str)
1498 .collect::<BTreeSet<_>>();
1499 let right_preserves_left = certificate
1500 .right_preserves_left_preconditions
1501 .iter()
1502 .map(String::as_str)
1503 .collect::<BTreeSet<_>>();
1504 if left_preconditions.len() != certificate.left_preconditions.len()
1505 || right_preconditions.len() != certificate.right_preconditions.len()
1506 || left_preserves_right.len() != certificate.left_preserves_right_preconditions.len()
1507 || right_preserves_left.len() != certificate.right_preserves_left_preconditions.len()
1508 {
1509 return Err(reject("precondition evidence contains duplicate entries"));
1510 }
1511 if left_preserves_right != right_preconditions || right_preserves_left != left_preconditions {
1512 return Err(reject("mutual precondition preservation is incomplete"));
1513 }
1514 Ok(())
1515}
1516
1517fn check_token_ownership_separability_v0(
1518 certificate: &TokenOwnershipSeparabilityCertV0,
1519 path: &[usize],
1520 rule_id: &str,
1521) -> Result<(), CertificateRejectionV0> {
1522 let reject = |reason: &str, token: Option<String>| {
1523 CertificateRejectionV0::new(
1524 side_condition_site_v0(path, rule_id),
1525 CertificateRejectionKindV0::TokenOwnershipSeparabilityRejected {
1526 reason: reason.to_owned(),
1527 token,
1528 },
1529 )
1530 };
1531 if !certificate.complete {
1532 return Err(reject("ownership census is incomplete", None));
1533 }
1534 if certificate.unattributed_emitted_token_count != 0 {
1535 return Err(reject("emitted token has no attributed owner", None));
1536 }
1537 if certificate.interface_mismatch_count != 0 {
1538 return Err(reject("emitted token disagrees with its interface", None));
1539 }
1540 if certificate.emitted_token_count != certificate.ownerships.len() {
1541 return Err(reject(
1542 "emitted token count does not match ownership rows",
1543 None,
1544 ));
1545 }
1546 let mut tokens = BTreeSet::new();
1547 for ownership in &certificate.ownerships {
1548 if ownership.emitted_token.is_empty() {
1549 return Err(reject("ownership row has an empty emitted token", None));
1550 }
1551 if !tokens.insert(ownership.emitted_token.as_str()) {
1552 return Err(reject(
1553 "ownership census repeats an emitted token",
1554 Some(ownership.emitted_token.clone()),
1555 ));
1556 }
1557 if ownership.module_paths.len() != 1 || ownership.module_paths[0].is_empty() {
1558 return Err(reject(
1559 "emitted token does not resolve to exactly one module path",
1560 Some(ownership.emitted_token.clone()),
1561 ));
1562 }
1563 }
1564 if certificate.modeled_preimage_count != certificate.ownerships.len() {
1565 return Err(reject(
1566 "modeled preimages do not form a one-to-one ownership relation",
1567 None,
1568 ));
1569 }
1570 Ok(())
1571}
1572
1573fn side_condition_site_v0(path: &[usize], rule_id: &str) -> RewriteFailureSiteV0 {
1574 RewriteFailureSiteV0 {
1575 input: RewriteCheckInputV0::Certificate,
1576 certificate_path: path.to_vec(),
1577 term_path: Vec::new(),
1578 rule_id: Some(rule_id.to_owned()),
1579 }
1580}
1581
1582fn cascade_key_from_certificate_v0(
1583 key: &CascadeWinnerKeyCertV0,
1584 path: &[usize],
1585 rule_id: &str,
1586) -> Result<CascadeKey, CertificateRejectionV0> {
1587 let layer_ordinal = match key.layer_ordinal {
1588 Some(ordinal) => Some(LayerOrdinal::new(ordinal).ok_or_else(|| {
1589 CertificateRejectionV0::new(
1590 side_condition_site_v0(path, rule_id),
1591 CertificateRejectionKindV0::InvalidLayerOrdinal { observed: ordinal },
1592 )
1593 })?),
1594 None => None,
1595 };
1596 Ok(CascadeKey::new(
1597 key.level.to_cascade_level(),
1598 normalized_layer_rank(key.layer_important, layer_ordinal),
1599 key.scope_proximity,
1600 Specificity::new(
1601 key.specificity_ids,
1602 key.specificity_classes,
1603 key.specificity_elements,
1604 ),
1605 key.source_order,
1606 ))
1607}
1608
1609fn check_cascade_winner_equality_v0(
1610 certificate: &CascadeWinnerEqualityCertV0,
1611 path: &[usize],
1612 rule_id: &str,
1613) -> Result<(), CertificateRejectionV0> {
1614 let before_key = cascade_key_from_certificate_v0(&certificate.before_key, path, rule_id)?;
1617 let after_key = cascade_key_from_certificate_v0(&certificate.after_key, path, rule_id)?;
1618 let before_tokenization =
1619 tokenize_dom_class_attribute_v0(Some(&certificate.before_class_attribute));
1620 let after_tokenization =
1621 tokenize_dom_class_attribute_v0(Some(&certificate.after_class_attribute));
1622 let (
1623 DomClassTokenizationV0::Known {
1624 word: before_word, ..
1625 },
1626 DomClassTokenizationV0::Known {
1627 word: after_word, ..
1628 },
1629 ) = (before_tokenization, after_tokenization)
1630 else {
1631 return Err(CertificateRejectionV0::new(
1632 side_condition_site_v0(path, rule_id),
1633 CertificateRejectionKindV0::CascadeTokenizationUnavailable,
1634 ));
1635 };
1636 let winner_ids_equal = certificate.before_winner_id == certificate.after_winner_id;
1637 let cascade_keys_equal = before_key == after_key;
1638 let token_support_equal = token_support_v0(&before_word) == token_support_v0(&after_word);
1639 if winner_ids_equal && cascade_keys_equal && token_support_equal {
1640 return Ok(());
1641 }
1642 Err(CertificateRejectionV0::new(
1643 side_condition_site_v0(path, rule_id),
1644 CertificateRejectionKindV0::CascadeWinnerEqualityRejected {
1645 winner_ids_equal,
1646 cascade_keys_equal,
1647 token_support_equal,
1648 },
1649 ))
1650}
1651
1652fn computed_value_environment_v0(
1653 entries: &[ComputedValueEnvironmentEntryV0],
1654 path: &[usize],
1655 rule_id: &str,
1656) -> Result<BTreeMap<String, CascadeValue>, CertificateRejectionV0> {
1657 let mut environment = BTreeMap::new();
1658 for entry in entries {
1659 if environment
1660 .insert(entry.name.clone(), entry.value.to_cascade_value())
1661 .is_some()
1662 {
1663 return Err(CertificateRejectionV0::new(
1664 side_condition_site_v0(path, rule_id),
1665 CertificateRejectionKindV0::DuplicateComputedValueEnvironmentEntry {
1666 name: entry.name.clone(),
1667 },
1668 ));
1669 }
1670 }
1671 Ok(environment)
1672}
1673
1674fn check_computed_value_equality_v0(
1675 certificate: &ComputedValueEqualityCertV0,
1676 path: &[usize],
1677 rule_id: &str,
1678) -> Result<(), CertificateRejectionV0> {
1679 let before_environment =
1682 computed_value_environment_v0(&certificate.before_environment, path, rule_id)?;
1683 let after_environment =
1684 computed_value_environment_v0(&certificate.after_environment, path, rule_id)?;
1685 let before_resolved = resolve_custom_property_env_least_fixed_point(&before_environment);
1686 let after_resolved = resolve_custom_property_env_least_fixed_point(&after_environment);
1687 let before_value = before_resolved.get(&certificate.property);
1688 let after_value = after_resolved.get(&certificate.property);
1689 if before_value.is_some() && before_value == after_value {
1690 return Ok(());
1691 }
1692 Err(CertificateRejectionV0::new(
1693 side_condition_site_v0(path, rule_id),
1694 CertificateRejectionKindV0::ComputedValueEqualityRejected {
1695 property: certificate.property.clone(),
1696 before_present: before_value.is_some(),
1697 after_present: after_value.is_some(),
1698 },
1699 ))
1700}
1701
1702fn check_source_map_trace_v0(
1703 certificate: &SourceMapTraceCertV0,
1704 path: &[usize],
1705 rule_id: &str,
1706) -> Result<(), CertificateRejectionV0> {
1707 if certificate.before_segments.is_empty() || certificate.after_segments.is_empty() {
1710 return Err(CertificateRejectionV0::new(
1711 side_condition_site_v0(path, rule_id),
1712 CertificateRejectionKindV0::EmptySourceMapTrace,
1713 ));
1714 }
1715 if certificate.before_segments.len() != certificate.after_segments.len() {
1716 return Err(CertificateRejectionV0::new(
1717 side_condition_site_v0(path, rule_id),
1718 CertificateRejectionKindV0::SourceMapTraceRejected {
1719 segment_index: 0,
1720 reason: "segment count differs".to_owned(),
1721 },
1722 ));
1723 }
1724 for (index, (before, after)) in certificate
1725 .before_segments
1726 .iter()
1727 .zip(&certificate.after_segments)
1728 .enumerate()
1729 {
1730 let ranges_valid = before.original_start <= before.original_end
1731 && before.generated_start <= before.generated_end
1732 && after.original_start <= after.original_end
1733 && after.generated_start <= after.generated_end;
1734 let source_projection_equal = before.source_path == after.source_path
1735 && before.source_digest == after.source_digest
1736 && before.original_start == after.original_start
1737 && before.original_end == after.original_end
1738 && before.pass_id == after.pass_id;
1739 if !ranges_valid || !source_projection_equal {
1740 return Err(CertificateRejectionV0::new(
1741 side_condition_site_v0(path, rule_id),
1742 CertificateRejectionKindV0::SourceMapTraceRejected {
1743 segment_index: index,
1744 reason: if ranges_valid {
1745 "source projection differs".to_owned()
1746 } else {
1747 "segment range is inverted".to_owned()
1748 },
1749 },
1750 ));
1751 }
1752 }
1753 Ok(())
1754}
1755
1756fn collect_pattern_variables(pattern: &RewritePatternV0, variables: &mut BTreeSet<String>) {
1757 let mut stack = vec![pattern];
1758 while let Some(current) = stack.pop() {
1759 match current {
1760 RewritePatternV0::Atom { .. } => {}
1761 RewritePatternV0::Variable { name } => {
1762 variables.insert(name.clone());
1763 }
1764 RewritePatternV0::Apply { operands, .. } => stack.extend(operands),
1765 }
1766 }
1767}
1768
1769fn instantiate_pattern(
1770 pattern: &RewritePatternV0,
1771 substitutions: &BTreeMap<&str, &RewriteTermV0>,
1772 nodes: &mut usize,
1773) -> Option<RewriteTermV0> {
1774 *nodes = nodes.saturating_add(1);
1775 if *nodes > REWRITE_TERM_MAX_NODES_V0 {
1776 return None;
1777 }
1778 match pattern {
1779 RewritePatternV0::Atom { value } => Some(RewriteTermV0::atom(value)),
1780 RewritePatternV0::Variable { name } => {
1781 let term = substitutions.get(name.as_str())?;
1782 let term_nodes = term_node_count_v0(term)?;
1783 *nodes = nodes.saturating_add(term_nodes.saturating_sub(1));
1784 (*nodes <= REWRITE_TERM_MAX_NODES_V0).then(|| (*term).clone())
1785 }
1786 RewritePatternV0::Apply { operator, operands } => {
1787 let mut instantiated = Vec::with_capacity(operands.len());
1788 for operand in operands {
1789 instantiated.push(instantiate_pattern(operand, substitutions, nodes)?);
1790 }
1791 Some(RewriteTermV0::apply(operator, instantiated))
1792 }
1793 }
1794}
1795
1796fn term_node_count_v0(term: &RewriteTermV0) -> Option<usize> {
1797 let mut stack = vec![term];
1798 let mut nodes = 0_usize;
1799 while let Some(current) = stack.pop() {
1800 nodes = nodes.checked_add(1)?;
1801 if nodes > REWRITE_TERM_MAX_NODES_V0 {
1802 return None;
1803 }
1804 if let RewriteTermV0::Apply { operands, .. } = current {
1805 stack.extend(operands);
1806 }
1807 }
1808 Some(nodes)
1809}
1810
1811fn first_term_mismatch_path(expected: &RewriteTermV0, observed: &RewriteTermV0) -> Vec<usize> {
1812 let mut stack = vec![(expected, observed, Vec::<usize>::new())];
1813 while let Some((left, right, path)) = stack.pop() {
1814 match (left, right) {
1815 (RewriteTermV0::Atom { value: left }, RewriteTermV0::Atom { value: right })
1816 if left == right => {}
1817 (
1818 RewriteTermV0::Apply {
1819 operator: left_operator,
1820 operands: left_operands,
1821 },
1822 RewriteTermV0::Apply {
1823 operator: right_operator,
1824 operands: right_operands,
1825 },
1826 ) if left_operator == right_operator && left_operands.len() == right_operands.len() => {
1827 for index in (0..left_operands.len()).rev() {
1828 let mut child_path = path.clone();
1829 child_path.push(index);
1830 stack.push((&left_operands[index], &right_operands[index], child_path));
1831 }
1832 }
1833 _ => return path,
1834 }
1835 }
1836 Vec::new()
1837}
1838
1839pub fn rewrite_rule_catalog_content_digest_hex_v0(catalog: &RewriteRuleCatalogV0) -> String {
1842 digest_hex_v0(&catalog_content_digest_v0(catalog))
1843}
1844
1845fn catalog_content_digest_v0(catalog: &RewriteRuleCatalogV0) -> [u8; 32] {
1846 let mut operators = catalog
1847 .operators
1848 .iter()
1849 .map(|operator| {
1850 let mut material = Vec::new();
1851 append_framed_bytes_v0(&mut material, operator.operator.as_bytes());
1852 append_framed_bytes_v0(&mut material, operator.arity.to_string().as_bytes());
1853 material
1854 })
1855 .collect::<Vec<_>>();
1856 operators.sort();
1857
1858 let mut rules = catalog
1859 .rules
1860 .iter()
1861 .map(|rule| {
1862 let mut material = Vec::new();
1863 append_framed_bytes_v0(&mut material, rule.rule_id.as_bytes());
1864 append_pattern_material_v0(&mut material, &rule.before_pattern);
1865 append_pattern_material_v0(&mut material, &rule.after_pattern);
1866 append_framed_bytes_v0(
1867 &mut material,
1868 rewrite_side_condition_kind_id_v0(rule.side_condition_kind).as_bytes(),
1869 );
1870 material
1871 })
1872 .collect::<Vec<_>>();
1873 rules.sort();
1874
1875 let mut hasher = blake3::Hasher::new();
1876 update_framed_hash_v0(&mut hasher, REWRITE_RULE_CATALOG_SCHEMA_ID_V0.as_bytes());
1877 update_framed_hash_v0(&mut hasher, catalog.schema_version.as_bytes());
1878 update_framed_hash_v0(&mut hasher, operators.len().to_string().as_bytes());
1879 for operator in operators {
1880 update_framed_hash_v0(&mut hasher, operator.as_slice());
1881 }
1882 update_framed_hash_v0(&mut hasher, rules.len().to_string().as_bytes());
1883 for rule in rules {
1884 update_framed_hash_v0(&mut hasher, rule.as_slice());
1885 }
1886 *hasher.finalize().as_bytes()
1887}
1888
1889fn append_pattern_material_v0(material: &mut Vec<u8>, pattern: &RewritePatternV0) {
1890 match pattern {
1891 RewritePatternV0::Atom { value } => {
1892 append_framed_bytes_v0(material, b"atom");
1893 append_framed_bytes_v0(material, value.as_bytes());
1894 }
1895 RewritePatternV0::Variable { name } => {
1896 append_framed_bytes_v0(material, b"variable");
1897 append_framed_bytes_v0(material, name.as_bytes());
1898 }
1899 RewritePatternV0::Apply { operator, operands } => {
1900 append_framed_bytes_v0(material, b"apply");
1901 append_framed_bytes_v0(material, operator.as_bytes());
1902 append_framed_bytes_v0(material, operands.len().to_string().as_bytes());
1903 for operand in operands {
1904 append_pattern_material_v0(material, operand);
1905 }
1906 }
1907 }
1908}
1909
1910fn rewrite_side_condition_kind_id_v0(kind: RewriteSideConditionKindV0) -> &'static str {
1911 match kind {
1912 RewriteSideConditionKindV0::NoSideCondition => "noSideCondition",
1913 RewriteSideConditionKindV0::CascadeWinnerEquality => "cascadeWinnerEquality",
1914 RewriteSideConditionKindV0::ComputedValueEquality => "computedValueEquality",
1915 RewriteSideConditionKindV0::SourceMapTrace => "sourceMapTrace",
1916 RewriteSideConditionKindV0::TokenOwnershipSeparability => "tokenOwnershipSeparability",
1917 RewriteSideConditionKindV0::TransformIndependence => "transformIndependence",
1918 }
1919}
1920
1921fn append_framed_bytes_v0(material: &mut Vec<u8>, value: &[u8]) {
1922 material.extend_from_slice(value.len().to_string().as_bytes());
1923 material.push(0);
1924 material.extend_from_slice(value);
1925 material.push(0xff);
1926}
1927
1928fn update_framed_hash_v0(hasher: &mut blake3::Hasher, value: &[u8]) {
1929 hasher.update(value.len().to_string().as_bytes());
1930 hasher.update(b"\0");
1931 hasher.update(value);
1932 hasher.update(b"\xff");
1933}
1934
1935fn term_digest_v0(term: &RewriteTermV0) -> [u8; 32] {
1936 let mut hasher = blake3::Hasher::new();
1937 let mut stack = vec![term];
1938 while let Some(current) = stack.pop() {
1939 match current {
1940 RewriteTermV0::Atom { value } => {
1941 hasher.update(b"atom\0");
1942 hasher.update(value.len().to_string().as_bytes());
1943 hasher.update(b"\0");
1944 hasher.update(value.as_bytes());
1945 }
1946 RewriteTermV0::Apply { operator, operands } => {
1947 hasher.update(b"apply\0");
1948 hasher.update(operator.len().to_string().as_bytes());
1949 hasher.update(b"\0");
1950 hasher.update(operator.as_bytes());
1951 hasher.update(b"\0");
1952 hasher.update(operands.len().to_string().as_bytes());
1953 for operand in operands.iter().rev() {
1954 stack.push(operand);
1955 }
1956 }
1957 }
1958 }
1959 *hasher.finalize().as_bytes()
1960}
1961
1962fn digest_hex_v0(digest: &[u8; 32]) -> String {
1963 digest.iter().map(|byte| format!("{byte:02x}")).collect()
1964}
1965
1966#[cfg(test)]
1967mod tests {
1968 use std::panic::{AssertUnwindSafe, catch_unwind};
1969
1970 use super::*;
1971
1972 fn selector_terms() -> (RewriteTermV0, RewriteTermV0) {
1973 let before = RewriteTermV0::apply(
1974 "selectorConcat",
1975 vec![
1976 RewriteTermV0::atom(".root"),
1977 RewriteTermV0::apply(
1978 "selectorIs",
1979 vec![RewriteTermV0::apply(
1980 "selectorList2",
1981 vec![RewriteTermV0::atom(".a"), RewriteTermV0::atom(".a")],
1982 )],
1983 ),
1984 ],
1985 );
1986 let after = RewriteTermV0::apply(
1987 "selectorConcat",
1988 vec![RewriteTermV0::atom(".root"), RewriteTermV0::atom(".a")],
1989 );
1990 (before, after)
1991 }
1992
1993 fn substitution(variable: &str, value: &str) -> RewriteSubstitutionEntryV0 {
1994 RewriteSubstitutionEntryV0 {
1995 variable: variable.to_owned(),
1996 term: RewriteTermV0::atom(value),
1997 }
1998 }
1999
2000 fn selector_certificate() -> RewriteCertificateEnvelopeV0 {
2001 let deduplicate = RewriteCertificateV0::Rewrite {
2002 rule_id: "selector-list-deduplicate-v0".to_owned(),
2003 substitution: vec![substitution("x", ".a")],
2004 side_condition: SideConditionCertV0::NoSideCondition,
2005 };
2006 let inside_is = RewriteCertificateV0::Trans {
2007 left: Box::new(RewriteCertificateV0::Cong {
2008 operator: "selectorIs".to_owned(),
2009 certificates: vec![deduplicate],
2010 }),
2011 right: Box::new(RewriteCertificateV0::Rewrite {
2012 rule_id: "selector-is-single-v0".to_owned(),
2013 substitution: vec![substitution("x", ".a")],
2014 side_condition: SideConditionCertV0::NoSideCondition,
2015 }),
2016 };
2017 RewriteCertificateEnvelopeV0 {
2018 schema_version: REWRITE_CERTIFICATE_SCHEMA_VERSION_V0.to_owned(),
2019 max_depth: 5,
2020 max_nodes: 8,
2021 certificate: RewriteCertificateV0::Cong {
2022 operator: "selectorConcat".to_owned(),
2023 certificates: vec![
2024 RewriteCertificateV0::Refl {
2025 term: RewriteTermV0::atom(".root"),
2026 },
2027 inside_is,
2028 ],
2029 },
2030 }
2031 }
2032
2033 fn cascade_winner_key() -> CascadeWinnerKeyCertV0 {
2034 CascadeWinnerKeyCertV0 {
2035 level: CascadeLevelCertV0::AuthorNormal,
2036 layer_important: false,
2037 layer_ordinal: Some(0),
2038 scope_proximity: 0,
2039 specificity_ids: 0,
2040 specificity_classes: 2,
2041 specificity_elements: 0,
2042 source_order: 3,
2043 }
2044 }
2045
2046 fn cascade_winner_certificate() -> CascadeWinnerEqualityCertV0 {
2047 CascadeWinnerEqualityCertV0 {
2048 before_winner_id: "declaration-color".to_owned(),
2049 after_winner_id: "declaration-color".to_owned(),
2050 before_key: cascade_winner_key(),
2051 after_key: cascade_winner_key(),
2052 before_class_attribute: "root a".to_owned(),
2053 after_class_attribute: "root a".to_owned(),
2054 }
2055 }
2056
2057 fn selector_certificate_with_cascade_side_condition() -> RewriteCertificateEnvelopeV0 {
2058 let mut envelope = selector_certificate();
2059 replace_side_condition(
2060 &mut envelope.certificate,
2061 &SideConditionCertV0::CascadeWinnerEquality {
2062 certificate: cascade_winner_certificate(),
2063 },
2064 );
2065 envelope
2066 }
2067
2068 fn replace_side_condition(
2069 certificate: &mut RewriteCertificateV0,
2070 side_condition: &SideConditionCertV0,
2071 ) {
2072 match certificate {
2073 RewriteCertificateV0::Refl { .. } => {}
2074 RewriteCertificateV0::Sym { certificate } => {
2075 replace_side_condition(certificate, side_condition);
2076 }
2077 RewriteCertificateV0::Trans { left, right } => {
2078 replace_side_condition(left, side_condition);
2079 replace_side_condition(right, side_condition);
2080 }
2081 RewriteCertificateV0::Cong { certificates, .. } => {
2082 for child in certificates {
2083 replace_side_condition(child, side_condition);
2084 }
2085 }
2086 RewriteCertificateV0::Rewrite {
2087 side_condition: observed,
2088 ..
2089 } => *observed = side_condition.clone(),
2090 }
2091 }
2092
2093 fn mutate_cascade_specificity(certificate: &mut RewriteCertificateV0) {
2094 match certificate {
2095 RewriteCertificateV0::Refl { .. } => {}
2096 RewriteCertificateV0::Sym { certificate } => {
2097 mutate_cascade_specificity(certificate);
2098 }
2099 RewriteCertificateV0::Trans { left, right } => {
2100 mutate_cascade_specificity(left);
2101 mutate_cascade_specificity(right);
2102 }
2103 RewriteCertificateV0::Cong { certificates, .. } => {
2104 for child in certificates {
2105 mutate_cascade_specificity(child);
2106 }
2107 }
2108 RewriteCertificateV0::Rewrite {
2109 side_condition: SideConditionCertV0::CascadeWinnerEquality { certificate },
2110 ..
2111 } => {
2112 certificate.after_key.specificity_classes =
2113 certificate.after_key.specificity_classes.saturating_add(1);
2114 }
2115 RewriteCertificateV0::Rewrite { .. } => {}
2116 }
2117 }
2118
2119 fn single_rule_catalog(
2120 rule_id: &str,
2121 before: &str,
2122 after: &str,
2123 side_condition_kind: RewriteSideConditionKindV0,
2124 ) -> RewriteRuleCatalogV0 {
2125 RewriteRuleCatalogV0 {
2126 schema_version: REWRITE_RULE_CATALOG_SCHEMA_VERSION_V0.to_owned(),
2127 operators: Vec::new(),
2128 rules: vec![RewriteRuleV0 {
2129 rule_id: rule_id.to_owned(),
2130 before_pattern: RewritePatternV0::atom(before),
2131 after_pattern: RewritePatternV0::atom(after),
2132 side_condition_kind,
2133 }],
2134 }
2135 }
2136
2137 fn single_rule_certificate(
2138 rule_id: &str,
2139 side_condition: SideConditionCertV0,
2140 ) -> RewriteCertificateEnvelopeV0 {
2141 RewriteCertificateEnvelopeV0 {
2142 schema_version: REWRITE_CERTIFICATE_SCHEMA_VERSION_V0.to_owned(),
2143 max_depth: 1,
2144 max_nodes: 1,
2145 certificate: RewriteCertificateV0::Rewrite {
2146 rule_id: rule_id.to_owned(),
2147 substitution: Vec::new(),
2148 side_condition,
2149 },
2150 }
2151 }
2152
2153 fn literal(value: &str) -> ComputedValueTermV0 {
2154 ComputedValueTermV0::Literal {
2155 value: value.to_owned(),
2156 }
2157 }
2158
2159 fn computed_value_certificate(after_value: &str) -> ComputedValueEqualityCertV0 {
2160 ComputedValueEqualityCertV0 {
2161 property: "--space".to_owned(),
2162 before_environment: vec![
2163 ComputedValueEnvironmentEntryV0 {
2164 name: "--base".to_owned(),
2165 value: literal("8px"),
2166 },
2167 ComputedValueEnvironmentEntryV0 {
2168 name: "--space".to_owned(),
2169 value: ComputedValueTermV0::Variable {
2170 name: "--base".to_owned(),
2171 fallback: None,
2172 },
2173 },
2174 ],
2175 after_environment: vec![
2176 ComputedValueEnvironmentEntryV0 {
2177 name: "--base".to_owned(),
2178 value: literal("8px"),
2179 },
2180 ComputedValueEnvironmentEntryV0 {
2181 name: "--space".to_owned(),
2182 value: literal(after_value),
2183 },
2184 ],
2185 }
2186 }
2187
2188 fn source_map_certificate() -> SourceMapTraceCertV0 {
2189 SourceMapTraceCertV0 {
2190 before_segments: vec![SourceMapTraceSegmentV0 {
2191 source_path: "fixture/selector.module.css".to_owned(),
2192 source_digest: "c6f1172b".to_owned(),
2193 original_start: 0,
2194 original_end: 15,
2195 generated_start: 0,
2196 generated_end: 15,
2197 pass_id: "selector-is-where-compression".to_owned(),
2198 }],
2199 after_segments: vec![SourceMapTraceSegmentV0 {
2200 source_path: "fixture/selector.module.css".to_owned(),
2201 source_digest: "c6f1172b".to_owned(),
2202 original_start: 0,
2203 original_end: 15,
2204 generated_start: 0,
2205 generated_end: 10,
2206 pass_id: "selector-is-where-compression".to_owned(),
2207 }],
2208 }
2209 }
2210
2211 fn check_selector(
2212 catalog: &RewriteRuleCatalogV0,
2213 certificate: &RewriteCertificateEnvelopeV0,
2214 ) -> Result<RewriteIssuanceTokenV0, CertificateRejectionV0> {
2215 let (before, after) = selector_terms();
2216 check_rewrite_certificate_v0(
2217 &before,
2218 &after,
2219 catalog,
2220 certificate,
2221 &CanonicalRewriteAssumptionsV0::default(),
2222 )
2223 }
2224
2225 #[test]
2226 fn real_selector_rewrite_accepts_cascade_winner_equality_certificate() {
2227 let result = check_selector(
2228 &selector_rewrite_rule_catalog_with_cascade_winner_equality_v0(),
2229 &selector_certificate_with_cascade_side_condition(),
2230 );
2231 assert!(result.is_ok(), "cascade cert rejected: {result:?}");
2232 let Ok(token) = result else {
2233 return;
2234 };
2235 println!(
2236 "cascadeWinnerEquality=accepted checkedRules={:?}",
2237 token.checked_rule_ids_v0()
2238 );
2239 }
2240
2241 #[test]
2242 fn specificity_perturbation_rejects_cascade_cert_without_producer_boolean_input() {
2243 let catalog = selector_rewrite_rule_catalog_with_cascade_winner_equality_v0();
2244 let mut certificate = selector_certificate_with_cascade_side_condition();
2245 mutate_cascade_specificity(&mut certificate.certificate);
2246 let producer_specificity_preserved_values = [true, false];
2247 let mut rejections = Vec::new();
2248 for producer_specificity_preserved in producer_specificity_preserved_values {
2249 let result = check_selector(&catalog, &certificate);
2250 assert!(
2251 result.is_err(),
2252 "specificity perturbation accepted with producer={producer_specificity_preserved}"
2253 );
2254 let Err(rejection) = result else {
2255 continue;
2256 };
2257 rejections.push((*rejection.rejection).clone());
2258 }
2259 assert_eq!(rejections.len(), 2);
2260 assert_eq!(rejections[0], rejections[1]);
2261 assert!(matches!(
2262 rejections[0],
2263 CertificateRejectionKindV0::CascadeWinnerEqualityRejected {
2264 cascade_keys_equal: false,
2265 ..
2266 }
2267 ));
2268 println!(
2269 "specificityPerturbation={:?} producerFieldTrueFalseSame={}",
2270 rejections[0],
2271 rejections[0] == rejections[1]
2272 );
2273 }
2274
2275 #[test]
2276 fn custom_property_rewrite_accepts_and_rejects_from_fixed_point_values() {
2277 let catalog = single_rule_catalog(
2278 "custom-property-inline-v0",
2279 "var(--base)",
2280 "8px",
2281 RewriteSideConditionKindV0::ComputedValueEquality,
2282 );
2283 let before = RewriteTermV0::atom("var(--base)");
2284 let after = RewriteTermV0::atom("8px");
2285 let accepted = single_rule_certificate(
2286 "custom-property-inline-v0",
2287 SideConditionCertV0::ComputedValueEquality {
2288 certificate: computed_value_certificate("8px"),
2289 },
2290 );
2291 let accepted_result = check_rewrite_certificate_v0(
2292 &before,
2293 &after,
2294 &catalog,
2295 &accepted,
2296 &CanonicalRewriteAssumptionsV0::default(),
2297 );
2298 assert!(
2299 accepted_result.is_ok(),
2300 "computed-value cert rejected: {accepted_result:?}"
2301 );
2302
2303 let rejected = single_rule_certificate(
2304 "custom-property-inline-v0",
2305 SideConditionCertV0::ComputedValueEquality {
2306 certificate: computed_value_certificate("9px"),
2307 },
2308 );
2309 let rejected_result = check_rewrite_certificate_v0(
2310 &before,
2311 &after,
2312 &catalog,
2313 &rejected,
2314 &CanonicalRewriteAssumptionsV0::default(),
2315 );
2316 assert!(
2317 rejected_result.is_err(),
2318 "computed-value perturbation accepted"
2319 );
2320 let Err(rejection) = rejected_result else {
2321 return;
2322 };
2323 assert!(matches!(
2324 *rejection.rejection,
2325 CertificateRejectionKindV0::ComputedValueEqualityRejected { .. }
2326 ));
2327 println!(
2328 "computedValue accepted=true perturbedRejection={:?}",
2329 rejection.rejection
2330 );
2331 }
2332
2333 #[test]
2334 fn selector_rewrite_accepts_and_rejects_from_emitted_source_map_segments() {
2335 let catalog = single_rule_catalog(
2336 "selector-trace-v0",
2337 ".root:is(.a)",
2338 ".root.a",
2339 RewriteSideConditionKindV0::SourceMapTrace,
2340 );
2341 let before = RewriteTermV0::atom(".root:is(.a)");
2342 let after = RewriteTermV0::atom(".root.a");
2343 let accepted = single_rule_certificate(
2344 "selector-trace-v0",
2345 SideConditionCertV0::SourceMapTrace {
2346 certificate: source_map_certificate(),
2347 },
2348 );
2349 let accepted_result = check_rewrite_certificate_v0(
2350 &before,
2351 &after,
2352 &catalog,
2353 &accepted,
2354 &CanonicalRewriteAssumptionsV0::default(),
2355 );
2356 assert!(
2357 accepted_result.is_ok(),
2358 "source-map cert rejected: {accepted_result:?}"
2359 );
2360
2361 let mut perturbed_trace = source_map_certificate();
2362 perturbed_trace.after_segments[0].original_start = 1;
2363 let rejected = single_rule_certificate(
2364 "selector-trace-v0",
2365 SideConditionCertV0::SourceMapTrace {
2366 certificate: perturbed_trace,
2367 },
2368 );
2369 let rejected_result = check_rewrite_certificate_v0(
2370 &before,
2371 &after,
2372 &catalog,
2373 &rejected,
2374 &CanonicalRewriteAssumptionsV0::default(),
2375 );
2376 assert!(rejected_result.is_err(), "source-map perturbation accepted");
2377 let Err(rejection) = rejected_result else {
2378 return;
2379 };
2380 assert!(matches!(
2381 *rejection.rejection,
2382 CertificateRejectionKindV0::SourceMapTraceRejected {
2383 segment_index: 0,
2384 ..
2385 }
2386 ));
2387 println!(
2388 "sourceMapTrace accepted=true perturbedRejection={:?}",
2389 rejection.rejection
2390 );
2391 }
2392
2393 #[test]
2394 fn side_condition_certificates_round_trip_through_serde() -> Result<(), serde_json::Error> {
2395 let certificates = [
2396 SideConditionCertV0::CascadeWinnerEquality {
2397 certificate: cascade_winner_certificate(),
2398 },
2399 SideConditionCertV0::ComputedValueEquality {
2400 certificate: computed_value_certificate("8px"),
2401 },
2402 SideConditionCertV0::SourceMapTrace {
2403 certificate: source_map_certificate(),
2404 },
2405 SideConditionCertV0::TokenOwnershipSeparability {
2406 certificate: TokenOwnershipSeparabilityCertV0 {
2407 complete: true,
2408 modeled_preimage_count: 1,
2409 emitted_token_count: 1,
2410 ownerships: vec![TokenOwnershipCertEntryV0 {
2411 emitted_token: "_shared_0".to_owned(),
2412 module_paths: vec!["src/one.module.css".to_owned()],
2413 }],
2414 unattributed_emitted_token_count: 0,
2415 interface_mismatch_count: 0,
2416 },
2417 },
2418 SideConditionCertV0::TransformIndependence {
2419 certificate: Box::new(TransformIndependenceCertV0 {
2420 left_pass_id: "number-compression".to_owned(),
2421 right_pass_id: "color-compression".to_owned(),
2422 observation_profile_id: "exact-emission-bytes-v0".to_owned(),
2423 profile_observers: vec!["rawBytes".to_owned()],
2424 observation_rows: vec![TransformIndependenceObservationCertRowV0 {
2425 fixture_id: "disjoint-values".to_owned(),
2426 observer: "rawBytes".to_owned(),
2427 left_then_right: ".a{color:red;margin:.5px}".to_owned(),
2428 right_then_left: ".a{color:red;margin:.5px}".to_owned(),
2429 }],
2430 left_preconditions: vec!["equivalentLiteralValue".to_owned()],
2431 right_preconditions: vec!["equivalentLiteralValue".to_owned()],
2432 left_preserves_right_preconditions: vec!["equivalentLiteralValue".to_owned()],
2433 right_preserves_left_preconditions: vec!["equivalentLiteralValue".to_owned()],
2434 disqualifying_descriptor_edges: Vec::new(),
2435 }),
2436 },
2437 ];
2438 for certificate in certificates {
2439 let encoded = serde_json::to_string(&certificate)?;
2440 let decoded = serde_json::from_str::<SideConditionCertV0>(&encoded)?;
2441 assert_eq!(decoded, certificate);
2442 }
2443 Ok(())
2444 }
2445
2446 #[test]
2447 fn token_ownership_side_condition_requires_one_owner_per_emitted_token() {
2448 let catalog = single_rule_catalog(
2449 "closed-world-ownership-admission-v0",
2450 "admissionRequested",
2451 "admissionGranted",
2452 RewriteSideConditionKindV0::TokenOwnershipSeparability,
2453 );
2454 let before = RewriteTermV0::atom("admissionRequested");
2455 let after = RewriteTermV0::atom("admissionGranted");
2456 let certificate = |module_paths: Vec<String>, modeled_preimage_count| {
2457 single_rule_certificate(
2458 "closed-world-ownership-admission-v0",
2459 SideConditionCertV0::TokenOwnershipSeparability {
2460 certificate: TokenOwnershipSeparabilityCertV0 {
2461 complete: true,
2462 modeled_preimage_count,
2463 emitted_token_count: 1,
2464 ownerships: vec![TokenOwnershipCertEntryV0 {
2465 emitted_token: "_shared_0".to_owned(),
2466 module_paths,
2467 }],
2468 unattributed_emitted_token_count: 0,
2469 interface_mismatch_count: 0,
2470 },
2471 },
2472 )
2473 };
2474 let accepted = check_rewrite_certificate_v0(
2475 &before,
2476 &after,
2477 &catalog,
2478 &certificate(vec!["src/one.module.css".to_owned()], 1),
2479 &CanonicalRewriteAssumptionsV0::default(),
2480 );
2481 assert!(accepted.is_ok(), "unique ownership rejected: {accepted:?}");
2482
2483 let rejected = check_rewrite_certificate_v0(
2484 &before,
2485 &after,
2486 &catalog,
2487 &certificate(
2488 vec![
2489 "src/one.module.css".to_owned(),
2490 "src/two.module.css".to_owned(),
2491 ],
2492 2,
2493 ),
2494 &CanonicalRewriteAssumptionsV0::default(),
2495 );
2496 assert!(rejected.is_err(), "ambiguous ownership accepted");
2497 let Err(rejection) = rejected else {
2498 return;
2499 };
2500 assert!(matches!(
2501 *rejection.rejection,
2502 CertificateRejectionKindV0::TokenOwnershipSeparabilityRejected {
2503 token: Some(ref token),
2504 ..
2505 } if token == "_shared_0"
2506 ));
2507 }
2508
2509 #[test]
2510 fn transform_independence_requires_observation_and_precondition_halves() {
2511 let catalog = single_rule_catalog(
2512 "adjacent-schedule-swap-v0",
2513 "numberThenColor",
2514 "colorThenNumber",
2515 RewriteSideConditionKindV0::TransformIndependence,
2516 );
2517 let before = RewriteTermV0::atom("numberThenColor");
2518 let after = RewriteTermV0::atom("colorThenNumber");
2519 let independence = TransformIndependenceCertV0 {
2520 left_pass_id: "number-compression".to_owned(),
2521 right_pass_id: "color-compression".to_owned(),
2522 observation_profile_id: "exact-emission-bytes-v0".to_owned(),
2523 profile_observers: vec!["rawBytes".to_owned()],
2524 observation_rows: vec![TransformIndependenceObservationCertRowV0 {
2525 fixture_id: "disjoint-values".to_owned(),
2526 observer: "rawBytes".to_owned(),
2527 left_then_right: ".a{color:red;margin:.5px}".to_owned(),
2528 right_then_left: ".a{color:red;margin:.5px}".to_owned(),
2529 }],
2530 left_preconditions: vec!["equivalentLiteralValue".to_owned()],
2531 right_preconditions: vec!["equivalentLiteralValue".to_owned()],
2532 left_preserves_right_preconditions: vec!["equivalentLiteralValue".to_owned()],
2533 right_preserves_left_preconditions: vec!["equivalentLiteralValue".to_owned()],
2534 disqualifying_descriptor_edges: Vec::new(),
2535 };
2536 let envelope = |certificate| {
2537 single_rule_certificate(
2538 "adjacent-schedule-swap-v0",
2539 SideConditionCertV0::TransformIndependence {
2540 certificate: Box::new(certificate),
2541 },
2542 )
2543 };
2544 let accepted = check_rewrite_certificate_v0(
2545 &before,
2546 &after,
2547 &catalog,
2548 &envelope(independence.clone()),
2549 &CanonicalRewriteAssumptionsV0::default(),
2550 );
2551 assert!(accepted.is_ok(), "independence cert rejected: {accepted:?}");
2552
2553 let mut dependent = independence;
2554 dependent
2555 .disqualifying_descriptor_edges
2556 .push("conflictsWith:color-mix-lowering:color-function-lowering".to_owned());
2557 let rejected = check_rewrite_certificate_v0(
2558 &before,
2559 &after,
2560 &catalog,
2561 &envelope(dependent),
2562 &CanonicalRewriteAssumptionsV0::default(),
2563 );
2564 assert!(matches!(
2565 rejected,
2566 Err(CertificateRejectionV0 {
2567 rejection,
2568 ..
2569 }) if matches!(
2570 *rejection,
2571 CertificateRejectionKindV0::TransformIndependenceRejected { .. }
2572 )
2573 ));
2574 }
2575
2576 #[test]
2577 fn real_selector_trans_cong_rewrite_chain_issues_token() {
2578 let catalog = selector_rewrite_rule_catalog_v0();
2579 let result = check_selector(&catalog, &selector_certificate());
2580 assert!(result.is_ok(), "selector certificate rejected: {result:?}");
2581 let Ok(token) = result else {
2582 return;
2583 };
2584
2585 assert_eq!(
2586 token.checked_rule_ids_v0(),
2587 ["selector-list-deduplicate-v0", "selector-is-single-v0"]
2588 );
2589 assert_ne!(token.before_digest_hex_v0(), token.after_digest_hex_v0());
2590 assert_eq!(
2591 token.catalog_schema_id_v0(),
2592 REWRITE_RULE_CATALOG_SCHEMA_ID_V0
2593 );
2594 assert!(token.matches_catalog_v0(&catalog));
2595 println!(
2596 "issued=true beforeDigest={} afterDigest={} catalogSchemaId={} catalogDigest={} checkedRules={:?}",
2597 token.before_digest_hex_v0(),
2598 token.after_digest_hex_v0(),
2599 token.catalog_schema_id_v0(),
2600 token.catalog_content_digest_hex_v0(),
2601 token.checked_rule_ids_v0()
2602 );
2603 }
2604
2605 #[test]
2606 fn catalog_digest_is_order_independent_and_content_sensitive() {
2607 let catalog = selector_rewrite_rule_catalog_v0();
2608 let mut permuted = catalog.clone();
2609 permuted.operators.reverse();
2610 permuted.rules.reverse();
2611 assert_eq!(
2612 rewrite_rule_catalog_content_digest_hex_v0(&catalog),
2613 rewrite_rule_catalog_content_digest_hex_v0(&permuted)
2614 );
2615
2616 let mut spoofed = catalog.clone();
2617 spoofed.rules[1].before_pattern = RewritePatternV0::variable("anything");
2618 spoofed.rules[1].after_pattern = RewritePatternV0::variable("whatever");
2619 assert_ne!(
2620 rewrite_rule_catalog_content_digest_hex_v0(&catalog),
2621 rewrite_rule_catalog_content_digest_hex_v0(&spoofed)
2622 );
2623 }
2624
2625 #[test]
2626 fn one_token_substitution_mutation_names_the_after_subterm() {
2627 let mut certificate = selector_certificate();
2628 let RewriteCertificateV0::Cong { certificates, .. } = &mut certificate.certificate else {
2629 unreachable!("fixture root is congruence")
2630 };
2631 let RewriteCertificateV0::Trans { right, .. } = &mut certificates[1] else {
2632 unreachable!("fixture inner node is transitivity")
2633 };
2634 let RewriteCertificateV0::Rewrite { substitution, .. } = right.as_mut() else {
2635 unreachable!("fixture right node is rewrite")
2636 };
2637 substitution[0].term = RewriteTermV0::atom(".b");
2638
2639 let result = check_selector(&selector_rewrite_rule_catalog_v0(), &certificate);
2640 assert!(result.is_err(), "one-token substitution mutation accepted");
2641 let Err(rejection) = result else {
2642 return;
2643 };
2644 assert_eq!(
2645 *rejection.rejection,
2646 CertificateRejectionKindV0::TransitiveMiddleMismatch
2647 );
2648 assert_eq!(rejection.site.certificate_path, vec![1]);
2649 assert_eq!(rejection.site.term_path, vec![0]);
2650 println!(
2651 "rejection={:?} certificatePath={:?} termPath={:?}",
2652 rejection.rejection, rejection.site.certificate_path, rejection.site.term_path
2653 );
2654 }
2655
2656 #[test]
2657 fn favourable_producer_fields_cannot_replace_a_missing_certificate() {
2658 let producer_specificity_preserved = true;
2659 let producer_computed_value_preserved = true;
2660 let producer_provenance_preserved = true;
2661 assert!(
2662 producer_specificity_preserved
2663 && producer_computed_value_preserved
2664 && producer_provenance_preserved
2665 );
2666 let (before, after) = selector_terms();
2667 let result = check_optional_rewrite_certificate_v0(
2668 &before,
2669 &after,
2670 &selector_rewrite_rule_catalog_v0(),
2671 None,
2672 &CanonicalRewriteAssumptionsV0::default(),
2673 );
2674 assert!(result.is_err(), "missing certificate minted a token");
2675 let Err(rejection) = result else {
2676 return;
2677 };
2678 assert_eq!(
2679 *rejection.rejection,
2680 CertificateRejectionKindV0::MissingCertificate
2681 );
2682 println!(
2683 "producerFields=true,true,true rejection={:?}",
2684 rejection.rejection
2685 );
2686 }
2687
2688 #[test]
2689 fn adversarial_corpus_returns_six_typed_rejections_without_panicking() {
2690 let catalog = selector_rewrite_rule_catalog_v0();
2691 let (before, after) = selector_terms();
2692
2693 let mut unknown_rule = selector_certificate();
2694 let RewriteCertificateV0::Cong { certificates, .. } = &mut unknown_rule.certificate else {
2695 unreachable!("fixture root is congruence")
2696 };
2697 let RewriteCertificateV0::Trans { right, .. } = &mut certificates[1] else {
2698 unreachable!("fixture inner node is transitivity")
2699 };
2700 let RewriteCertificateV0::Rewrite { rule_id, .. } = right.as_mut() else {
2701 unreachable!("fixture right node is rewrite")
2702 };
2703 *rule_id = "unknown-rule-v0".to_owned();
2704
2705 let mut arity = selector_certificate();
2706 let RewriteCertificateV0::Cong { certificates, .. } = &mut arity.certificate else {
2707 unreachable!("fixture root is congruence")
2708 };
2709 certificates.pop();
2710
2711 let missing_substitution = RewriteCertificateEnvelopeV0 {
2712 schema_version: REWRITE_CERTIFICATE_SCHEMA_VERSION_V0.to_owned(),
2713 max_depth: 1,
2714 max_nodes: 1,
2715 certificate: RewriteCertificateV0::Rewrite {
2716 rule_id: "selector-is-single-v0".to_owned(),
2717 substitution: Vec::new(),
2718 side_condition: SideConditionCertV0::NoSideCondition,
2719 },
2720 };
2721
2722 let mut depth = selector_certificate();
2723 depth.max_depth = 2;
2724
2725 let trans_mismatch = RewriteCertificateEnvelopeV0 {
2726 schema_version: REWRITE_CERTIFICATE_SCHEMA_VERSION_V0.to_owned(),
2727 max_depth: 2,
2728 max_nodes: 3,
2729 certificate: RewriteCertificateV0::Trans {
2730 left: Box::new(RewriteCertificateV0::Refl {
2731 term: RewriteTermV0::atom(".a"),
2732 }),
2733 right: Box::new(RewriteCertificateV0::Refl {
2734 term: RewriteTermV0::atom(".b"),
2735 }),
2736 },
2737 };
2738
2739 let typed_cases = [
2740 ("unknownRule", unknown_rule, "unknownRule"),
2741 ("congruenceArity", arity, "operatorArityMismatch"),
2742 (
2743 "missingSubstitution",
2744 missing_substitution,
2745 "missingSubstitutionVariable",
2746 ),
2747 ("declaredDepth", depth, "declaredBoundExceeded"),
2748 (
2749 "transitiveMiddle",
2750 trans_mismatch,
2751 "transitiveMiddleMismatch",
2752 ),
2753 ];
2754 let mut observed = Vec::new();
2755 for (name, certificate, expected_kind) in typed_cases {
2756 let outcome = catch_unwind(AssertUnwindSafe(|| {
2757 check_rewrite_certificate_v0(
2758 &before,
2759 &after,
2760 &catalog,
2761 &certificate,
2762 &CanonicalRewriteAssumptionsV0::default(),
2763 )
2764 }));
2765 assert!(outcome.is_ok(), "{name} panicked");
2766 let Some(result) = outcome.ok() else {
2767 continue;
2768 };
2769 assert!(result.is_err(), "{name} did not produce a typed rejection");
2770 let Err(rejection) = result else {
2771 continue;
2772 };
2773 let encoded = serde_json::to_value(&rejection);
2774 assert!(encoded.is_ok(), "{name} rejection did not serialize");
2775 let Ok(value) = encoded else {
2776 continue;
2777 };
2778 assert_eq!(value["rejection"]["kind"], expected_kind);
2779 assert_eq!(value["site"]["input"], "certificate");
2780 println!(
2781 "case={name} rejection={} site={}",
2782 value["rejection"], value["site"]
2783 );
2784 observed.push(name);
2785 }
2786
2787 let malformed = catch_unwind(AssertUnwindSafe(|| {
2788 check_serialized_rewrite_certificate_v0(
2789 &before,
2790 &after,
2791 &catalog,
2792 r#"{"schemaVersion":"0","certificate":{"kind":"trans""#,
2793 &CanonicalRewriteAssumptionsV0::default(),
2794 )
2795 }));
2796 assert!(malformed.is_ok(), "malformed serde input panicked");
2797 let Some(result) = malformed.ok() else {
2798 return;
2799 };
2800 assert!(result.is_err(), "malformed serde input was not rejected");
2801 let Err(rejection) = result else {
2802 return;
2803 };
2804 assert!(matches!(
2805 *rejection.rejection,
2806 CertificateRejectionKindV0::MalformedCertificate { .. }
2807 ));
2808 assert_eq!(
2809 rejection.site.input,
2810 RewriteCheckInputV0::SerializedCertificate
2811 );
2812 println!(
2813 "case=malformedSerde rejection={:?} site={:?}",
2814 rejection.rejection, rejection.site
2815 );
2816 observed.push("malformedSerde");
2817 assert_eq!(observed.len(), 6);
2818 }
2819
2820 #[test]
2821 fn fixed_seed_input_order_permutations_issue_identical_tokens() {
2822 let baseline_catalog = selector_rewrite_rule_catalog_v0();
2823 let baseline_certificate = selector_certificate();
2824 let baseline_result = check_selector(&baseline_catalog, &baseline_certificate);
2825 assert!(
2826 baseline_result.is_ok(),
2827 "baseline rejected: {baseline_result:?}"
2828 );
2829 let Ok(baseline) = baseline_result else {
2830 return;
2831 };
2832 let mut state = 0x6a09_e667_f3bc_c909_u64;
2833
2834 for _ in 0..32 {
2835 let mut catalog = selector_rewrite_rule_catalog_v0();
2836 seeded_shuffle(&mut catalog.operators, &mut state);
2837 seeded_shuffle(&mut catalog.rules, &mut state);
2838 let mut certificate = selector_certificate();
2839 reverse_substitution_order(&mut certificate.certificate);
2840 let observed_result = check_selector(&catalog, &certificate);
2841 assert!(
2842 observed_result.is_ok(),
2843 "permutation rejected: {observed_result:?}"
2844 );
2845 let Ok(observed) = observed_result else {
2846 continue;
2847 };
2848 assert_eq!(observed, baseline);
2849 }
2850 }
2851
2852 fn reverse_substitution_order(certificate: &mut RewriteCertificateV0) {
2853 match certificate {
2854 RewriteCertificateV0::Refl { .. } => {}
2855 RewriteCertificateV0::Sym { certificate } => {
2856 reverse_substitution_order(certificate);
2857 }
2858 RewriteCertificateV0::Trans { left, right } => {
2859 reverse_substitution_order(left);
2860 reverse_substitution_order(right);
2861 }
2862 RewriteCertificateV0::Cong { certificates, .. } => {
2863 for child in certificates {
2864 reverse_substitution_order(child);
2865 }
2866 }
2867 RewriteCertificateV0::Rewrite { substitution, .. } => substitution.reverse(),
2868 }
2869 }
2870
2871 fn seeded_shuffle<T>(values: &mut [T], state: &mut u64) {
2872 for index in (1..values.len()).rev() {
2873 *state = state
2874 .wrapping_mul(6_364_136_223_846_793_005)
2875 .wrapping_add(1_442_695_040_888_963_407);
2876 let target = (*state as usize) % (index + 1);
2877 values.swap(index, target);
2878 }
2879 }
2880
2881 #[test]
2882 fn grammar_round_trips_through_serde() -> Result<(), serde_json::Error> {
2883 let certificate = selector_certificate();
2884 let encoded = serde_json::to_string(&certificate)?;
2885 let decoded = serde_json::from_str::<RewriteCertificateEnvelopeV0>(&encoded)?;
2886 assert_eq!(decoded, certificate);
2887 Ok(())
2888 }
2889}