From 80fe486e6d96f7a4ede6316413a83840488a3a0f Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 11:35:55 +0100 Subject: [PATCH 01/19] Add scrypto_decode_with_nice_error --- radix-common/src/data/manifest/definitions.rs | 33 ++++++++++++++++++ radix-common/src/data/scrypto/definitions.rs | 34 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/radix-common/src/data/manifest/definitions.rs b/radix-common/src/data/manifest/definitions.rs index cc21960c8f4..a7bbb80ff6a 100644 --- a/radix-common/src/data/manifest/definitions.rs +++ b/radix-common/src/data/manifest/definitions.rs @@ -103,6 +103,39 @@ pub fn manifest_decode(buf: &[u8]) -> Result manifest_decode_with_depth_limit(buf, MANIFEST_SBOR_V1_MAX_DEPTH) } +/// Decodes a data structure from a byte array. +/// +/// If an error occurs, the type's schema is exported and used to give a better error message. +/// +/// NOTE: +/// * The error path runs very slowly. This should only be used where errors are NOT expected. +pub fn manifest_decode_with_nice_error( + buf: &[u8], +) -> Result { + match manifest_decode(buf) { + Ok(value) => Ok(value), + Err(err) => { + let (local_type_id, schema) = + generate_full_schema_from_single_type::(); + let schema = schema.as_unique_version(); + match validate_payload_against_schema::( + buf, + schema, + local_type_id, + &(), + SCRYPTO_SBOR_V1_MAX_DEPTH, + ) { + Ok(()) => { + // This case is unexpected. We got a decode error, but it's valid against the schema. + // In this case, let's just debug-print the DecodeError. + Err(format!("{err:?}")) + } + Err(err) => Err(err.error_message(schema)), + } + } + } +} + pub fn manifest_decode_with_depth_limit( buf: &[u8], depth_limit: usize, diff --git a/radix-common/src/data/scrypto/definitions.rs b/radix-common/src/data/scrypto/definitions.rs index 547e56da14b..35a494e73cd 100644 --- a/radix-common/src/data/scrypto/definitions.rs +++ b/radix-common/src/data/scrypto/definitions.rs @@ -63,6 +63,40 @@ pub fn scrypto_decode(buf: &[u8]) -> Result { scrypto_decode_with_depth_limit(buf, SCRYPTO_SBOR_V1_MAX_DEPTH) } +/// Decodes a data structure from a byte array. +/// +/// If an error occurs, the type's schema is exported and used to give a better error message. +/// +/// NOTE: +/// * The error path runs very slowly. This should only be used where errors are NOT expected. +/// * This should not be used in Scrypto, as it will pull in the schema aggregation code which is large. +pub fn scrypto_decode_with_nice_error( + buf: &[u8], +) -> Result { + match scrypto_decode(buf) { + Ok(value) => Ok(value), + Err(err) => { + let (local_type_id, schema) = + generate_full_schema_from_single_type::(); + let schema = schema.as_unique_version(); + match validate_payload_against_schema::( + buf, + schema, + local_type_id, + &(), + SCRYPTO_SBOR_V1_MAX_DEPTH, + ) { + Ok(()) => { + // This case is unexpected. We got a decode error, but it's valid against the schema. + // In this case, let's just debug-print the DecodeError. + Err(format!("{err:?}")) + } + Err(err) => Err(err.error_message(schema)), + } + } + } +} + pub fn scrypto_decode_with_depth_limit( buf: &[u8], depth_limit: usize, From 0f514c88e1c89baaae1a01296ae7f1cae225a36a Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 11:36:22 +0100 Subject: [PATCH 02/19] fix: Minor fix to describe.rs --- sbor-derive-common/src/describe.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sbor-derive-common/src/describe.rs b/sbor-derive-common/src/describe.rs index 904427808e5..8a9e37ebddc 100644 --- a/sbor-derive-common/src/describe.rs +++ b/sbor-derive-common/src/describe.rs @@ -178,7 +178,7 @@ fn handle_normal_describe( // Note that it might seem possible to still hit issues with infinite recursion, if you pass a type as its own generic type parameter. // EG (via a type alias B = A), but these types won't come up in practice because they require an infinite generic depth // which the compiler will throw out for other reasons. - &[#(<#child_types>::TYPE_ID,)*], + &[#(<#child_types as sbor::Describe<#custom_type_kind_generic>>::TYPE_ID,)*], &#code_hash ) }; @@ -555,7 +555,7 @@ mod tests { { const TYPE_ID: sbor::RustTypeId = sbor::RustTypeId::novel_with_code( "Test", - &[::TYPE_ID, ::TYPE_ID,], + &[>::TYPE_ID, >::TYPE_ID,], &#code_hash ); From aadc39dec6d278c3e70f2eba53be4dd09a8307b6 Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 11:49:35 +0100 Subject: [PATCH 03/19] refactor: Improvements to traverser and errors --- .../src/data/manifest/custom_traversal.rs | 2 +- .../src/data/scrypto/custom_schema.rs | 91 ++- .../src/data/scrypto/custom_traversal.rs | 2 +- radix-common/src/macros.rs | 40 - .../src/types/indexed_value.rs | 6 +- .../src/model/preparation/references.rs | 10 +- sbor/src/basic.rs | 29 +- .../payload_validation/payload_validator.rs | 33 +- .../representations/display/nested_string.rs | 2 +- .../display/rustlike_string.rs | 2 +- sbor/src/schema/custom_traits.rs | 13 +- sbor/src/schema/schema.rs | 30 + .../type_validation_validation.rs | 2 +- sbor/src/schema/type_data/type_kind.rs | 89 ++- sbor/src/schema/type_data/type_metadata.rs | 18 + sbor/src/schema/type_data/type_validation.rs | 122 +-- sbor/src/traversal/mod.rs | 2 + sbor/src/traversal/path_formatting.rs | 195 +++++ sbor/src/traversal/typed/events.rs | 59 +- sbor/src/traversal/typed/full_location.rs | 220 +++--- sbor/src/traversal/typed/typed_traverser.rs | 59 +- sbor/src/traversal/untyped/events.rs | 46 +- sbor/src/traversal/untyped/traverser.rs | 706 ++++++++++-------- 23 files changed, 1166 insertions(+), 612 deletions(-) create mode 100644 sbor/src/traversal/path_formatting.rs diff --git a/radix-common/src/data/manifest/custom_traversal.rs b/radix-common/src/data/manifest/custom_traversal.rs index 1e40c63d104..75fbb18660d 100644 --- a/radix-common/src/data/manifest/custom_traversal.rs +++ b/radix-common/src/data/manifest/custom_traversal.rs @@ -18,7 +18,7 @@ impl CustomTraversal for ManifestCustomTraversal { type CustomValueKind = ManifestCustomValueKind; type CustomTerminalValueRef<'de> = ManifestCustomTerminalValueRef; - fn decode_custom_value_body<'de, R>( + fn read_custom_value_body<'de, R>( custom_value_kind: Self::CustomValueKind, reader: &mut R, ) -> Result, DecodeError> diff --git a/radix-common/src/data/scrypto/custom_schema.rs b/radix-common/src/data/scrypto/custom_schema.rs index d0533bb84b7..2f55e6ce72a 100644 --- a/radix-common/src/data/scrypto/custom_schema.rs +++ b/radix-common/src/data/scrypto/custom_schema.rs @@ -15,6 +15,15 @@ pub enum ScryptoCustomTypeKind { NonFungibleLocalId, } +#[derive(Debug, Copy, Clone, PartialEq, Eq, ManifestSbor, ScryptoSbor)] +pub enum ScryptoCustomTypeKindLabel { + Reference, + Own, + Decimal, + PreciseDecimal, + NonFungibleLocalId, +} + #[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoSbor)] pub enum ScryptoCustomTypeValidation { Reference(ReferenceValidation), @@ -32,6 +41,43 @@ pub enum ReferenceValidation { IsInternalTyped(Option, String), } +impl ReferenceValidation { + fn compare(base: &Self, compared: &Self) -> ValidationChange { + match (base, compared) { + (base, compared) if base == compared => ValidationChange::Unchanged, + (ReferenceValidation::IsGlobal, compared) if compared.requires_global() => ValidationChange::Strengthened, + (base, ReferenceValidation::IsGlobal) if base.requires_global() => ValidationChange::Weakened, + (ReferenceValidation::IsInternal, compared) if compared.requires_internal() => ValidationChange::Strengthened, + (base, ReferenceValidation::IsInternal) if base.requires_internal() => ValidationChange::Weakened, + (_, _) => ValidationChange::Incomparable, + } + } + + fn requires_global(&self) -> bool { + match self { + ReferenceValidation::IsGlobal => true, + ReferenceValidation::IsGlobalPackage => true, + ReferenceValidation::IsGlobalComponent => true, + ReferenceValidation::IsGlobalResourceManager => true, + ReferenceValidation::IsGlobalTyped(_, _) => true, + ReferenceValidation::IsInternal => false, + ReferenceValidation::IsInternalTyped(_, _) => false, + } + } + + fn requires_internal(&self) -> bool { + match self { + ReferenceValidation::IsGlobal => false, + ReferenceValidation::IsGlobalPackage => false, + ReferenceValidation::IsGlobalComponent => false, + ReferenceValidation::IsGlobalResourceManager => false, + ReferenceValidation::IsGlobalTyped(_, _) => false, + ReferenceValidation::IsInternal => true, + ReferenceValidation::IsInternalTyped(_, _) => true, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoSbor)] pub enum OwnValidation { IsBucket, @@ -43,6 +89,16 @@ pub enum OwnValidation { } impl OwnValidation { + fn compare(base: &Self, compared: &Self) -> ValidationChange { + // This is strictly a little hard - if we get issues, we may wish to match + // IsTypedObject(ResourcePackage, "FungibleBucket") as a strengthening of IsBucket and so on. + if base == compared { + ValidationChange::Unchanged + } else { + ValidationChange::Incomparable + } + } + pub fn could_match_manifest_bucket(&self) -> bool { match self { OwnValidation::IsBucket => true, @@ -95,9 +151,41 @@ impl ReferenceValidation { impl CustomTypeKind for ScryptoCustomTypeKind { type CustomTypeValidation = ScryptoCustomTypeValidation; + type CustomTypeKindLabel = ScryptoCustomTypeKindLabel; + + fn label(&self) -> Self::CustomTypeKindLabel { + match self { + ScryptoCustomTypeKind::Reference => ScryptoCustomTypeKindLabel::Reference, + ScryptoCustomTypeKind::Own => ScryptoCustomTypeKindLabel::Own, + ScryptoCustomTypeKind::Decimal => ScryptoCustomTypeKindLabel::Decimal, + ScryptoCustomTypeKind::PreciseDecimal => ScryptoCustomTypeKindLabel::PreciseDecimal, + ScryptoCustomTypeKind::NonFungibleLocalId => ScryptoCustomTypeKindLabel::NonFungibleLocalId, + } + } +} + +impl CustomTypeKindLabel for ScryptoCustomTypeKindLabel { + fn name(&self) -> &'static str { + match self { + ScryptoCustomTypeKindLabel::Reference => "Reference", + ScryptoCustomTypeKindLabel::Own => "Own", + ScryptoCustomTypeKindLabel::Decimal => "Decimal", + ScryptoCustomTypeKindLabel::PreciseDecimal => "PreciseDecimal", + ScryptoCustomTypeKindLabel::NonFungibleLocalId => "NonFungibleLocalId", + } + } } -impl CustomTypeValidation for ScryptoCustomTypeValidation {} +impl CustomTypeValidation for ScryptoCustomTypeValidation { + fn compare(base: &Self, compared: &Self) -> ValidationChange { + match (base, compared) { + (ScryptoCustomTypeValidation::Reference(base), ScryptoCustomTypeValidation::Reference(compared)) => ReferenceValidation::compare(base, compared), + (ScryptoCustomTypeValidation::Reference(_), ScryptoCustomTypeValidation::Own(_)) => ValidationChange::Incomparable, + (ScryptoCustomTypeValidation::Own(_), ScryptoCustomTypeValidation::Reference(_)) => ValidationChange::Incomparable, + (ScryptoCustomTypeValidation::Own(base), ScryptoCustomTypeValidation::Own(compared)) => OwnValidation::compare(base, compared), + } + } +} #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub struct ScryptoCustomSchema {} @@ -110,6 +198,7 @@ lazy_static::lazy_static! { impl CustomSchema for ScryptoCustomSchema { type CustomTypeKind = ScryptoCustomTypeKind; + type CustomTypeKindLabel = ScryptoCustomTypeKindLabel; type CustomTypeValidation = ScryptoCustomTypeValidation; fn linearize_type_kind( diff --git a/radix-common/src/data/scrypto/custom_traversal.rs b/radix-common/src/data/scrypto/custom_traversal.rs index 7b64a14af6e..f9228a4f2fc 100644 --- a/radix-common/src/data/scrypto/custom_traversal.rs +++ b/radix-common/src/data/scrypto/custom_traversal.rs @@ -18,7 +18,7 @@ impl CustomTraversal for ScryptoCustomTraversal { type CustomValueKind = ScryptoCustomValueKind; type CustomTerminalValueRef<'de> = ScryptoCustomTerminalValueRef; - fn decode_custom_value_body<'de, R>( + fn read_custom_value_body<'de, R>( custom_value_kind: Self::CustomValueKind, reader: &mut R, ) -> Result, DecodeError> diff --git a/radix-common/src/macros.rs b/radix-common/src/macros.rs index 866a1235a41..40fe781cd7f 100644 --- a/radix-common/src/macros.rs +++ b/radix-common/src/macros.rs @@ -60,46 +60,6 @@ macro_rules! well_known_scrypto_custom_type { }; } -#[macro_export] -macro_rules! schemaless_scrypto_custom_type { - // without describe - ($t:ty, $value_kind:expr, $size: expr) => { - impl sbor::Categorize<$crate::data::scrypto::ScryptoCustomValueKind> for $t { - #[inline] - fn value_kind() -> sbor::ValueKind<$crate::data::scrypto::ScryptoCustomValueKind> { - sbor::ValueKind::Custom($value_kind) - } - } - - impl> - sbor::Encode<$crate::data::scrypto::ScryptoCustomValueKind, E> for $t - { - #[inline] - fn encode_value_kind(&self, encoder: &mut E) -> Result<(), sbor::EncodeError> { - encoder.write_value_kind(Self::value_kind()) - } - - #[inline] - fn encode_body(&self, encoder: &mut E) -> Result<(), sbor::EncodeError> { - encoder.write_slice(&self.to_vec()) - } - } - - impl> - sbor::Decode<$crate::data::scrypto::ScryptoCustomValueKind, D> for $t - { - fn decode_body_with_value_kind( - decoder: &mut D, - value_kind: sbor::ValueKind<$crate::data::scrypto::ScryptoCustomValueKind>, - ) -> Result { - decoder.check_preloaded_value_kind(value_kind, Self::value_kind())?; - let slice = decoder.read_slice($size)?; - Self::try_from(slice).map_err(|_| sbor::DecodeError::InvalidCustomValue) - } - } - }; -} - #[macro_export] macro_rules! manifest_type { // without describe diff --git a/radix-engine-interface/src/types/indexed_value.rs b/radix-engine-interface/src/types/indexed_value.rs index a2b20f854ec..4a4e55842a5 100644 --- a/radix-engine-interface/src/types/indexed_value.rs +++ b/radix-engine-interface/src/types/indexed_value.rs @@ -21,9 +21,11 @@ impl IndexedScryptoValue { fn new(bytes: Vec) -> Result { let mut traverser = ScryptoTraverser::new( &bytes, - SCRYPTO_SBOR_V1_MAX_DEPTH, ExpectedStart::PayloadPrefix(SCRYPTO_SBOR_V1_PAYLOAD_PREFIX), - true, + VecTraverserConfig { + max_depth: SCRYPTO_SBOR_V1_MAX_DEPTH, + check_exact_end: true, + } ); let mut references = Vec::::new(); let mut owned_nodes = Vec::::new(); diff --git a/radix-transactions/src/model/preparation/references.rs b/radix-transactions/src/model/preparation/references.rs index ca33b0576dd..b2369e03bfe 100644 --- a/radix-transactions/src/model/preparation/references.rs +++ b/radix-transactions/src/model/preparation/references.rs @@ -6,8 +6,14 @@ pub fn extract_references( expected_start: ExpectedStart, ) -> IndexSet { let mut references = index_set_new(); - let mut traverser = - ManifestTraverser::new(&encoded, MANIFEST_SBOR_V1_MAX_DEPTH, expected_start, true); + let mut traverser = ManifestTraverser::new( + &encoded, + expected_start, + VecTraverserConfig { + max_depth: MANIFEST_SBOR_V1_MAX_DEPTH, + check_exact_end: true, + }, + ); loop { let event = traverser.next_event(); match event.event { diff --git a/sbor/src/basic.rs b/sbor/src/basic.rs index 486836e1b38..b0d17531099 100644 --- a/sbor/src/basic.rs +++ b/sbor/src/basic.rs @@ -142,7 +142,7 @@ impl CustomTraversal for NoCustomTraversal { type CustomValueKind = NoCustomValueKind; type CustomTerminalValueRef<'de> = NoCustomTerminalValueRef; - fn decode_custom_value_body<'de, R>( + fn read_custom_value_body<'de, R>( _custom_value_kind: Self::CustomValueKind, _reader: &mut R, ) -> Result, DecodeError> @@ -157,22 +157,42 @@ impl CustomTraversal for NoCustomTraversal { pub fn basic_payload_traverser<'b>(buf: &'b [u8]) -> BasicTraverser<'b> { BasicTraverser::new( buf, - BASIC_SBOR_V1_MAX_DEPTH, ExpectedStart::PayloadPrefix(BASIC_SBOR_V1_PAYLOAD_PREFIX), - true, + VecTraverserConfig { + max_depth: BASIC_SBOR_V1_MAX_DEPTH, + check_exact_end: true, + }, ) } #[derive(Debug, Clone, PartialEq, Eq, Sbor)] pub enum NoCustomTypeKind {} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Sbor)] +pub enum NoCustomTypeKindLabel {} + #[derive(Debug, Clone, PartialEq, Eq, Sbor)] pub enum NoCustomTypeValidation {} -impl CustomTypeValidation for NoCustomTypeValidation {} +impl CustomTypeValidation for NoCustomTypeValidation { + fn compare(_base: &Self, _compared: &Self) -> ValidationChange { + unreachable!("No custom validations exist") + } +} impl CustomTypeKind for NoCustomTypeKind { type CustomTypeValidation = NoCustomTypeValidation; + type CustomTypeKindLabel = NoCustomTypeKindLabel; + + fn label(&self) -> Self::CustomTypeKindLabel { + unreachable!("No custom type kinds exist") + } +} + +impl CustomTypeKindLabel for NoCustomTypeKindLabel { + fn name(&self) -> &'static str { + unreachable!("No custom type kinds exist") + } } lazy_static::lazy_static! { @@ -186,6 +206,7 @@ pub enum NoCustomSchema {} impl CustomSchema for NoCustomSchema { type CustomTypeKind = NoCustomTypeKind; + type CustomTypeKindLabel = NoCustomTypeKindLabel; type CustomTypeValidation = NoCustomTypeValidation; fn linearize_type_kind( diff --git a/sbor/src/payload_validation/payload_validator.rs b/sbor/src/payload_validation/payload_validator.rs index d6b5743b152..018c5c4da69 100644 --- a/sbor/src/payload_validation/payload_validator.rs +++ b/sbor/src/payload_validation/payload_validator.rs @@ -598,6 +598,15 @@ mod tests { payload: Vec, expected_path: &str, expected_cause: &str, + ) { + check_location_path_with_depth_limit::(payload, 64, expected_path, expected_cause) + } + + fn check_location_path_with_depth_limit( + payload: Vec, + depth_limit: usize, + expected_path: &str, + expected_cause: &str, ) { let (type_id, schema) = generate_full_schema_from_single_type::(); @@ -606,7 +615,7 @@ mod tests { schema.v1(), type_id, &mut (), - 64, + depth_limit, ) else { panic!("Validation did not error with too short a payload"); }; @@ -616,6 +625,15 @@ mod tests { #[test] pub fn mismatched_type_full_location_path_is_readable() { + check_location_path_with_depth_limit::( + basic_encode(&MyStruct2 { + field1: 3, + field2: MyStruct2Inner { inner1: 1, inner2: 2 }, + }).unwrap(), + 2, + "MyStruct2.[1|field2]->MyStruct2Inner.[0|inner1]", + "DecodeError(MaxDepthExceeded(2))", + ); check_location_path::( basic_encode(&BasicValue::Tuple { fields: vec![ @@ -642,6 +660,17 @@ mod tests { "MyStruct2.[1|field2]->MyStruct2Inner.[0|inner1]", "{ expected_type: U16, found: U8 }", ); + check_location_path::( + basic_encode(&BasicValue::Tuple { + fields: vec![ + BasicValue::U16 { value: 1 }, + BasicValue::Array { element_value_kind: ValueKind::U8, elements: vec![], }, + ], + }) + .unwrap(), + "MyStruct2.[1|field2]", + "{ expected_type: Tuple, found: Array }", + ); check_location_path::( basic_encode(&BasicValue::Tuple { fields: vec![ @@ -696,7 +725,7 @@ mod tests { payload.pop(); // remove last byte check_location_path::>( payload, - "Array", + "Array.[2]", "DecodeError(BufferUnderflow { required: 3, remaining: 2 })", ); diff --git a/sbor/src/representations/display/nested_string.rs b/sbor/src/representations/display/nested_string.rs index cde0e137cdf..10a969bbc1a 100644 --- a/sbor/src/representations/display/nested_string.rs +++ b/sbor/src/representations/display/nested_string.rs @@ -96,7 +96,7 @@ fn format_value_tree( let typed_event = traverser.next_event(); match typed_event.event { ContainerStart(type_id, container_header) => { - let parent_depth = typed_event.location.typed_ancestor_path.len(); + let parent_depth = typed_event.location.typed_container_path.len(); match container_header { ContainerHeader::Tuple(header) => { format_tuple(f, traverser, context, type_id, header, parent_depth) diff --git a/sbor/src/representations/display/rustlike_string.rs b/sbor/src/representations/display/rustlike_string.rs index 7a438b77ad9..169e0f9c0a9 100644 --- a/sbor/src/representations/display/rustlike_string.rs +++ b/sbor/src/representations/display/rustlike_string.rs @@ -89,7 +89,7 @@ fn format_value_tree( let typed_event = traverser.next_event(); match typed_event.event { ContainerStart(type_id, container_header) => { - let parent_depth = typed_event.location.typed_ancestor_path.len(); + let parent_depth = typed_event.location.typed_container_path.len(); match container_header { ContainerHeader::Tuple(header) => { format_tuple(f, traverser, context, type_id, header, parent_depth) diff --git a/sbor/src/schema/custom_traits.rs b/sbor/src/schema/custom_traits.rs index 09d41a6d22b..3b3c31c7e9a 100644 --- a/sbor/src/schema/custom_traits.rs +++ b/sbor/src/schema/custom_traits.rs @@ -4,16 +4,27 @@ use crate::*; pub trait CustomTypeKind: Debug + Clone + PartialEq + Eq { type CustomTypeValidation: CustomTypeValidation; + type CustomTypeKindLabel: CustomTypeKindLabel; + + fn label(&self) -> Self::CustomTypeKindLabel; +} + +pub trait CustomTypeKindLabel: Debug + Copy + Clone + PartialEq + Eq { + fn name(&self) -> &'static str; } -pub trait CustomTypeValidation: Debug + Clone + PartialEq + Eq {} +pub trait CustomTypeValidation: Debug + Clone + PartialEq + Eq { + fn compare(base: &Self, compared: &Self) -> ValidationChange; +} pub trait CustomSchema: Debug + Clone + Copy + PartialEq + Eq + 'static { type CustomTypeValidation: CustomTypeValidation; type CustomTypeKind: CustomTypeKind< L, CustomTypeValidation = Self::CustomTypeValidation, + CustomTypeKindLabel = Self::CustomTypeKindLabel, >; + type CustomTypeKindLabel: CustomTypeKindLabel; fn linearize_type_kind( type_kind: Self::CustomTypeKind, diff --git a/sbor/src/schema/schema.rs b/sbor/src/schema/schema.rs index 0ac5291a725..3426ca31464 100644 --- a/sbor/src/schema/schema.rs +++ b/sbor/src/schema/schema.rs @@ -148,6 +148,36 @@ impl SchemaV1 { } } + pub fn resolve_type_data( + &self, + type_id: LocalTypeId, + ) -> Option<( + &TypeKind, LocalTypeId>, + &TypeMetadata, + &TypeValidation, + )> { + match type_id { + LocalTypeId::WellKnown(index) => { + let Some(type_data) = S::resolve_well_known_type(index) else { + return None; + }; + Some((&type_data.kind, &type_data.metadata, &type_data.validation)) + } + LocalTypeId::SchemaLocalIndex(index) => { + let Some(type_kind) = self.type_kinds.get(index) else { + return None; + }; + let Some(type_metadata) = self.type_metadata.get(index) else { + return None; + }; + let Some(type_validation) = self.type_validations.get(index) else { + return None; + }; + Some((type_kind, type_metadata, type_validation)) + } + } + } + pub fn validate(&self) -> Result<(), SchemaValidationError> { validate_schema(self) } diff --git a/sbor/src/schema/schema_validation/type_validation_validation.rs b/sbor/src/schema/schema_validation/type_validation_validation.rs index 9003066f827..d8a500d0fa0 100644 --- a/sbor/src/schema/schema_validation/type_validation_validation.rs +++ b/sbor/src/schema/schema_validation/type_validation_validation.rs @@ -103,7 +103,7 @@ pub fn validate_custom_type_validation<'a, S: CustomSchema>( Ok(()) } -fn validate_numeric_validation( +fn validate_numeric_validation( numeric_validation: &NumericValidation, ) -> Result<(), SchemaValidationError> { if let Some(min) = numeric_validation.min { diff --git a/sbor/src/schema/type_data/type_kind.rs b/sbor/src/schema/type_data/type_kind.rs index 5f10dcc5b51..e1926b9bc56 100644 --- a/sbor/src/schema/type_data/type_kind.rs +++ b/sbor/src/schema/type_data/type_kind.rs @@ -4,8 +4,8 @@ use crate::rust::vec::Vec; /// A schema for the values that a codec can decode / views as valid #[derive(Debug, Clone, PartialEq, Eq, Sbor)] -#[sbor(child_types = "C,L", categorize_types = "L")] -pub enum TypeKind, L: SchemaTypeLink> { +#[sbor(child_types = "T,L", categorize_types = "L")] +pub enum TypeKind, L: SchemaTypeLink> { Any, // Simple Types @@ -32,5 +32,88 @@ pub enum TypeKind, L: SchemaTypeLink> { Map { key_type: L, value_type: L }, // Custom Types - Custom(C), + Custom(T), } + +impl, L: SchemaTypeLink> TypeKind { + pub fn label(&self) -> TypeKindLabel { + match self { + TypeKind::Any => TypeKindLabel::Any, + TypeKind::Bool => TypeKindLabel::Bool, + TypeKind::I8 => TypeKindLabel::I8, + TypeKind::I16 => TypeKindLabel::I16, + TypeKind::I32 => TypeKindLabel::I32, + TypeKind::I64 => TypeKindLabel::I64, + TypeKind::I128 => TypeKindLabel::I128, + TypeKind::U8 => TypeKindLabel::U8, + TypeKind::U16 => TypeKindLabel::U16, + TypeKind::U32 => TypeKindLabel::U32, + TypeKind::U64 => TypeKindLabel::U64, + TypeKind::U128 => TypeKindLabel::U128, + TypeKind::String => TypeKindLabel::String, + TypeKind::Array { .. } => TypeKindLabel::Array, + TypeKind::Tuple { .. } => TypeKindLabel::Tuple, + TypeKind::Enum { .. } => TypeKindLabel::Enum, + TypeKind::Map { .. } => TypeKindLabel::Map, + TypeKind::Custom(custom) => TypeKindLabel::Custom(custom.label()), + } + } + + pub fn category_name(&self) -> &'static str { + self.label().name() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Sbor)] +pub enum TypeKindLabel { + Any, + + // Simple Types + Bool, + I8, + I16, + I32, + I64, + I128, + U8, + U16, + U32, + U64, + U128, + String, + + // Composite Types + Array, + Tuple, + Enum, + Map, + + // Custom Types + Custom(T), +} + + +impl TypeKindLabel { + pub fn name(&self) -> &'static str { + match self { + TypeKindLabel::Any => "Any", + TypeKindLabel::Bool => "Bool", + TypeKindLabel::I8 => "I8", + TypeKindLabel::I16 => "I16", + TypeKindLabel::I32 => "I32", + TypeKindLabel::I64 => "I64", + TypeKindLabel::I128 => "I128", + TypeKindLabel::U8 => "U8", + TypeKindLabel::U16 => "U16", + TypeKindLabel::U32 => "U32", + TypeKindLabel::U64 => "U64", + TypeKindLabel::U128 => "U128", + TypeKindLabel::String => "String", + TypeKindLabel::Array => "Array", + TypeKindLabel::Tuple => "Tuple", + TypeKindLabel::Enum => "Enum", + TypeKindLabel::Map => "Map", + TypeKindLabel::Custom(custom) => custom.name(), + } + } +} \ No newline at end of file diff --git a/sbor/src/schema/type_data/type_metadata.rs b/sbor/src/schema/type_data/type_metadata.rs index 41c73ce9a26..98ff1ee14f5 100644 --- a/sbor/src/schema/type_data/type_metadata.rs +++ b/sbor/src/schema/type_data/type_metadata.rs @@ -1,3 +1,5 @@ +use borrow::Borrow; + use crate::rust::prelude::*; use crate::*; @@ -82,6 +84,22 @@ impl TypeMetadata { } } + pub fn get_field_name<'a>(&'a self, field_index: usize) -> Option<&'a str> { + match &self.child_names { + Some(ChildNames::NamedFields(field_names)) => { + Some(field_names.get(field_index)?.borrow()) + } + _ => None, + } + } + + pub fn get_enum_variant_data<'a>(&'a self, discriminator: u8) -> Option<&'a TypeMetadata> { + match &self.child_names { + Some(ChildNames::EnumVariants(variants)) => variants.get(&discriminator), + _ => None, + } + } + pub fn get_matching_tuple_data(&self, fields_length: usize) -> TupleData { TupleData { name: self.get_name(), diff --git a/sbor/src/schema/type_data/type_validation.rs b/sbor/src/schema/type_data/type_validation.rs index 1f207cfbc27..f081cb5c4f6 100644 --- a/sbor/src/schema/type_data/type_validation.rs +++ b/sbor/src/schema/type_data/type_validation.rs @@ -44,80 +44,118 @@ impl LengthValidation { pub fn is_valid(&self, length: usize) -> bool { self.min.unwrap_or(0) as usize <= length && length <= self.max.unwrap_or(u32::MAX) as usize } + + pub fn compare(base: &Self, compared: &Self) -> ValidationChange { + // Massage it into an equivalent numeric validation comparison + NumericValidation::compare( + &NumericValidation::with_bounds(base.min, base.max), + &NumericValidation::with_bounds(compared.min, compared.max), + ) + } } /// Represents additional validation that should be performed on the numeric value. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Sbor)] -pub struct NumericValidation { +pub struct NumericValidation { pub min: Option, pub max: Option, } -impl NumericValidation { +impl NumericValidation { + pub const fn with_bounds(min: Option, max: Option) -> Self { + Self { + min, + max, + } + } + pub const fn none() -> Self { Self { min: None, max: None, } } -} -impl NumericValidation { - pub fn is_valid(&self, value: i8) -> bool { - self.min.unwrap_or(i8::MIN) <= value && value <= self.max.unwrap_or(i8::MAX) + pub fn compare(base: &Self, compared: &Self) -> ValidationChange { + // Slight warning - `compare` takes the opposite argument order to `cmp`. + // This is to be consistent with the schema comparison arg ordering. + let min_change = match compared.effective_min().cmp(&base.effective_min()) { + core::cmp::Ordering::Less => ValidationChange::Weakened, // Min has decreased + core::cmp::Ordering::Equal => ValidationChange::Unchanged, + core::cmp::Ordering::Greater => ValidationChange::Strengthened, // Min has increased + }; + let max_change = match compared.effective_max().cmp(&base.effective_max()) { + core::cmp::Ordering::Less => ValidationChange::Strengthened, // Max has decreased + core::cmp::Ordering::Equal => ValidationChange::Unchanged, + core::cmp::Ordering::Greater => ValidationChange::Weakened, // Max has increased + }; + ValidationChange::combine(min_change, max_change) } -} -impl NumericValidation { - pub fn is_valid(&self, value: i16) -> bool { - self.min.unwrap_or(i16::MIN) <= value && value <= self.max.unwrap_or(i16::MAX) + pub fn is_valid(&self, value: T) -> bool { + self.effective_min() <= value && value <= self.effective_max() } -} -impl NumericValidation { - pub fn is_valid(&self, value: i32) -> bool { - self.min.unwrap_or(i32::MIN) <= value && value <= self.max.unwrap_or(i32::MAX) + pub fn effective_min(&self) -> T { + self.min.unwrap_or(T::MIN_VALUE) } -} -impl NumericValidation { - pub fn is_valid(&self, value: i64) -> bool { - self.min.unwrap_or(i64::MIN) <= value && value <= self.max.unwrap_or(i64::MAX) + pub fn effective_max(&self) -> T { + self.max.unwrap_or(T::MAX_VALUE) } } -impl NumericValidation { - pub fn is_valid(&self, value: i128) -> bool { - self.min.unwrap_or(i128::MIN) <= value && value <= self.max.unwrap_or(i128::MAX) - } +pub trait NumericValidationBound: Ord + Copy { + const MAX_VALUE: Self; + const MIN_VALUE: Self; } -impl NumericValidation { - pub fn is_valid(&self, value: u8) -> bool { - self.min.unwrap_or(u8::MIN) <= value && value <= self.max.unwrap_or(u8::MAX) - } +impl NumericValidationBound for i8 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; } -impl NumericValidation { - pub fn is_valid(&self, value: u16) -> bool { - self.min.unwrap_or(u16::MIN) <= value && value <= self.max.unwrap_or(u16::MAX) - } +impl NumericValidationBound for i16 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; } -impl NumericValidation { - pub fn is_valid(&self, value: u32) -> bool { - self.min.unwrap_or(u32::MIN) <= value && value <= self.max.unwrap_or(u32::MAX) - } +impl NumericValidationBound for i32 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; } -impl NumericValidation { - pub fn is_valid(&self, value: u64) -> bool { - self.min.unwrap_or(u64::MIN) <= value && value <= self.max.unwrap_or(u64::MAX) - } +impl NumericValidationBound for i64 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; } -impl NumericValidation { - pub fn is_valid(&self, value: u128) -> bool { - self.min.unwrap_or(u128::MIN) <= value && value <= self.max.unwrap_or(u128::MAX) - } +impl NumericValidationBound for i128 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; +} + +impl NumericValidationBound for u8 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; +} + +impl NumericValidationBound for u16 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; +} + +impl NumericValidationBound for u32 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; +} + +impl NumericValidationBound for u64 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; +} + +impl NumericValidationBound for u128 { + const MAX_VALUE: Self = Self::MAX; + const MIN_VALUE: Self = Self::MIN; } diff --git a/sbor/src/traversal/mod.rs b/sbor/src/traversal/mod.rs index 3f84ce69644..e8600c6b97d 100644 --- a/sbor/src/traversal/mod.rs +++ b/sbor/src/traversal/mod.rs @@ -1,5 +1,7 @@ +mod path_formatting; mod typed; mod untyped; +pub use path_formatting::*; pub use typed::*; pub use untyped::*; diff --git a/sbor/src/traversal/path_formatting.rs b/sbor/src/traversal/path_formatting.rs new file mode 100644 index 00000000000..e75c3fb8e37 --- /dev/null +++ b/sbor/src/traversal/path_formatting.rs @@ -0,0 +1,195 @@ +use core::ops::Deref; + +use crate::rust::prelude::*; + +pub struct AnnotatedSborAncestor<'a> { + /// Ideally the type's type name, else the value kind name or type name, depending on what's available + pub name: Cow<'a, str>, + pub container: AnnotatedSborAncestorContainer<'a>, +} + +pub enum AnnotatedSborAncestorContainer<'a> { + Tuple { + field_index: usize, + field_name: Option>, + }, + EnumVariant { + discriminator: u8, + variant_name: Option>, + field_index: usize, + field_name: Option>, + }, + Array { + /// If locating a value, we will have indices, if locating a type, we won't + index: Option, + }, + Map { + /// If locating a value, we will have indices, if locating a type, we won't + index: Option, + entry_part: MapEntryPart, + }, +} + +pub struct AnnotatedSborPartialLeaf<'a> { + /// Ideally the type's type name, else the value kind name or type name, depending on what's available + pub name: Cow<'a, str>, + pub partial_leaf_locator: Option>, +} + +pub enum AnnotatedSborPartialLeafLocator<'a> { + Tuple { + field_offset: Option, + field_name: Option<&'a str>, + }, + EnumVariant { + variant_discriminator: Option, + variant_name: Option<&'a str>, + field_offset: Option, + field_name: Option<&'a str>, + }, + Array { + index: Option, + }, + Map { + index: Option, + entry_part: Option, + }, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum MapEntryPart { + Key, + Value, +} + +pub trait PathAnnotate { + fn iter_ancestor_path(&self) -> Box> + '_>; + + fn annotated_leaf(&self) -> Option>; + + fn format_path(&self) -> String { + let mut buf = String::new(); + self.write_path(&mut buf).unwrap(); + buf + } + + fn write_path(&self, buf: &mut impl core::fmt::Write) -> core::fmt::Result { + let mut is_first = true; + for AnnotatedSborAncestor { name, container } in self.iter_ancestor_path() { + if is_first { + is_first = false; + } else { + write!(buf, "->")?; + } + write!(buf, "{name}")?; + match container { + AnnotatedSborAncestorContainer::Tuple { + field_index: field_offset, + field_name, + } => { + if let Some(field_name) = field_name { + write!(buf, ".[{field_offset}|{field_name}]")?; + } else { + write!(buf, ".[{field_offset}]")?; + } + } + AnnotatedSborAncestorContainer::EnumVariant { + discriminator: variant_discriminator, + variant_name, + field_index: field_offset, + field_name, + } => { + if let Some(variant_name) = variant_name { + write!(buf, "::{{{variant_discriminator}|{variant_name}}}")?; + } else { + write!(buf, "::{{{variant_discriminator}}}")?; + } + if let Some(field_name) = field_name { + write!(buf, ".[{field_offset}|{field_name}]")?; + } else { + write!(buf, ".[{field_offset}]")?; + } + } + AnnotatedSborAncestorContainer::Array { index } => { + if let Some(index) = index { + write!(buf, ".[{index}]")?; + } + } + AnnotatedSborAncestorContainer::Map { index, entry_part } => { + if let Some(index) = index { + write!(buf, ".[{index}]")?; + } + match entry_part { + MapEntryPart::Key => write!(buf, ".Key")?, + MapEntryPart::Value => write!(buf, ".Value")?, + } + } + } + } + if let Some(AnnotatedSborPartialLeaf { + name, + partial_leaf_locator: partial_kinded_data, + }) = self.annotated_leaf() + { + if !is_first { + write!(buf, "->")?; + } + write!(buf, "{}", name.deref())?; + if let Some(partial_kinded_data) = partial_kinded_data { + match partial_kinded_data { + AnnotatedSborPartialLeafLocator::Tuple { + field_offset, + field_name, + } => { + if let Some(field_offset) = field_offset { + if let Some(field_name) = field_name { + write!(buf, ".[{field_offset}|{field_name}]")?; + } else { + write!(buf, ".[{field_offset}]")?; + } + } + } + AnnotatedSborPartialLeafLocator::EnumVariant { + variant_discriminator, + variant_name, + field_offset, + field_name, + } => { + if let Some(variant_discriminator) = variant_discriminator { + if let Some(variant_name) = variant_name { + write!(buf, "::{{{variant_discriminator}|{variant_name}}}")?; + } else { + write!(buf, "::{{{variant_discriminator}}}")?; + } + if let Some(field_offset) = field_offset { + if let Some(field_name) = field_name { + write!(buf, ".[{field_offset}|{field_name}]")?; + } else { + write!(buf, ".[{field_offset}]")?; + } + } + } + } + AnnotatedSborPartialLeafLocator::Array { index } => { + if let Some(index) = index { + write!(buf, ".[{index}]")?; + } + } + AnnotatedSborPartialLeafLocator::Map { index, entry_part } => { + if let Some(index) = index { + write!(buf, ".[{index}]")?; + } + if let Some(entry_part) = entry_part { + match entry_part { + MapEntryPart::Key => write!(buf, ".Key")?, + MapEntryPart::Value => write!(buf, ".Value")?, + } + } + } + } + } + } + + Ok(()) + } +} diff --git a/sbor/src/traversal/typed/events.rs b/sbor/src/traversal/typed/events.rs index 23fe577e227..31a89ca7272 100644 --- a/sbor/src/traversal/typed/events.rs +++ b/sbor/src/traversal/typed/events.rs @@ -14,14 +14,7 @@ impl<'t, 's, 'de, E: CustomExtension> TypedLocatedTraversalEvent<'t, 's, 'de, E> FullLocation { start_offset: self.location.location.start_offset, end_offset: self.location.location.end_offset, - ancestor_path: self - .location - .location - .ancestor_path - .iter() - .cloned() - .zip(self.location.typed_ancestor_path.iter().cloned()) - .collect(), + ancestor_path: self.location.typed_ancestor_path(), current_value_info: self.event.current_value_info(), } } @@ -77,7 +70,10 @@ impl<'de, E: CustomExtension> TypedTraversalEvent<'de, E> { TypedTraversalEvent::Error(TypedTraversalError::ValueMismatchWithType( type_mismatch_error, )) => match type_mismatch_error { - // For these, we have a type mismatch - so we can't return accurate information on "current value" + // For these, we have a type mismatch - so it's not 100% clear what we should display as "current value". + // It probably makes sense to show the expected type name in the error message, but this will require some + // refactoring (e.g. adding the parent type to the Mismatching Child errors, and replacing CurrentValueInfo + // with something like AnnotatedSborPartialLeaf) // Instead, let's handle these when we print the full location TypeMismatchError::MismatchingType { .. } | TypeMismatchError::MismatchingChildElementType { .. } @@ -144,11 +140,30 @@ impl CurrentValueInfo { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct TypedLocation<'t, 's, C: CustomTraversal> { - pub location: Location<'t, C>, +pub struct TypedLocation<'t, 's, T: CustomTraversal> { + pub location: Location<'t, T>, /// The path of container types from the root to the current value. /// If the event is ContainerStart/End, this does not include the newly started/ended container. - pub typed_ancestor_path: &'t [ContainerType<'s>], + /// + /// NOTE: This list includes types for newly read container headers _before_ any children are read, + /// which is before the Location adds them to `ancestor_path`. So in some instances this `typed_container_path` + /// may be strictly longer than `location.ancestor_path`. + pub typed_container_path: &'t [ContainerType<'s>], +} + +impl<'t, 's, T: CustomTraversal> TypedLocation<'t, 's, T> { + pub fn typed_ancestor_path(&self) -> Vec<(AncestorState, ContainerType<'s>)> { + let untyped_ancestor_path = self.location.ancestor_path; + + // As per the note on `typed_container_path`, it can be longer than the ancestor list. + // But zip will end when the shortest iterator ends, so this will correct only return the types of + // the full ancestors. + untyped_ancestor_path + .iter() + .cloned() + .zip(self.typed_container_path.iter().cloned()) + .collect() + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -161,27 +176,23 @@ pub enum TypedTraversalError { #[derive(Debug, Clone, PartialEq, Eq)] pub enum TypeMismatchError { MismatchingType { - expected_type_id: LocalTypeId, - expected_type_kind: - TypeKind<::CustomTypeKind, LocalTypeId>, + type_id: LocalTypeId, + expected_type_kind: TypeKindLabel<::CustomTypeKindLabel>, actual_value_kind: ValueKind, }, MismatchingChildElementType { - expected_type_id: LocalTypeId, - expected_type_kind: - TypeKind<::CustomTypeKind, LocalTypeId>, + type_id: LocalTypeId, + expected_type_kind: TypeKindLabel<::CustomTypeKindLabel>, actual_value_kind: ValueKind, }, MismatchingChildKeyType { - expected_type_id: LocalTypeId, - expected_type_kind: - TypeKind<::CustomTypeKind, LocalTypeId>, + type_id: LocalTypeId, + expected_type_kind: TypeKindLabel<::CustomTypeKindLabel>, actual_value_kind: ValueKind, }, MismatchingChildValueType { - expected_type_id: LocalTypeId, - expected_type_kind: - TypeKind<::CustomTypeKind, LocalTypeId>, + type_id: LocalTypeId, + expected_type_kind: TypeKindLabel<::CustomTypeKindLabel>, actual_value_kind: ValueKind, }, MismatchingTupleLength { diff --git a/sbor/src/traversal/typed/full_location.rs b/sbor/src/traversal/typed/full_location.rs index 9d53891150f..5e68adc8508 100644 --- a/sbor/src/traversal/typed/full_location.rs +++ b/sbor/src/traversal/typed/full_location.rs @@ -1,6 +1,5 @@ use super::*; use crate::rust::fmt::*; -use crate::rust::format; use crate::rust::prelude::*; use crate::traversal::*; use crate::*; @@ -9,10 +8,119 @@ use crate::*; pub struct FullLocation<'s, E: CustomExtension> { pub start_offset: usize, pub end_offset: usize, - pub ancestor_path: Vec<(ContainerState, ContainerType<'s>)>, + pub ancestor_path: Vec<(AncestorState, ContainerType<'s>)>, pub current_value_info: Option>, } +impl<'s, 'a, E: CustomExtension> PathAnnotate + for (&'a FullLocation<'s, E>, &'a Schema) +{ + fn iter_ancestor_path(&self) -> Box> + '_> { + let (full_location, schema) = self; + let schema = *schema; + let iterator = + full_location + .ancestor_path + .iter() + .map(|(container_state, container_type)| { + let type_id = container_type.self_type(); + let metadata = schema.resolve_type_metadata(type_id); + let name = metadata + .and_then(|m| m.get_name()) + .unwrap_or_else(|| container_state.container_header.value_kind_name()); + let header = container_state.container_header; + + let current_child_index = container_state.current_child_index; + + let container = match header { + ContainerHeader::EnumVariant(variant_header) => { + let discriminator = variant_header.variant; + let variant_data = metadata + .and_then(|m| m.get_enum_variant_data(discriminator)); + let variant_name = variant_data + .and_then(|d| d.get_name()) + .map(Cow::Borrowed); + let field_index = current_child_index; + let field_name = variant_data + .and_then(|d| d.get_field_name(field_index)) + .map(Cow::Borrowed); + AnnotatedSborAncestorContainer::EnumVariant { + discriminator, + variant_name, + field_index, + field_name, + } + } + ContainerHeader::Tuple(_) => { + let field_index = current_child_index; + let field_name = metadata + .and_then(|d| d.get_field_name(field_index)) + .map(Cow::Borrowed); + AnnotatedSborAncestorContainer::Tuple { + field_index, + field_name, + } + } + ContainerHeader::Array(_) => { + let index = Some(current_child_index); + AnnotatedSborAncestorContainer::Array { index } + } + ContainerHeader::Map(_) => { + let index = Some(current_child_index / 2); + let entry_part = if current_child_index % 2 == 0 { + MapEntryPart::Key + } else { + MapEntryPart::Value + }; + AnnotatedSborAncestorContainer::Map { index, entry_part } + } + }; + + AnnotatedSborAncestor { + name: Cow::Borrowed(name), + container, + } + }); + Box::new(iterator) + } + + fn annotated_leaf(&self) -> Option> { + let current_value_info = self.0.current_value_info.as_ref()?; + let schema = self.1; + + let metadata = schema.resolve_type_metadata(current_value_info.type_id); + let name = metadata + .and_then(|m| m.get_name()) + .map(Cow::Borrowed) + // We should consider falling back to the TypeKind's name before falling back to the value kind + .unwrap_or_else(|| Cow::Owned(current_value_info.value_kind.to_string())); + let partial_kinded_data = match current_value_info.value_kind { + ValueKind::Enum => { + if let Some(variant_discriminator) = current_value_info.variant { + let variant_data = + metadata.and_then(|v| v.get_enum_variant_data(variant_discriminator)); + let variant_name = variant_data.and_then(|d| d.get_name()); + + Some(AnnotatedSborPartialLeafLocator::EnumVariant { + variant_discriminator: Some(variant_discriminator), + variant_name, + field_offset: None, + field_name: None, + }) + } else { + None + } + } + _ => None, + }; + + Some(AnnotatedSborPartialLeaf { + name, + partial_leaf_locator: partial_kinded_data, + }) + } +} + impl<'s, E: CustomExtension> FullLocation<'s, E> { /// This enables a full path to be provided in an error message, which can have a debug such as: /// EG: `MyStruct.hello[0]->MyEnum::Option2{1}.inner[0]->MyEnum::Option1{0}.[0]->Map[0].Value->Array[0]->Tuple.[0]->Enum::{6}.[0]->Tuple.[1]->Map[0].Key` @@ -20,112 +128,6 @@ impl<'s, E: CustomExtension> FullLocation<'s, E> { /// As much information is extracted from the Type as possible, falling back to data from the value model /// if the Type is Any. pub fn path_to_string(&self, schema: &Schema) -> String { - let mut buf = String::new(); - let mut is_first = true; - for (container_state, container_type) in self.ancestor_path.iter() { - if is_first { - is_first = false; - } else { - write!(buf, "->").unwrap(); - } - let type_id = container_type.self_type(); - let metadata = schema.resolve_type_metadata(type_id); - let type_name = metadata - .and_then(|m| m.get_name()) - .unwrap_or_else(|| container_state.container_header.value_kind_name()); - let current_child_index = container_state.current_child_index; - let header = container_state.container_header; - match header { - ContainerHeader::EnumVariant(variant_header) => { - let variant_data = metadata.and_then(|v| match &v.child_names { - Some(ChildNames::EnumVariants(variants)) => { - variants.get(&variant_header.variant) - } - _ => None, - }); - let variant_part = variant_data - .and_then(|d| d.get_name()) - .map(|variant_name| { - format!("::{{{}|{}}}", variant_header.variant, variant_name,) - }) - .unwrap_or_else(|| format!("::{{{}}}", variant_header.variant)); - let field_part = if let Some(child_index) = current_child_index { - variant_data - .and_then(|d| match &d.child_names { - Some(ChildNames::NamedFields(fields)) => fields.get(child_index), - _ => None, - }) - .map(|field_name| format!(".[{}|{}]", child_index, field_name)) - .unwrap_or_else(|| format!(".[{}]", child_index)) - } else { - format!("") - }; - write!(buf, "{}{}{}", type_name, variant_part, field_part).unwrap(); - } - ContainerHeader::Tuple(_) => { - let field_part = if let Some(child_index) = current_child_index { - metadata - .and_then(|d| match &d.child_names { - Some(ChildNames::NamedFields(fields)) => fields.get(child_index), - _ => None, - }) - .map(|field_name| format!(".[{}|{}]", child_index, field_name)) - .unwrap_or_else(|| format!(".[{}]", child_index)) - } else { - format!("") - }; - write!(buf, "{}{}", type_name, field_part).unwrap(); - } - ContainerHeader::Array(_) => { - let field_part = if let Some(child_index) = current_child_index { - format!(".[{}]", child_index) - } else { - format!("") - }; - write!(buf, "{}{}", type_name, field_part).unwrap(); - } - ContainerHeader::Map(_) => { - let field_part = if let Some(child_index) = current_child_index { - let entry_index = child_index / 2; - let key_or_value = if child_index % 2 == 0 { "Key" } else { "Value" }; - format!(".[{}].{}", entry_index, key_or_value) - } else { - format!("") - }; - - write!(buf, "{}{}", type_name, field_part).unwrap(); - } - } - } - if let Some(current_value_info) = &self.current_value_info { - if !is_first { - write!(buf, "->").unwrap(); - } - let type_kind = schema - .resolve_type_kind(current_value_info.type_id) - .expect("Type index not found in given schema"); - let metadata = if !matches!(type_kind, TypeKind::Any) { - schema.resolve_type_metadata(current_value_info.type_id) - } else { - None - }; - let type_name = metadata - .and_then(|m| m.get_name_string()) - .unwrap_or_else(|| current_value_info.value_kind.to_string()); - if let Some(variant) = current_value_info.variant { - let variant_data = metadata.and_then(|v| match &v.child_names { - Some(ChildNames::EnumVariants(variants)) => variants.get(&variant), - _ => None, - }); - let variant_part = variant_data - .and_then(|d| d.get_name()) - .map(|variant_name| format!("::{{{}|{}}}", variant, variant_name,)) - .unwrap_or_else(|| format!("::{{{}}}", variant)); - write!(buf, "{}{}", type_name, variant_part).unwrap(); - } else { - write!(buf, "{}", type_name).unwrap(); - } - } - buf + (self, schema).format_path() } } diff --git a/sbor/src/traversal/typed/typed_traverser.rs b/sbor/src/traversal/typed/typed_traverser.rs index 3f4aff75105..1c3c5251f3d 100644 --- a/sbor/src/traversal/typed/typed_traverser.rs +++ b/sbor/src/traversal/typed/typed_traverser.rs @@ -124,7 +124,14 @@ impl<'de, 's, E: CustomExtension> TypedTraverser<'de, 's, E> { check_exact_end: bool, ) -> Self { Self { - traverser: VecTraverser::new(input, max_depth, expected_start, check_exact_end), + traverser: VecTraverser::new( + input, + expected_start, + VecTraverserConfig { + max_depth, + check_exact_end, + }, + ), state: TypedTraverserState { container_stack: Vec::with_capacity(max_depth), schema, @@ -140,7 +147,7 @@ impl<'de, 's, E: CustomExtension> TypedTraverser<'de, 's, E> { TypedLocatedTraversalEvent { location: TypedLocation { location, - typed_ancestor_path: &self.state.container_stack, + typed_container_path: &self.state.container_stack, }, event: typed_event, } @@ -159,7 +166,7 @@ impl<'de, 's, E: CustomExtension> TypedTraverser<'de, 's, E> { TypedLocatedTraversalEvent { location: TypedLocation { location, - typed_ancestor_path: &self.state.container_stack, + typed_container_path: &self.state.container_stack, }, event: typed_event, }, @@ -249,7 +256,7 @@ impl<'de, 's, E: CustomExtension> TypedTraverser<'de, 's, E> { next_event.display_as_unexpected_event("ContainerEnd at correct level", schema) ); } - let back_at_start_depth = next_event.location.typed_ancestor_path.len() == start_depth; + let back_at_start_depth = next_event.location.typed_container_path.len() == start_depth; if back_at_start_depth { match next_event.event { TypedTraversalEvent::ContainerEnd(type_id, header) => { @@ -308,8 +315,8 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { _ => return_type_mismatch_error!( location, TypeMismatchError::MismatchingType { - expected_type_id: type_id, - expected_type_kind: container_type.clone(), + type_id, + expected_type_kind: container_type.label(), actual_value_kind: ValueKind::Tuple } ), @@ -338,8 +345,8 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { _ => return_type_mismatch_error!( location, TypeMismatchError::MismatchingType { - expected_type_id: type_id, - expected_type_kind: container_type.clone(), + type_id, + expected_type_kind: container_type.label(), actual_value_kind: ValueKind::Enum } ), @@ -361,8 +368,8 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { return_type_mismatch_error!( location, TypeMismatchError::MismatchingChildElementType { - expected_type_id: *element_type_id, - expected_type_kind: element_type.clone(), + type_id: *element_type_id, + expected_type_kind: element_type.label(), actual_value_kind: element_value_kind } ) @@ -373,8 +380,8 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { _ => return_type_mismatch_error!( location, TypeMismatchError::MismatchingType { - expected_type_id: type_id, - expected_type_kind: container_type.clone(), + type_id, + expected_type_kind: container_type.label(), actual_value_kind: ValueKind::Array } ), @@ -394,8 +401,8 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { return_type_mismatch_error!( location, TypeMismatchError::MismatchingChildKeyType { - expected_type_id: *key_type_id, - expected_type_kind: key_type.clone(), + type_id: *key_type_id, + expected_type_kind: key_type.label(), actual_value_kind: key_value_kind } ) @@ -409,8 +416,8 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { return_type_mismatch_error!( location, TypeMismatchError::MismatchingChildValueType { - expected_type_id: *value_type_id, - expected_type_kind: value_type.clone(), + type_id: *value_type_id, + expected_type_kind: value_type.label(), actual_value_kind: value_value_kind } ) @@ -424,8 +431,8 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { _ => return_type_mismatch_error!( location, TypeMismatchError::MismatchingType { - expected_type_id: type_id, - expected_type_kind: container_type.clone(), + type_id, + expected_type_kind: container_type.label(), actual_value_kind: ValueKind::Map } ), @@ -447,8 +454,8 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { return_type_mismatch_error!( location, TypeMismatchError::MismatchingType { - expected_type_id: type_id, - expected_type_kind: type_kind.clone(), + type_id, + expected_type_kind: type_kind.label(), actual_value_kind: value_kind } ) @@ -469,8 +476,8 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { return_type_mismatch_error!( location, TypeMismatchError::MismatchingType { - expected_type_id: type_id, - expected_type_kind: type_kind.clone(), + type_id, + expected_type_kind: type_kind.label(), actual_value_kind: value_kind } ) @@ -489,10 +496,10 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { } fn get_type_id(&self, location: &Location) -> LocalTypeId { - match location.ancestor_path.last() { - Some(container_state) => { - let child_index = container_state.current_child_index.expect("Callers should ensure `current_child_index.is_some()`"); - match container_state.container_header { + match location.get_latest_ancestor() { + Some(ancestor_state) => { + let child_index = ancestor_state.current_child_index; + match ancestor_state.container_header { ContainerHeader::Tuple(_) | ContainerHeader::EnumVariant(_) | ContainerHeader::Array(_) => { diff --git a/sbor/src/traversal/untyped/events.rs b/sbor/src/traversal/untyped/events.rs index e4d00da5969..721c23c6463 100644 --- a/sbor/src/traversal/untyped/events.rs +++ b/sbor/src/traversal/untyped/events.rs @@ -4,22 +4,22 @@ use crate::*; use super::CustomTraversal; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct LocatedTraversalEvent<'t, 'de, C: CustomTraversal> { - pub location: Location<'t, C>, - pub event: TraversalEvent<'de, C>, +pub struct LocatedTraversalEvent<'t, 'de, T: CustomTraversal> { + pub location: Location<'t, T>, + pub event: TraversalEvent<'de, T>, } #[derive(Debug, Clone, PartialEq, Eq)] -pub enum TraversalEvent<'de, C: CustomTraversal> { - ContainerStart(ContainerHeader), - ContainerEnd(ContainerHeader), - TerminalValue(TerminalValueRef<'de, C>), +pub enum TraversalEvent<'de, T: CustomTraversal> { + ContainerStart(ContainerHeader), + ContainerEnd(ContainerHeader), + TerminalValue(TerminalValueRef<'de, T>), TerminalValueBatch(TerminalValueBatchRef<'de>), End, DecodeError(DecodeError), } -impl<'de, C: CustomTraversal> TraversalEvent<'de, C> { +impl<'de, T: CustomTraversal> TraversalEvent<'de, T> { pub fn is_error(&self) -> bool { match self { TraversalEvent::DecodeError(_) => true, @@ -31,7 +31,7 @@ impl<'de, C: CustomTraversal> TraversalEvent<'de, C> { /// The Location of the encoding - capturing both the byte offset in the payload, and also /// the container-path-based location in the SBOR value model. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Location<'t, C: CustomTraversal> { +pub struct Location<'t, T: CustomTraversal> { /// An offset in the payload, where this `Location` starts. /// The meaning of this offset depends on the context of the event, eg: /// * For ContainerStart, this is the start of the value @@ -44,12 +44,20 @@ pub struct Location<'t, C: CustomTraversal> { /// * For ContainerEnd, this is the end of the whole container value /// * For DecodeError, this is the location where the error occurred pub end_offset: usize, - /// The path of containers from the root to the current value. - /// If the event is ContainerStart/ContainerEnd, this does not include the newly started/ended container. - pub ancestor_path: &'t [ContainerState], + /// The path of containers from the root to the current value. All containers in this list have `current_child_index` set. + /// Note that for certain events: + /// * For ContainerStart/ContainerEnd, this does NOT include the newly started/ended container. + /// * For TerminalValue/TerminalValueBatch, this includes all ancestors of that value/value batch. + /// * For DecodeError, it only includes ancestors where we have started to read their children. + /// * For End, this is an empty slice + pub ancestor_path: &'t [AncestorState], } -impl<'t, C: CustomTraversal> Location<'t, C> { +impl<'t, T: CustomTraversal> Location<'t, T> { + pub fn get_latest_ancestor(&self) -> Option<&AncestorState> { + self.ancestor_path.last() + } + /// Gives the offset of the start of the value body (ignoring the value kind byte). /// The result is only valid if this location corresponds to a ContainerStart/TerminalValue/ContainerEnd event. pub fn get_start_offset_of_value_body(&self) -> usize { @@ -71,11 +79,11 @@ impl<'t, C: CustomTraversal> Location<'t, C> { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ContainerHeader { +pub enum ContainerHeader { Tuple(TupleHeader), EnumVariant(EnumVariantHeader), - Array(ArrayHeader), - Map(MapHeader), + Array(ArrayHeader), + Map(MapHeader), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -102,8 +110,8 @@ pub struct MapHeader { pub length: usize, } -impl ContainerHeader { - pub fn get_own_value_kind(&self) -> ValueKind { +impl ContainerHeader { + pub fn get_own_value_kind(&self) -> ValueKind { match self { ContainerHeader::Tuple(_) => ValueKind::Tuple, ContainerHeader::EnumVariant(_) => ValueKind::Enum, @@ -133,7 +141,7 @@ impl ContainerHeader { pub fn get_implicit_child_value_kind( &self, index: usize, - ) -> Option> { + ) -> Option> { match self { ContainerHeader::Tuple(_) => None, ContainerHeader::EnumVariant(_) => None, diff --git a/sbor/src/traversal/untyped/traverser.rs b/sbor/src/traversal/untyped/traverser.rs index 1ecac14a496..4096e98cd51 100644 --- a/sbor/src/traversal/untyped/traverser.rs +++ b/sbor/src/traversal/untyped/traverser.rs @@ -1,6 +1,7 @@ use super::*; use crate::decoder::BorrowingDecoder; use crate::rust::prelude::*; +use crate::rust::str; use crate::value_kind::*; use crate::*; @@ -13,9 +14,11 @@ pub fn calculate_value_tree_body_byte_length<'de, 's, E: CustomExtension>( ) -> Result { let mut traverser = VecTraverser::::new( partial_payload, - depth_limit - current_depth, ExpectedStart::ValueBody(value_kind), - false, + VecTraverserConfig { + max_depth: depth_limit - current_depth, + check_exact_end: false, + }, ); loop { let next_event = traverser.next_event(); @@ -33,7 +36,7 @@ pub trait CustomTraversal: Copy + Debug + Clone + PartialEq + Eq { CustomValueKind = Self::CustomValueKind, >; - fn decode_custom_value_body<'de, R>( + fn read_custom_value_body<'de, R>( custom_value_kind: Self::CustomValueKind, reader: &mut R, ) -> Result, DecodeError> @@ -48,42 +51,23 @@ pub trait CustomTerminalValueRef: Debug + Clone + PartialEq + Eq { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ContainerState { - pub container_header: ContainerHeader, +pub struct AncestorState { + pub container_header: ContainerHeader, + /// The byte offset of the start of the container in the input buffer pub container_start_offset: usize, - pub container_child_count: usize, - pub current_child_index: Option, + /// Goes from 0,... container_header.child_count() - 1 as children in the container are considered. + /// NOTE: For maps, container_header.child_count() = 2 * Map length + /// + /// The `current_child_index` does NOT necessarily point at a valid value which can be decoded, + /// - the index is updated before the child is read, to record errors against it. + pub current_child_index: usize, } -impl ContainerState { - pub fn is_complete(&self) -> bool { - if self.container_child_count == 0 { - return true; - } - - if let Some(index) = self.current_child_index { - if index >= self.container_child_count - 1 { - return true; - } - } - - return false; - } - - pub fn advance_current_child_index(&mut self) { - self.advance_current_child_index_by(1) - } - - pub fn advance_current_child_index_by(&mut self, n: usize) { - if n == 0 { - return; - } - - if let Some(index) = self.current_child_index { - self.current_child_index = Some(index + n) - } else { - self.current_child_index = Some(n - 1) - } +impl AncestorState { + #[inline] + fn get_implicit_value_kind_of_current_child(&self) -> Option> { + self.container_header + .get_implicit_child_value_kind(self.current_child_index) } } @@ -91,60 +75,38 @@ impl ContainerState { /// It turns payload decoding into a pull-based event stream. /// /// The caller is responsible for stopping calling `next_event` after an Error or End event. -pub struct VecTraverser<'de, C: CustomTraversal> { - max_depth: usize, - check_exact_end: bool, - decoder: VecDecoder<'de, C::CustomValueKind>, - container_stack: Vec>, - next_event_override: NextEventOverride, -} - -#[derive(Debug, Clone, Copy)] -pub enum NextEventOverride { - ReadPrefix(u8), - ReadRootValue, - ReadRootValueWithValueKind(ValueKind), - ReadBytes(usize), - None, -} - -#[macro_export] -macro_rules! terminal_value_from_body { - ($self: expr, $value_type: ident, $type: ident, $start_offset: expr, $value_kind: expr) => {{ - terminal_value!( - $self, - $value_type, - $start_offset, - $type::decode_body_with_value_kind(&mut $self.decoder, $value_kind) - ) - }}; +pub struct VecTraverser<'de, T: CustomTraversal> { + decoder: VecDecoder<'de, T::CustomValueKind>, + ancestor_path: Vec>, + next_action: NextAction, + config: VecTraverserConfig, } -#[macro_export] -macro_rules! terminal_value { - ($self: expr, $value_type: ident, $start_offset: expr, $decoded: expr) => {{ - match $decoded { - Ok(value) => LocatedTraversalEvent { - event: TraversalEvent::TerminalValue(TerminalValueRef::$value_type(value)), - location: Location { - start_offset: $start_offset, - end_offset: $self.get_offset(), - ancestor_path: &$self.container_stack, - }, - }, - Err(error) => $self.map_error($start_offset, error), - } - }}; +pub struct VecTraverserConfig { + pub max_depth: usize, + pub check_exact_end: bool, } -#[macro_export] -macro_rules! return_if_error { - ($self: expr, $result: expr) => {{ - match $result { - Ok(value) => value, - Err(error) => return $self.map_error($self.get_offset(), error), - } - }}; +#[derive(Debug, Clone, Copy)] +pub enum NextAction { + ReadPrefix { + expected_prefix: u8, + }, + ReadRootValue, + ReadRootValueBody { + implicit_value_kind: ValueKind, + }, + ReadContainerContentStart { + container_header: ContainerHeader, + container_start_offset: usize, + }, + /// The state which is put into after entering parent, and + /// the default state to return to from below + ReadNextChildOrExitContainer, + Errored, + Ended, + /// Impossible to observe this value + InProgressPlaceholder, } #[derive(Debug, Clone, Copy)] @@ -157,313 +119,393 @@ pub enum ExpectedStart { impl<'de, T: CustomTraversal> VecTraverser<'de, T> { pub fn new( input: &'de [u8], - max_depth: usize, expected_start: ExpectedStart, - check_exact_end: bool, + config: VecTraverserConfig, ) -> Self { Self { - decoder: VecDecoder::new(input, max_depth), - container_stack: Vec::with_capacity(max_depth), - max_depth, - next_event_override: match expected_start { - ExpectedStart::PayloadPrefix(prefix) => NextEventOverride::ReadPrefix(prefix), - ExpectedStart::Value => NextEventOverride::ReadRootValue, - ExpectedStart::ValueBody(value_kind) => { - NextEventOverride::ReadRootValueWithValueKind(value_kind) - } + // Note that the VecTraverser needs to be very low level for performance, + // so purposefully doesn't use the depth tracking in the decoder itself. + // But we set a max depth anyway, for safety. + decoder: VecDecoder::new(input, config.max_depth), + ancestor_path: Vec::with_capacity(config.max_depth), + next_action: match expected_start { + ExpectedStart::PayloadPrefix(prefix) => NextAction::ReadPrefix { + expected_prefix: prefix, + }, + ExpectedStart::Value => NextAction::ReadRootValue, + ExpectedStart::ValueBody(value_kind) => NextAction::ReadRootValueBody { + implicit_value_kind: value_kind, + }, }, - check_exact_end, + config, } } pub fn next_event<'t>(&'t mut self) -> LocatedTraversalEvent<'t, 'de, T> { - match self.next_event_override { - NextEventOverride::ReadPrefix(expected_prefix) => { - self.next_event_override = NextEventOverride::ReadRootValue; - return_if_error!( - self, - self.decoder.read_and_check_payload_prefix(expected_prefix) - ); - self.next_event() - } - NextEventOverride::ReadRootValue => { - self.next_event_override = NextEventOverride::None; - self.read_root_value(None) + let (event, next_action) = Self::step( + core::mem::replace(&mut self.next_action, NextAction::InProgressPlaceholder), + &self.config, + &mut self.decoder, + &mut self.ancestor_path, + ); + self.next_action = next_action; + event + } + + #[inline] + fn step<'t, 'd>( + action: NextAction, + config: &VecTraverserConfig, + decoder: &'d mut VecDecoder<'de, T::CustomValueKind>, + ancestor_path: &'t mut Vec>, + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { + match action { + NextAction::ReadPrefix { expected_prefix } => { + // The reading of the prefix has no associated event, so we perform the prefix check first, + // and then proceed to read the root value if it succeeds. + let start_offset = decoder.get_offset(); + match decoder.read_and_check_payload_prefix(expected_prefix) { + Ok(()) => { + // Prefix read successfully. Now read root value. + ActionHandler::new_from_current_offset(ancestor_path, decoder) + .read_value(None) + } + Err(error) => { + ActionHandler::new_with_fixed_offset(ancestor_path, decoder, start_offset) + .complete_with_error(error) + } + } } - NextEventOverride::ReadRootValueWithValueKind(value_kind) => { - self.next_event_override = NextEventOverride::None; - self.read_root_value(Some(value_kind)) + NextAction::ReadRootValue => { + ActionHandler::new_from_current_offset(ancestor_path, decoder).read_value(None) } - NextEventOverride::ReadBytes(size) => { - self.next_event_override = NextEventOverride::None; - self.read_bytes_event_override(size) + NextAction::ReadRootValueBody { + implicit_value_kind, + } => ActionHandler::new_from_current_offset(ancestor_path, decoder) + .read_value(Some(implicit_value_kind)), + NextAction::ReadContainerContentStart { + container_header, + container_start_offset, + } => { + let container_child_size = container_header.get_child_count(); + if container_child_size == 0 { + // If the container has no children, we immediately container end without ever bothering + // adding it as an ancestor. + return ActionHandler::new_with_fixed_offset( + ancestor_path, + decoder, + container_start_offset, + ) + .complete_container_end(container_header); + } + + // Add ancestor before checking for max depth so that the ancestor stack is + // correct if the depth check returns an error + ancestor_path.push(AncestorState { + container_header, + container_start_offset, + current_child_index: 0, + }); + // We know we're about to read a child at depth ancestor_path.len() + 1 - so + // it's an error if ancestor_path.len() >= config.max_depth. + // (We avoid the +1 so that we don't need to worry about overflow). + if ancestor_path.len() >= config.max_depth { + return ActionHandler::new_from_current_offset(ancestor_path, decoder) + .complete_with_error(DecodeError::MaxDepthExceeded(config.max_depth)); + } + + let parent = ancestor_path.last_mut().unwrap(); + let parent_container = &parent.container_header; + let is_byte_array = matches!( + parent_container, + ContainerHeader::Array(ArrayHeader { + element_value_kind: ValueKind::U8, + .. + }) + ); + // If it's a byte array, we do a batch-read optimisation + if is_byte_array { + // We know this is >= 1 from the above check + let array_length = container_child_size; + let max_index_which_would_be_read = array_length - 1; + // Set current child index before we read so that if we get an error on read + // then it comes through at the max child index we attempted to read. + parent.current_child_index = max_index_which_would_be_read; + ActionHandler::new_from_current_offset(ancestor_path, decoder) + .read_byte_array(array_length) + } else { + // NOTE: parent.current_child_index is already 0, so no need to change it + let implicit_value_kind = parent.get_implicit_value_kind_of_current_child(); + ActionHandler::new_from_current_offset(ancestor_path, decoder) + .read_value(implicit_value_kind) + } } - NextEventOverride::None => { - let parent = self.container_stack.last(); + NextAction::ReadNextChildOrExitContainer => { + let parent = ancestor_path.last_mut(); match parent { Some(parent) => { - if parent.is_complete() { - self.exit_container() + let next_child_index = parent.current_child_index + 1; + let is_complete = + next_child_index >= parent.container_header.get_child_count(); + if is_complete { + // We pop the completed parent from the ancestor list + let AncestorState { + container_header, + container_start_offset, + .. + } = ancestor_path.pop().expect("Parent has just been read"); + + ActionHandler::new_with_fixed_offset( + ancestor_path, + decoder, + container_start_offset, + ) + .complete_container_end(container_header) } else { - self.read_child_value() + parent.current_child_index = next_child_index; + let implicit_value_kind = + parent.get_implicit_value_kind_of_current_child(); + ActionHandler::new_from_current_offset(ancestor_path, decoder) + .read_value(implicit_value_kind) } } - None => self.read_end(), + None => { + ActionHandler::new_from_current_offset(ancestor_path, decoder).end(config) + } } } + NextAction::Errored => { + panic!("It is unsupported to call `next_event` on a traverser which has returned an error.") + } + NextAction::Ended => { + panic!("It is unsupported to call `next_event` on a traverser which has already emitted an end event.") + } + NextAction::InProgressPlaceholder => { + unreachable!("It is not possible to observe this value - it is a placeholder for rust memory safety.") + } } } +} - fn enter_container<'t>( - &'t mut self, - start_offset: usize, - container_header: ContainerHeader, - ) -> LocatedTraversalEvent<'t, 'de, T> { - let child_count = container_header.get_child_count(); - - self.container_stack.push(ContainerState { - container_header, - container_start_offset: start_offset, - container_child_count: child_count, - current_child_index: None, - }); - - // Check depth: either container stack overflows or children of this container will overflow. - if self.container_stack.len() > self.max_depth - || self.container_stack.len() == self.max_depth && child_count > 0 - { - return self.map_error(start_offset, DecodeError::MaxDepthExceeded(self.max_depth)); +macro_rules! handle_error { + ($action_handler: expr, $result: expr$(,)?) => {{ + match $result { + Ok(value) => value, + Err(error) => { + return $action_handler.complete_with_error(error); + } } + }}; +} - LocatedTraversalEvent { - event: TraversalEvent::ContainerStart(container_header), - location: Location { - start_offset, - end_offset: self.get_offset(), - ancestor_path: &self.container_stack[0..self.container_stack.len() - 1], - }, +/// This is just an encapsulation to improve code quality by: +/// * Removing code duplication by capturing the ancestor_path/decoder/start_offset in one place +/// * Ensuring code correctness by fixing the ancestor path +struct ActionHandler<'t, 'd, 'de, T: CustomTraversal> { + ancestor_path: &'t [AncestorState], + decoder: &'d mut VecDecoder<'de, T::CustomValueKind>, + start_offset: usize, +} + +impl<'t, 'd, 'de, T: CustomTraversal> ActionHandler<'t, 'd, 'de, T> { + #[inline] + fn new_from_current_offset( + ancestor_path: &'t [AncestorState], + decoder: &'d mut VecDecoder<'de, T::CustomValueKind>, + ) -> Self { + let start_offset = decoder.get_offset(); + Self { + ancestor_path, + decoder, + start_offset, } } - fn exit_container<'t>(&'t mut self) -> LocatedTraversalEvent<'t, 'de, T> { - let container = self.container_stack.pop().unwrap(); - LocatedTraversalEvent { - event: TraversalEvent::ContainerEnd(container.container_header), - location: Location { - start_offset: container.container_start_offset, - end_offset: self.get_offset(), - ancestor_path: &self.container_stack, - }, + #[inline] + fn new_with_fixed_offset( + ancestor_path: &'t [AncestorState], + decoder: &'d mut VecDecoder<'de, T::CustomValueKind>, + start_offset: usize, + ) -> Self { + Self { + ancestor_path, + decoder, + start_offset, } } - fn read_root_value<'t>( - &'t mut self, - value_kind: Option>, - ) -> LocatedTraversalEvent<'t, 'de, T> { - let start_offset = self.decoder.get_offset(); - let value_kind = match value_kind { + #[inline] + fn read_value( + self, + implicit_value_kind: Option>, + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { + let value_kind = match implicit_value_kind { Some(value_kind) => value_kind, - None => return_if_error!(self, self.decoder.read_value_kind()), + None => handle_error!(self, self.decoder.read_value_kind()), }; - self.next_value(start_offset, value_kind) + self.read_value_body(value_kind) } - fn read_child_value<'t>(&'t mut self) -> LocatedTraversalEvent<'t, 'de, T> { - let start_offset = self.decoder.get_offset(); - let parent = self.container_stack.last_mut().unwrap(); - parent.advance_current_child_index(); - let value_kind = parent - .container_header - .get_implicit_child_value_kind(parent.current_child_index.unwrap()); - let value_kind = match value_kind { - Some(value_kind) => value_kind, - None => return_if_error!(self, self.decoder.read_value_kind()), - }; - self.next_value(start_offset, value_kind) + #[inline] + fn read_byte_array( + self, + array_length: usize, + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { + let bytes = handle_error!(self, self.decoder.read_slice_from_payload(array_length)); + self.complete( + TraversalEvent::TerminalValueBatch(TerminalValueBatchRef::U8(bytes)), + // This is the correct action to ensure we exit the container on the next step + NextAction::ReadNextChildOrExitContainer, + ) } - fn next_value<'t>( - &'t mut self, - start_offset: usize, + #[inline] + fn end( + self, + config: &VecTraverserConfig, + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { + if config.check_exact_end { + handle_error!(self, self.decoder.check_end()); + } + self.complete(TraversalEvent::End, NextAction::Ended) + } + + fn read_value_body( + self, value_kind: ValueKind, - ) -> LocatedTraversalEvent<'t, 'de, T> { + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { match value_kind { - ValueKind::Bool => { - terminal_value_from_body!(self, Bool, bool, start_offset, value_kind) - } - ValueKind::I8 => { - terminal_value_from_body!(self, I8, i8, start_offset, value_kind) - } - ValueKind::I16 => { - terminal_value_from_body!(self, I16, i16, start_offset, value_kind) - } - ValueKind::I32 => { - terminal_value_from_body!(self, I32, i32, start_offset, value_kind) - } - ValueKind::I64 => { - terminal_value_from_body!(self, I64, i64, start_offset, value_kind) - } - ValueKind::I128 => { - terminal_value_from_body!(self, I128, i128, start_offset, value_kind) - } - ValueKind::U8 => { - terminal_value_from_body!(self, U8, u8, start_offset, value_kind) - } - ValueKind::U16 => { - terminal_value_from_body!(self, U16, u16, start_offset, value_kind) + ValueKind::Bool => self.read_terminal_value(value_kind, TerminalValueRef::Bool), + ValueKind::I8 => self.read_terminal_value(value_kind, TerminalValueRef::I8), + ValueKind::I16 => self.read_terminal_value(value_kind, TerminalValueRef::I16), + ValueKind::I32 => self.read_terminal_value(value_kind, TerminalValueRef::I32), + ValueKind::I64 => self.read_terminal_value(value_kind, TerminalValueRef::I64), + ValueKind::I128 => self.read_terminal_value(value_kind, TerminalValueRef::I128), + ValueKind::U8 => self.read_terminal_value(value_kind, TerminalValueRef::U8), + ValueKind::U16 => self.read_terminal_value(value_kind, TerminalValueRef::U16), + ValueKind::U32 => self.read_terminal_value(value_kind, TerminalValueRef::U32), + ValueKind::U64 => self.read_terminal_value(value_kind, TerminalValueRef::U64), + ValueKind::U128 => self.read_terminal_value(value_kind, TerminalValueRef::U128), + ValueKind::String => { + let length = handle_error!(self, self.decoder.read_size()); + let bytes = handle_error!(self, self.decoder.read_slice_from_payload(length)); + let string_body = handle_error!( + self, + str::from_utf8(bytes).map_err(|_| DecodeError::InvalidUtf8) + ); + self.complete( + TraversalEvent::TerminalValue(TerminalValueRef::String(string_body)), + NextAction::ReadNextChildOrExitContainer, + ) } - ValueKind::U32 => { - terminal_value_from_body!(self, U32, u32, start_offset, value_kind) + ValueKind::Array => { + let element_value_kind = handle_error!(self, self.decoder.read_value_kind()); + let length = handle_error!(self, self.decoder.read_size()); + self.complete_container_start(ContainerHeader::Array(ArrayHeader { + element_value_kind, + length, + })) } - ValueKind::U64 => { - terminal_value_from_body!(self, U64, u64, start_offset, value_kind) + ValueKind::Map => { + let key_value_kind = handle_error!(self, self.decoder.read_value_kind()); + let value_value_kind = handle_error!(self, self.decoder.read_value_kind()); + let length = handle_error!(self, self.decoder.read_size()); + self.complete_container_start(ContainerHeader::Map(MapHeader { + key_value_kind, + value_value_kind, + length, + })) } - ValueKind::U128 => { - terminal_value_from_body!(self, U128, u128, start_offset, value_kind) + ValueKind::Enum => { + let variant = handle_error!(self, self.decoder.read_byte()); + let length = handle_error!(self, self.decoder.read_size()); + self.complete_container_start(ContainerHeader::EnumVariant(EnumVariantHeader { + variant, + length, + })) } - ValueKind::String => { - terminal_value!(self, String, start_offset, self.decode_string_body()) + ValueKind::Tuple => { + let length = handle_error!(self, self.decoder.read_size()); + self.complete_container_start(ContainerHeader::Tuple(TupleHeader { length })) } - ValueKind::Array => self.decode_array_header(start_offset), - ValueKind::Map => self.decode_map_header(start_offset), - ValueKind::Enum => self.decode_enum_variant_header(start_offset), - ValueKind::Tuple => self.decode_tuple_header(start_offset), ValueKind::Custom(custom_value_kind) => { - let result = T::decode_custom_value_body(custom_value_kind, &mut self.decoder); - let location = Location { - start_offset: start_offset, - end_offset: self.get_offset(), - ancestor_path: &self.container_stack, - }; - let event = match result { - Ok(custom_value) => { - TraversalEvent::TerminalValue(TerminalValueRef::Custom(custom_value)) - } - Err(decode_error) => TraversalEvent::DecodeError(decode_error), - }; - LocatedTraversalEvent { location, event } + let custom_value_ref = handle_error!( + self, + T::read_custom_value_body(custom_value_kind, self.decoder) + ); + self.complete( + TraversalEvent::TerminalValue(TerminalValueRef::Custom(custom_value_ref)), + NextAction::ReadNextChildOrExitContainer, + ) } } } - fn map_error<'t>( - &'t self, - start_offset: usize, - error: DecodeError, - ) -> LocatedTraversalEvent<'t, 'de, T> { - LocatedTraversalEvent { - event: TraversalEvent::DecodeError(error), - location: Location { - start_offset, - end_offset: self.get_offset(), - ancestor_path: &self.container_stack, - }, + #[inline] + fn read_terminal_value>>( + self, + value_kind: ValueKind, + value_ref_constructor: impl Fn(V) -> TerminalValueRef<'de, T>, + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { + match V::decode_body_with_value_kind(self.decoder, value_kind) { + Ok(value) => self.complete( + TraversalEvent::TerminalValue(value_ref_constructor(value)), + NextAction::ReadNextChildOrExitContainer, + ), + Err(error) => self.complete_with_error(error), } } #[inline] - fn get_offset(&self) -> usize { - self.decoder.get_offset() - } - - fn decode_string_body(&mut self) -> Result<&'de str, DecodeError> { - let size = self.decoder.read_size()?; - let bytes_slices = self.decoder.read_slice_from_payload(size)?; - sbor::rust::str::from_utf8(bytes_slices).map_err(|_| DecodeError::InvalidUtf8) - } - - fn decode_enum_variant_header<'t>( - &'t mut self, - start_offset: usize, - ) -> LocatedTraversalEvent<'t, 'de, T> { - let variant = return_if_error!(self, self.decoder.read_byte()); - let length = return_if_error!(self, self.decoder.read_size()); - self.enter_container( - start_offset, - ContainerHeader::EnumVariant(EnumVariantHeader { variant, length }), - ) - } - - fn decode_tuple_header<'t>( - &'t mut self, - start_offset: usize, - ) -> LocatedTraversalEvent<'t, 'de, T> { - let length = return_if_error!(self, self.decoder.read_size()); - self.enter_container(start_offset, ContainerHeader::Tuple(TupleHeader { length })) - } - - fn decode_array_header<'t>( - &'t mut self, - start_offset: usize, - ) -> LocatedTraversalEvent<'t, 'de, T> { - let element_value_kind = return_if_error!(self, self.decoder.read_value_kind()); - let length = return_if_error!(self, self.decoder.read_size()); - if element_value_kind == ValueKind::U8 && length > 0 { - self.next_event_override = NextEventOverride::ReadBytes(length); - } - self.enter_container( - start_offset, - ContainerHeader::Array(ArrayHeader { - element_value_kind, - length, - }), + fn complete_container_start( + self, + container_header: ContainerHeader, + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { + let next_action = NextAction::ReadContainerContentStart { + container_header: container_header.clone(), + container_start_offset: self.start_offset, + }; + self.complete( + TraversalEvent::ContainerStart(container_header), + next_action, ) } - fn decode_map_header<'t>( - &'t mut self, - start_offset: usize, - ) -> LocatedTraversalEvent<'t, 'de, T> { - let key_value_kind = return_if_error!(self, self.decoder.read_value_kind()); - let value_value_kind = return_if_error!(self, self.decoder.read_value_kind()); - let length = return_if_error!(self, self.decoder.read_size()); - self.enter_container( - start_offset, - ContainerHeader::Map(MapHeader { - key_value_kind, - value_value_kind, - length, - }), + #[inline] + fn complete_container_end( + self, + container_header: ContainerHeader, + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { + self.complete( + TraversalEvent::ContainerEnd(container_header), + // Continue interating the parent + NextAction::ReadNextChildOrExitContainer, ) } - fn read_end<'t>(&'t self) -> LocatedTraversalEvent<'t, 'de, T> { - if self.check_exact_end { - return_if_error!(self, self.decoder.check_end()); - } - let offset = self.decoder.get_offset(); - - LocatedTraversalEvent { - event: TraversalEvent::End, - location: Location { - start_offset: offset, - end_offset: offset, - ancestor_path: &self.container_stack, - }, - } + #[inline] + fn complete_with_error( + self, + error: DecodeError, + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { + self.complete(TraversalEvent::DecodeError(error), NextAction::Errored) } - fn read_bytes_event_override<'t>( - &'t mut self, - size: usize, - ) -> LocatedTraversalEvent<'t, 'de, T> { - let start_offset = self.get_offset(); - let bytes = return_if_error!(self, self.decoder.read_slice_from_payload(size)); - // Set it up so that we jump to the end of the child iteration - self.container_stack - .last_mut() - .unwrap() - .advance_current_child_index_by(size); - self.next_event_override = NextEventOverride::None; - LocatedTraversalEvent { - event: TraversalEvent::TerminalValueBatch(TerminalValueBatchRef::U8(bytes)), + #[inline] + fn complete( + self, + traversal_event: TraversalEvent<'de, T>, + next_action: NextAction, + ) -> (LocatedTraversalEvent<'t, 'de, T>, NextAction) { + let located_event = LocatedTraversalEvent { + event: traversal_event, location: Location { - start_offset, - end_offset: self.get_offset(), - ancestor_path: &self.container_stack, + start_offset: self.start_offset, + end_offset: self.decoder.get_offset(), + ancestor_path: self.ancestor_path, }, - } + }; + (located_event, next_action) } } From 7e708542c42a3adbb6a130092787a79625fdc651 Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 11:49:47 +0100 Subject: [PATCH 04/19] feature: Add schema comparison tooling --- sbor-tests/Cargo.toml | 1 + sbor-tests/tests/schema_comparison.rs | 454 +++++++ sbor/src/lib.rs | 3 + sbor/src/schema/mod.rs | 2 + sbor/src/schema/schema_comparison.rs | 1770 +++++++++++++++++++++++++ sbor/src/schema/type_aggregator.rs | 52 +- sbor/src/vec_traits.rs | 33 + 7 files changed, 2309 insertions(+), 6 deletions(-) create mode 100644 sbor-tests/tests/schema_comparison.rs create mode 100644 sbor/src/schema/schema_comparison.rs create mode 100644 sbor/src/vec_traits.rs diff --git a/sbor-tests/Cargo.toml b/sbor-tests/Cargo.toml index 1aa74bb9173..b4a3158bbed 100644 --- a/sbor-tests/Cargo.toml +++ b/sbor-tests/Cargo.toml @@ -9,6 +9,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } bincode = { workspace = true, features = ["derive"] } bencher = { workspace = true } +hex = { workspace = true } [[bench]] name = "bench" diff --git a/sbor-tests/tests/schema_comparison.rs b/sbor-tests/tests/schema_comparison.rs new file mode 100644 index 00000000000..92fea705296 --- /dev/null +++ b/sbor-tests/tests/schema_comparison.rs @@ -0,0 +1,454 @@ +use sbor::prelude::*; +use sbor::schema::*; +use sbor::BasicValue; +use sbor::NoCustomExtension; +use sbor::NoCustomSchema; +use sbor::NoCustomTypeKind; +use sbor::ComparisonSchema; + +//===================== +// HELPER CODE / TRAITS +//===================== +trait DerivableTypeSchema: Describe { + fn single_type_schema_version() -> String { + let single_type_schema_version: SingleTypeSchema = SingleTypeSchema::::from::(); + ComparisonSchema::::encode_to_hex(&single_type_schema_version) + } +} + +impl> DerivableTypeSchema for T {} + +trait RegisterType { + fn register_schema_of(self, name: &'static str) -> Self; +} + +impl> RegisterType for NamedSchemaVersions { + fn register_schema_of(self, name: &'static str) -> Self { + self.register_version(name, T::single_type_schema_version()) + } +} + +fn assert_extension() { + assert_type_backwards_compatibility::(|v| v + .register_schema_of::("base") + .register_schema_of::("latest") + ) +} + +fn assert_extension_ignoring_name_changes() { + let settings = SchemaComparisonSettings::allow_structural_extension() + .metadata_settings(SchemaComparisonMetadataSettings::allow_all_changes()); + assert_comparison_succeeds::(&settings); +} + +fn assert_equality() { + assert_comparison_succeeds::(&SchemaComparisonSettings::require_equality()); +} + +fn assert_equality_ignoring_name_changes() { + let settings = SchemaComparisonSettings::require_equality() + .metadata_settings(SchemaComparisonMetadataSettings::allow_all_changes()); + assert_comparison_succeeds::(&settings); +} + +fn assert_comparison_succeeds(settings: &SchemaComparisonSettings) { + assert_type_compatibility::( + settings, + |v| v + .register_schema_of::("base") + .register_schema_of::("latest") + ) +} + +fn assert_extension_multi( + base: NamedTypesSchema, + latest: NamedTypesSchema, +) { + let settings = SchemaComparisonSettings::allow_structural_extension(); + assert_multi_comparison_succeeds(&settings, base, latest); +} + +fn assert_equality_multi( + base: NamedTypesSchema, + latest: NamedTypesSchema, +) { + let settings = SchemaComparisonSettings::require_equality(); + assert_multi_comparison_succeeds(&settings, base, latest); +} + +fn assert_multi_comparison_succeeds( + settings: &SchemaComparisonSettings, + base: NamedTypesSchema, + latest: NamedTypesSchema, +) { + assert_type_collection_backwards_compatibility::( + settings, + latest.clone(), + |v| v + .register_version("base", base) + .register_version("latest", latest), + ) +} + +//============= +// HELPER TYPES +//============= + +#[derive(Sbor)] +#[sbor(type_name = "MyStruct")] +struct MyStruct { + val: u8, +} + +#[derive(Sbor)] +#[sbor(type_name = "MyStruct")] +struct MyStructFieldRenamed { + val_renamed: u8, +} + +#[derive(Sbor)] +#[sbor(type_name = "MyStructTypeRenamed")] +struct MyStructTypeRenamed { + val: u8, +} + +#[derive(Sbor)] +#[sbor(type_name = "MyStruct")] +struct MyStructNewField { + val: u8, + field_2: u8, +} + +type MyNamefreeNestedTuple = (u8, (u16, u16)); +type NamelessAny = BasicValue; + +#[derive(BasicDescribe)] +#[sbor(transparent)] +pub struct MyAny(pub BasicValue); + +#[derive(Sbor)] +#[sbor(type_name = "MyEnum")] +enum MyEnum { + Variant1, + Variant2, + Variant3(u8, u16), + Variant4 { my_val: i32, my_struct: MyStruct, }, +} + +#[derive(Sbor)] +#[sbor(type_name = "MyEnum")] +enum MyEnumVariantRenamed { + Variant1, + Variant2V2, + Variant3(u8, u16), + Variant4 { my_val: i32, my_struct: MyStruct, }, +} + +#[derive(Sbor)] +#[sbor(type_name = "MyEnum")] +enum MyEnumVariantFieldRenamed { + Variant1, + Variant2, + Variant3(u8, u16), + Variant4 { my_val_renamed: i32, my_struct: MyStruct, }, +} + +#[derive(Sbor)] +#[sbor(type_name = "MyEnum")] +enum MyEnumNewVariant { + Variant1, + Variant2, + Variant3(u8, u16), + Variant4 { my_val: i32, my_struct: MyStruct, }, + Variant5, +} + +#[derive(Sbor)] +#[sbor(type_name = "MyEnum")] +enum MyEnumVariantFieldAdded { + Variant1, + Variant2, + Variant3(u8, u16, u32), + Variant4 { my_val: i32, my_struct: MyStruct, }, +} + +#[derive(BasicDescribe)] +pub struct MyTupleOf(pub T1, pub T2, pub T3); + +// Form1 and Form2 are equivalent types (ignoring naming of the first field) +// But to verify equivalency of A = Form1 and B = Form2 it has to verify: +// * Via the Opposite variant: (A.Form2, B.Form1), +// * Via the Me variant: (A.Form1, B.Form2) +// * Via the Form1 variant: (A.Form1, B.Form1) +// * If a Form2 variant existed, (A.Form2, B.Form2) +// This demonstrates that verifying equivalency of schemas is O(N^2). +#[derive(Sbor)] +enum MyMultiRecursiveTypeForm1 { + Nothing, + Opposite(Option>), + Me(Option>), + Form1(Option>), +} + +#[derive(Sbor)] +enum MyMultiRecursiveTypeForm2 { + None, + Opposite(Option>), + Me(Option>), + Form1(Option>), +} + +//============ +// BASIC TESTS +//============ +#[test] +#[should_panic] +fn asserting_backwards_compatibility_requires_a_named_schema() { + assert_type_backwards_compatibility::(|v| v) +} + +#[test] +fn asserting_backwards_compatibility_with_a_single_latest_schema_version_succeeds() { + assert_type_backwards_compatibility::(|v| v + .register_schema_of::("latest") + ) +} + +#[test] +#[should_panic] +fn asserting_backwards_compatibility_with_incorrect_latest_schema_version_succeeds() { + assert_type_backwards_compatibility::(|v| v + .register_schema_of::("latest") + ) +} + +#[test] +fn asserting_backwards_compatibility_with_two_identical_schema_versions_succeeds() { + assert_extension::(); +} + +#[test] +fn recursive_types_work() { + assert_extension::(); + assert_extension::(); + // Note that, ignoring names, A and B are equivalent types, so this should work! + assert_equality_ignoring_name_changes::(); + assert_equality_ignoring_name_changes::(); +} + +#[test] +fn generic_types_work() { + assert_equality_ignoring_name_changes::< + MyTupleOf, + (u8, MyStruct, MyMultiRecursiveTypeForm2), + >(); +} + +//======================================== +// TYPE INCOMPATIBILITY - STRUCTURAL TESTS +//======================================== +#[test] +#[should_panic(expected = "TypeKindMismatch")] +fn changing_type_fails() { + assert_equality::(); +} + +#[test] +#[should_panic(expected = "TupleFieldCountMismatch")] +fn adding_tuple_field_fails() { + assert_equality::(); +} + +#[test] +#[should_panic(expected = "EnumVariantFieldCountMismatch")] +fn adding_enum_variant_field_fails() { + assert_equality::(); +} + +#[test] +fn adding_variant_succeeds() { + assert_extension::(); +} + +#[test] +#[should_panic(expected = "EnumSupportedVariantsMismatch")] +fn adding_variant_fails_if_equality_is_required() { + assert_equality::(); +} + +#[test] +#[should_panic(expected = "TypeKindMismatch")] +fn internal_type_change_fails() { + assert_equality_ignoring_name_changes::< + MyTupleOf, + (u8, MyStruct, MyMultiRecursiveTypeForm2), + >(); +} + +#[test] +fn replacing_with_any_succeeds() { + // Note that extension requires that names are preserved, + // so using just assert_extension in the first case would fail + assert_extension_ignoring_name_changes::(); + // But if the base type has no names, it is fine to replace with a nameless Any + assert_extension::(); +} + +//======================================== +// TYPE INCOMPATIBILITY - METADATA TESTS +//======================================== +#[test] +#[should_panic(expected = "FieldNameChangeError")] +fn updating_struct_field_name_fails() { + assert_extension::(); +} + +#[test] +#[should_panic(expected = "TypeNameChangeError")] +fn updating_type_name_fails() { + assert_extension::(); +} + +#[test] +#[should_panic(expected = "EnumVariantNameChangeError")] +fn updating_variant_name_fails() { + assert_extension::(); +} + +#[test] +#[should_panic(expected = "EnumVariantFieldNameChangeError")] +fn updating_variant_field_name_fails() { + assert_extension::(); +} + +#[test] +fn all_name_changes_allowed_succeeds() { + assert_equality_ignoring_name_changes::(); + assert_equality_ignoring_name_changes::(); + assert_equality_ignoring_name_changes::(); + assert_equality_ignoring_name_changes::(); + assert_equality_ignoring_name_changes::< + (MyStruct, MyStruct, MyEnum, MyEnum), + (MyStructFieldRenamed, MyStructTypeRenamed, MyEnumVariantRenamed, MyEnumVariantFieldRenamed), + >(); +} + +//======================================== +// TYPE INCOMPATIBILITY - VALIDATION TESTS +//======================================== +#[test] +#[should_panic(expected = "TypeValidationChangeError")] +fn changing_length_fails() { + assert_extension::<[u8; 5], [u8; 10]>(); +} + +#[test] +fn extension_removing_length_validation_succeeds() { + assert_extension::<[u8; 5], Vec>(); +} + +#[test] +#[should_panic(expected = "TypeValidationChangeError")] +fn equality_removing_length_validation_fails() { + assert_equality::<[u8; 5], Vec>(); +} + +//=================================== +// INCOMPLETENESS TESTS +//=================================== + +#[test] +#[should_panic(expected = "TypeUnreachableFromRootInBaseSchema")] +fn base_schema_not_covered_by_root_types_fails() { + let mut base_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.add_named_root_type_and_descendents::("enum"); + aggregator.generate_named_types_schema() + }; + let compared_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.generate_named_types_schema() + }; + // Forget about a root type - this leaves the schema not fully covered. + base_schema.type_ids.swap_remove("enum"); + + assert_equality_multi(base_schema, compared_schema); +} + +#[test] +#[should_panic(expected = "TypeUnreachableFromRootInComparedSchema")] +fn compared_schema_not_covered_by_root_types_fails() { + let base_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.generate_named_types_schema() + }; + let mut compared_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.add_named_root_type_and_descendents::("enum"); + aggregator.generate_named_types_schema() + }; + // Forget about a root type - this leaves the schema not fully covered. + compared_schema.type_ids.swap_remove("enum"); + + // Note - the process starts by comparing "latest" with "current" + // (i.e. compared_schema against itself) - so we get both a + // TypeUnreachableFromRootInBaseSchema and a TypeUnreachableFromRootInComparedSchema + assert_equality_multi(base_schema, compared_schema); +} + +#[test] +#[should_panic(expected = "NamedRootTypeMissingInComparedSchema")] +fn removed_root_type_fails_comparison() { + let base_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.add_named_root_type_and_descendents::("enum"); + aggregator.generate_named_types_schema() + }; + let compared_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.generate_named_types_schema() + }; + + assert_extension_multi(base_schema, compared_schema); +} + +#[test] +fn under_extension_added_root_type_succeeds() { + let base_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.generate_named_types_schema() + }; + let compared_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.add_named_root_type_and_descendents::("enum"); + aggregator.generate_named_types_schema() + }; + + assert_extension_multi(base_schema, compared_schema); +} + +#[test] +#[should_panic(expected = "DisallowedNewRootTypeInComparedSchema")] +fn under_equality_added_root_type_fails() { + let base_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.generate_named_types_schema() + }; + let compared_schema = { + let mut aggregator = TypeAggregator::::new(); + aggregator.add_named_root_type_and_descendents::("struct"); + aggregator.add_named_root_type_and_descendents::("enum"); + aggregator.generate_named_types_schema() + }; + + assert_equality_multi(base_schema, compared_schema); +} \ No newline at end of file diff --git a/sbor/src/lib.rs b/sbor/src/lib.rs index 060f6f49d80..d57bfb20eee 100644 --- a/sbor/src/lib.rs +++ b/sbor/src/lib.rs @@ -19,6 +19,8 @@ pub mod decode; pub mod decoder; /// SBOR encode trait. pub mod encode; +/// Simpler traits specific to encodability/decodability against vec-based encoders/decoders +pub mod vec_traits; /// SBOR payload wrappers. /// These are new types around an encoded payload or sub-payload, with helper methods / traits implemented. /// They can be used as a more efficient wrapper a ScryptoValue if the content of that value is not needed. @@ -113,6 +115,7 @@ pub mod prelude { }; pub use crate::{define_single_versioned, define_versioned}; pub use crate::{Categorize, Decode, Encode, Sbor, SborEnum, SborTuple}; + pub use crate::vec_traits::*; pub use crate::{DecodeError, EncodeError}; } diff --git a/sbor/src/schema/mod.rs b/sbor/src/schema/mod.rs index 137dfe56cb1..69b0cad2c05 100644 --- a/sbor/src/schema/mod.rs +++ b/sbor/src/schema/mod.rs @@ -2,6 +2,7 @@ mod custom_traits; mod describe; mod macros; mod schema; +mod schema_comparison; mod schema_validation; mod type_aggregator; mod type_data; @@ -12,6 +13,7 @@ pub use custom_traits::*; pub use describe::*; pub(crate) use macros::*; pub use schema::*; +pub use schema_comparison::*; pub use schema_validation::*; pub use type_aggregator::*; pub use type_data::*; diff --git a/sbor/src/schema/schema_comparison.rs b/sbor/src/schema/schema_comparison.rs new file mode 100644 index 00000000000..45975642477 --- /dev/null +++ b/sbor/src/schema/schema_comparison.rs @@ -0,0 +1,1770 @@ +use basic_well_known_types::ANY_TYPE; + +use crate::internal_prelude::*; +use crate::schema::*; +use crate::traversal::AnnotatedSborAncestor; +use crate::traversal::AnnotatedSborAncestorContainer; +use crate::traversal::AnnotatedSborPartialLeaf; +use crate::traversal::MapEntryPart; +use crate::traversal::PathAnnotate; +use crate::BASIC_SBOR_V1_MAX_DEPTH; +use radix_rust::rust::fmt::Write; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SchemaComparisonSettings { + completeness: SchemaComparisonCompletenessSettings, + structure: SchemaComparisonStructureSettings, + metadata: SchemaComparisonMetadataSettings, + validation: SchemaComparisonValidationSettings, +} + +impl SchemaComparisonSettings { + /// A set of defaults intended to enforce effective equality of the schemas, + /// but with clear error messages if they diverge + pub const fn require_equality() -> Self { + Self { + completeness: SchemaComparisonCompletenessSettings::enforce_type_roots_cover_schema_disallow_new_root_types(), + structure: SchemaComparisonStructureSettings::equality(), + metadata: SchemaComparisonMetadataSettings::equality(), + validation: SchemaComparisonValidationSettings::equality(), + } + } + + /// A set of defaults intended to capture a pretty tight definition of structural extension. + /// + /// This captures that: + /// * Payloads which are valid/decodable against the old schema are valid against the new schema + /// * Programmatic SBOR JSON is unchanged (that is, type/field/variant names are also unchanged) + /// + /// Notably: + /// * Type roots can be added in the compared schema, but we check that the type roots + /// provided completely cover both schemas + /// * Types must be structurally identical on their intersection, except new enum variants can be added + /// * Type metadata (e.g. names) must be identical on their intersection + /// * Type validation must be equal or strictly weaker in the new schema + pub const fn allow_structural_extension() -> Self { + Self { + completeness: SchemaComparisonCompletenessSettings::enforce_type_roots_cover_schema_allow_new_root_types(), + structure: SchemaComparisonStructureSettings::allow_extension(), + metadata: SchemaComparisonMetadataSettings::equality(), + validation: SchemaComparisonValidationSettings::allow_weakening(), + } + } + + pub const fn completeness_settings(mut self, checks: SchemaComparisonCompletenessSettings) -> Self { + self.completeness = checks; + self + } + + pub const fn structure_settings(mut self, checks: SchemaComparisonStructureSettings) -> Self { + self.structure = checks; + self + } + + pub const fn metadata_settings(mut self, checks: SchemaComparisonMetadataSettings) -> Self { + self.metadata = checks; + self + } + + pub const fn validation_settings(mut self, checks: SchemaComparisonValidationSettings) -> Self { + self.validation = checks; + self + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub struct SchemaComparisonCompletenessSettings { + allow_root_unreachable_types_in_base_schema: bool, + allow_root_unreachable_types_in_compared_schema: bool, + /// This is only relevant in the "multiple named roots" mode + allow_compared_to_have_more_root_types: bool, +} + +impl SchemaComparisonCompletenessSettings { + pub const fn allow_type_roots_not_to_cover_schema() -> Self { + Self { + allow_root_unreachable_types_in_base_schema: true, + allow_root_unreachable_types_in_compared_schema: true, + allow_compared_to_have_more_root_types: true, + } + } + + pub const fn enforce_type_roots_cover_schema_allow_new_root_types() -> Self { + Self { + allow_root_unreachable_types_in_base_schema: false, + allow_root_unreachable_types_in_compared_schema: false, + allow_compared_to_have_more_root_types: true, + } + } + + pub const fn enforce_type_roots_cover_schema_disallow_new_root_types() -> Self { + Self { + allow_root_unreachable_types_in_base_schema: false, + allow_root_unreachable_types_in_compared_schema: false, + allow_compared_to_have_more_root_types: false, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub struct SchemaComparisonStructureSettings { + allow_new_enum_variants: bool, + allow_replacing_with_any: bool, +} + +impl SchemaComparisonStructureSettings { + pub const fn equality() -> Self { + Self { + allow_new_enum_variants: false, + allow_replacing_with_any: false, + } + } + + pub const fn allow_extension() -> Self { + Self { + allow_new_enum_variants: true, + allow_replacing_with_any: true, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SchemaComparisonMetadataSettings { + type_name_changes: NameChangeRule, + field_name_changes: NameChangeRule, + variant_name_changes: NameChangeRule, +} + +impl SchemaComparisonMetadataSettings { + pub const fn equality() -> Self { + Self { + type_name_changes: NameChangeRule::equality(), + field_name_changes: NameChangeRule::equality(), + variant_name_changes: NameChangeRule::equality(), + } + } + + pub const fn allow_adding_names() -> Self { + Self { + type_name_changes: NameChangeRule::AllowAddingNames, + field_name_changes: NameChangeRule::AllowAddingNames, + variant_name_changes: NameChangeRule::AllowAddingNames, + } + } + + pub const fn allow_all_changes() -> Self { + Self { + type_name_changes: NameChangeRule::AllowAllChanges, + field_name_changes: NameChangeRule::AllowAllChanges, + variant_name_changes: NameChangeRule::AllowAllChanges, + } + } + + fn checks_required(&self) -> bool { + let everything_allowed = self.type_name_changes == NameChangeRule::AllowAllChanges + && self.field_name_changes == NameChangeRule::AllowAllChanges + && self.variant_name_changes == NameChangeRule::AllowAllChanges; + !everything_allowed + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NameChangeRule { + DisallowAllChanges, + AllowAddingNames, + AllowAllChanges, +} + +impl NameChangeRule { + pub const fn equality() -> Self { + Self::DisallowAllChanges + } +} + +pub enum NameChange<'a> { + Unchanged, + NameAdded { new_name: &'a str }, + NameRemoved { old_name: &'a str, }, + NameChanged { old_name: &'a str, new_name: &'a str, }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NameChangeError { + change: OwnedNameChange, + rule_broken: NameChangeRule, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OwnedNameChange { + Unchanged, + NameAdded { new_name: String }, + NameRemoved { old_name: String }, + NameChanged { old_name: String, new_name: String, }, +} + +impl<'a> NameChange<'a> { + pub fn of_changed_option(from: Option<&'a str>, to: Option<&'a str>) -> Self { + match (from, to) { + (Some(old_name), Some(new_name)) if old_name == new_name => NameChange::Unchanged, + (Some(old_name), Some(new_name)) => NameChange::NameChanged { + old_name, + new_name, + }, + (Some(old_name), None) => NameChange::NameRemoved { old_name }, + (None, Some(new_name)) => NameChange::NameAdded { new_name }, + (None, None) => NameChange::Unchanged, + } + } + + pub fn validate(&self, rule: NameChangeRule) -> Result<(), NameChangeError> { + let passes = match (self, rule) { + (NameChange::Unchanged, _) => true, + (_, NameChangeRule::AllowAllChanges) => true, + (_, NameChangeRule::DisallowAllChanges) => false, + (NameChange::NameAdded { .. }, NameChangeRule::AllowAddingNames) => true, + (NameChange::NameRemoved { .. }, NameChangeRule::AllowAddingNames) => false, + (NameChange::NameChanged { .. }, NameChangeRule::AllowAddingNames) => false, + }; + if passes { + Ok(()) + } else { + Err(NameChangeError { + rule_broken: rule, + change: self.into_owned(), + }) + } + } + + fn into_owned(&self) -> OwnedNameChange { + match *self { + NameChange::Unchanged => OwnedNameChange::Unchanged, + NameChange::NameAdded { new_name } => OwnedNameChange::NameAdded { new_name: new_name.to_string() }, + NameChange::NameRemoved { old_name } => OwnedNameChange::NameRemoved { old_name: old_name.to_string() }, + NameChange::NameChanged { old_name, new_name } => OwnedNameChange::NameChanged { old_name: old_name.to_string(), new_name: new_name.to_string(), }, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SchemaComparisonValidationSettings { + allow_validation_weakening: bool, +} + +impl SchemaComparisonValidationSettings { + pub const fn equality() -> Self { + Self { + allow_validation_weakening: false, + } + } + + pub const fn allow_weakening() -> Self { + Self { + allow_validation_weakening: true, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ValidationChange { + Unchanged, + Strengthened, + Weakened, + Incomparable, +} + +impl ValidationChange { + pub fn combine(self, other: ValidationChange) -> Self { + match (self, other) { + (ValidationChange::Incomparable, _) => ValidationChange::Incomparable, + (_, ValidationChange::Incomparable) => ValidationChange::Incomparable, + (ValidationChange::Unchanged, other) => other, + (other, ValidationChange::Unchanged) => other, + (ValidationChange::Strengthened, ValidationChange::Strengthened) => ValidationChange::Strengthened, + (ValidationChange::Strengthened, ValidationChange::Weakened) => ValidationChange::Incomparable, + (ValidationChange::Weakened, ValidationChange::Strengthened) => ValidationChange::Incomparable, + (ValidationChange::Weakened, ValidationChange::Weakened) => ValidationChange::Weakened, + } + } +} + +#[must_use = "You must read / handle the comparison result"] +pub struct SchemaComparisonResult<'s, S: CustomSchema> { + base_schema: &'s Schema, + compared_schema: &'s Schema, + errors: Vec>, +} + +impl<'s, S: CustomSchema> SchemaComparisonResult<'s, S> { + pub fn is_valid(&self) -> bool { + self.errors.len() == 0 + } + + pub fn error_message(&self, base_schema_name: &str, compared_schema_name: &str) -> Option { + if self.errors.len() == 0 { + return None; + } + let mut output = String::new(); + writeln!( + &mut output, + "Schema comparison FAILED between base schema ({}) and compared schema ({}) with {} errors:", + base_schema_name, + compared_schema_name, + self.errors.len(), + ).unwrap(); + for error in &self.errors { + write!(&mut output, "- ").unwrap(); + error.write_against_schemas(&mut output, &self.base_schema, &self.compared_schema).unwrap(); + writeln!(&mut output).unwrap(); + } + Some(output) + } + + pub fn assert_valid(&self, base_schema_name: &str, compared_schema_name: &str) { + if let Some(error_message) = self.error_message(base_schema_name, compared_schema_name) { + panic!("{}", error_message); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SchemaComparisonError { + error_detail: SchemaComparisonErrorDetail, + example_location: Option, +} + +impl SchemaComparisonError { + fn write_against_schemas( + &self, + f: &mut F, + base_schema: &Schema, + compared_schema: &Schema, + ) -> core::fmt::Result { + if let Some(location) = &self.example_location { + write!( + f, + "{:?} under {} at type path ", + &self.error_detail, + location.root_type_identifier, + )?; + (location, base_schema, compared_schema).write_path(f)?; + } else { + write!(f, "{:?}", &self.error_detail)?; + } + Ok(()) + } +} + +fn combine_names(base_name: Option<&str>, compared_name: Option<&str>) -> Option { + match (base_name, compared_name) { + (Some(base_name), Some(compared_name)) if base_name == compared_name => Some(base_name.to_string()), + (Some(base_name), Some(compared_name)) => Some(format!("{base_name}|{compared_name}")), + (Some(base_name), None) => Some(format!("{base_name}|anon")), + (None, Some(compared_name)) => Some(format!("anon|{compared_name}")), + (None, None) => None, + } +} + +impl<'s, 'a, S: CustomSchema> PathAnnotate + for (&'a TypeFullPath, &'a Schema, &'a Schema) +{ + fn iter_ancestor_path(&self) -> Box> + '_> { + let (full_path, base_schema, compared_schema) = *self; + + let iterator = full_path.ancestor_path + .iter() + .map(|path_segment| { + let base_metadata = base_schema.resolve_type_metadata(path_segment.parent_base_type_id) + .expect("Invalid base schema - Could not find metadata for base type"); + let compared_metadata = compared_schema.resolve_type_metadata(path_segment.parent_compared_type_id) + .expect("Invalid compared schema - Could not find metadata for compared type"); + + let name = Cow::Owned(combine_names( + base_metadata.get_name(), + compared_metadata.get_name() + ).unwrap_or_else(|| { + combine_names( + Some(base_schema.resolve_type_kind(path_segment.parent_base_type_id).unwrap().category_name()), + Some(compared_schema.resolve_type_kind(path_segment.parent_compared_type_id).unwrap().category_name()), + ).unwrap() + })); + + let container = match path_segment.child_locator { + ChildTypeLocator::Tuple { field_index } => { + let field_name = combine_names( + base_metadata.get_field_name(field_index), + compared_metadata.get_field_name(field_index), + ).map(Cow::Owned); + AnnotatedSborAncestorContainer::Tuple { + field_index, + field_name, + } + }, + ChildTypeLocator::EnumVariant { + discriminator, + field_index, + } => { + let base_variant_metadata = base_metadata.get_enum_variant_data(discriminator).expect("Base schema has variant names"); + let compared_variant_metadata = compared_metadata.get_enum_variant_data(discriminator).expect("Compared schema has variant names"); + let variant_name = combine_names( + base_variant_metadata.get_name(), + compared_variant_metadata.get_name(), + ).map(Cow::Owned); + let field_name = combine_names( + base_variant_metadata.get_field_name(field_index), + compared_variant_metadata.get_field_name(field_index), + ).map(Cow::Owned); + AnnotatedSborAncestorContainer::EnumVariant { + discriminator, + variant_name, + field_index, + field_name, + } + }, + ChildTypeLocator::Array { } => { + AnnotatedSborAncestorContainer::Array { + index: None, + } + }, + ChildTypeLocator::Map { entry_part } => { + AnnotatedSborAncestorContainer::Map { + index: None, + entry_part, + } + }, + }; + + AnnotatedSborAncestor { + name, + container, + } + }); + + Box::new(iterator) + } + + fn annotated_leaf(&self) -> Option> { + let (full_path, base_schema, compared_schema) = *self; + let base_type_id = full_path.leaf_base_type_id; + let compared_type_id = full_path.leaf_compared_type_id; + + let base_metadata = base_schema.resolve_type_metadata(base_type_id) + .expect("Invalid base schema - Could not find metadata for base type"); + let compared_metadata = compared_schema.resolve_type_metadata(compared_type_id) + .expect("Invalid compared schema - Could not find metadata for compared type"); + + let name = Cow::Owned(combine_names( + base_metadata.get_name(), + compared_metadata.get_name() + ).unwrap_or_else(|| { + combine_names( + Some(base_schema.resolve_type_kind(base_type_id).unwrap().category_name()), + Some(compared_schema.resolve_type_kind(compared_type_id).unwrap().category_name()), + ).unwrap() + })); + + Some(AnnotatedSborPartialLeaf { + name, + partial_leaf_locator: None, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct SchemaComparisonPathSegment { + parent_base_type_id: LocalTypeId, + parent_compared_type_id: LocalTypeId, + child_locator: ChildTypeLocator, +} + +impl SchemaComparisonPathSegment { + pub fn of(parent_base_type_id: &LocalTypeId, parent_compared_type_id: &LocalTypeId, child_locator: ChildTypeLocator) -> Self { + Self { + parent_base_type_id: *parent_base_type_id, + parent_compared_type_id: *parent_compared_type_id, + child_locator, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SchemaComparisonErrorDetail { + // Type kind errors + TypeKindMismatch { + base: TypeKindLabel, + compared: TypeKindLabel, + }, + TupleFieldCountMismatch { + base_field_count: usize, + compared_field_count: usize, + }, + EnumSupportedVariantsMismatch { + base_variants_missing_in_compared: IndexSet, + compared_variants_missing_in_base: IndexSet, + }, + EnumVariantFieldCountMismatch { + base_field_count: usize, + compared_field_count: usize, + variant_discriminator: u8, + }, + // Type metadata errors + TypeNameChangeError(NameChangeError), + FieldNameChangeError { + error: NameChangeError, + field_index: usize, + }, + EnumVariantNameChangeError { + error: NameChangeError, + variant_discriminator: u8, + }, + EnumVariantFieldNameChangeError { + error: NameChangeError, + variant_discriminator: u8, + field_index: usize, + }, + // Type validation error + TypeValidationChangeError { + change: ValidationChange, + old: TypeValidation, + new: TypeValidation, + }, + // Completeness errors + NamedRootTypeMissingInComparedSchema { + root_type_name: String, + }, + DisallowedNewRootTypeInComparedSchema { + root_type_name: String, + }, + TypeUnreachableFromRootInBaseSchema { + local_type_index: usize, + type_name: Option, + }, + TypeUnreachableFromRootInComparedSchema { + local_type_index: usize, + type_name: Option, + }, +} + +struct TypeKindComparisonResult { + children_needing_checking: Vec<(ChildTypeLocator, LocalTypeId, LocalTypeId)>, + errors: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ChildTypeLocator { + Tuple { field_index: usize, }, + EnumVariant { discriminator: u8, field_index: usize, }, + Array { }, // Unlike values, we don't have an index + Map { entry_part: MapEntryPart, } // Unlike values, we don't have an index +} + +impl TypeKindComparisonResult { + fn new() -> Self { + Self { + children_needing_checking: vec![], + errors: vec![], + } + } + + fn add_error(&mut self, error: SchemaComparisonErrorDetail) { + self.errors.push(error) + } + + fn with_mismatch_error( + mut self, + base_type_kind: &TypeKind, LocalTypeId>, + compared_type_kind: &TypeKind, LocalTypeId>, + ) -> Self { + self.add_error(SchemaComparisonErrorDetail::TypeKindMismatch { + base: base_type_kind.label(), + compared: compared_type_kind.label(), + }); + self + } + + fn with_error( + mut self, + error: SchemaComparisonErrorDetail, + ) -> Self { + self.add_error(error); + self + } + + fn add_child_to_check(&mut self, child_locator: ChildTypeLocator, base_type_id: LocalTypeId, compared_type_id: LocalTypeId) { + self.children_needing_checking.push((child_locator, base_type_id, compared_type_id)); + } +} + +struct TypeMetadataComparisonResult { + errors: Vec>, +} + +impl TypeMetadataComparisonResult { + fn new() -> Self { + Self { errors: vec![] } + } + + fn add_error(&mut self, error: SchemaComparisonErrorDetail) { + self.errors.push(error) + } +} + +struct TypeValidationComparisonResult { + errors: Vec>, +} + +impl TypeValidationComparisonResult { + fn new() -> Self { + Self { errors: vec![] } + } + + fn add_error(&mut self, error: SchemaComparisonErrorDetail) { + self.errors.push(error) + } +} + +struct ErrorsAggregator { + errors: Vec>, +} + +impl ErrorsAggregator { + fn new() -> Self { + Self { errors: vec![] } + } + + fn record_error( + &mut self, + error_detail: SchemaComparisonErrorDetail, + example_location: &TypeAncestorPath, + base_type_id: LocalTypeId, + compared_type_id: LocalTypeId, + ) { + self.errors.push(SchemaComparisonError { + error_detail, + example_location: Some(TypeFullPath { + root_type_identifier: example_location.root_type_identifier.clone(), + ancestor_path: example_location.ancestor_path.clone(), + leaf_base_type_id: base_type_id, + leaf_compared_type_id: compared_type_id, + }), + }) + } + + fn record_error_with_unvisited_location( + &mut self, + error_detail: SchemaComparisonErrorDetail, + ) { + self.errors.push(SchemaComparisonError { + error_detail, + example_location: None, + }) + } +} + +struct SchemaComparisonKernel<'s, 'o, S: CustomSchema> { + base_schema: &'s Schema, + compared_schema: &'s Schema, + settings: &'o SchemaComparisonSettings, + /// A matrix tracking if two types have been compared shallowly + cached_located_type_comparisons: + NonIterMap<(LocalTypeId, LocalTypeId), LocatedTypeComparisonResult>, + /// A list of pending comparisons + pending_comparison_work_list: Vec, + /// Used to cheaply capture whether we've seen a local type, for completeness checking + base_local_types_reachable_from_a_root: NonIterMap, + /// Used to cheaply capture whether we've seen a local type, for completeness checking + compared_local_types_reachable_from_a_root: NonIterMap, + + /// Tracking all the errors discovered + errors: ErrorsAggregator, +} + +impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { + /// This assumes the schemas provided are valid, and can panic if they're not. + /// + /// NOTE: This is NOT designed to be used: + /// * In situations where the schemas are untrusted. + /// The worst case runtime performance here for malicious schemas is O((N + W)^2) + /// where N is the number of schema types and W is the number of well known types. + /// * In situations where performance matters. + /// Whilst the expected performance for normal schemas is O(N), this + /// isn't designed in a very optimal way (e.g. there are lots of allocations, some + /// cloning etc). + fn new( + base_schema: &'s Schema, + compared_schema: &'s Schema, + settings: &'o SchemaComparisonSettings, + ) -> Self { + Self { + base_schema, + compared_schema, + settings, + cached_located_type_comparisons: Default::default(), + pending_comparison_work_list: Default::default(), + base_local_types_reachable_from_a_root: Default::default(), + compared_local_types_reachable_from_a_root: Default::default(), + errors: ErrorsAggregator::new(), + } + } + + pub fn compare_using_fixed_type_roots( + mut self, + type_roots: &[ComparisonTypeRoot], + ) -> SchemaComparisonResult<'s, S> { + // NOTE: While providing 0 type_roots is typically an accident, it isn't technically incorrect. + // There are some auto-generated cases (e.g. an empty interface) where it may make sense / be easiest + // to check an empty list of type roots. + for ComparisonTypeRoot { + name, + base_type_id, + compared_type_id, + } in type_roots.iter() + { + self.deep_compare_root_types(name, base_type_id, compared_type_id); + self.mark_root_reachable_base_types(base_type_id); + self.mark_root_reachable_compared_types(compared_type_id); + } + + self.check_for_completeness(); + self.into_result() + } + + pub fn compare_using_named_type_roots( + mut self, + base_type_roots: &IndexMap, + compared_type_roots: &IndexMap, + ) -> SchemaComparisonResult<'s, S> { + // First, let's loop through the base types, and compare them against the corresponding compared types. + // It is an error for a base named type not to exist in the corresponding compared list. + for (base_root_type_name, base_type_id) in base_type_roots.iter() { + if let Some(compared_type_id) = compared_type_roots.get(base_root_type_name) { + self.deep_compare_root_types(base_root_type_name, base_type_id, compared_type_id); + self.mark_root_reachable_base_types(base_type_id); + self.mark_root_reachable_compared_types(compared_type_id); + } else { + self.errors.record_error_with_unvisited_location(SchemaComparisonErrorDetail::NamedRootTypeMissingInComparedSchema { + root_type_name: base_root_type_name.clone(), + }); + self.mark_root_reachable_base_types(base_type_id); + } + } + + // We now loop through the compared types not covered in the above loop over base types + for (compared_root_type_name, compared_type_id) in compared_type_roots.iter() { + if !base_type_roots.contains_key(compared_root_type_name) { + if !self.settings.completeness.allow_compared_to_have_more_root_types { + self.errors.record_error_with_unvisited_location(SchemaComparisonErrorDetail::DisallowedNewRootTypeInComparedSchema { + root_type_name: compared_root_type_name.clone(), + }); + } + self.mark_root_reachable_compared_types(compared_type_id); + } + } + + self.check_for_completeness(); + self.into_result() + } + + fn deep_compare_root_types( + &mut self, + root_type_identifier: &str, + base_type_id: &LocalTypeId, + compared_type_id: &LocalTypeId, + ) { + self.pending_comparison_work_list + .push(PendingComparisonRequest { + base_type_id: *base_type_id, + compared_type_id: *compared_type_id, + ancestor_path: TypeAncestorPath { + root_type_identifier: root_type_identifier.to_string(), + ancestor_path: vec![], + }, + }); + // Run all comparison analysis we can perform. + // Due to the cache of shallow results over (TypesInBase * TypesInCompared), this must end. + while let Some(request) = self.pending_comparison_work_list.pop() { + self.run_single_type_comparison(request); + } + } + + fn mark_root_reachable_base_types( + &mut self, + root_base_type_id: &LocalTypeId, + ) { + // Due to the cache, we do max O(TypesInBase) work. + // Note that reachability analysis needs to be performed separately to comparison analysis, because + // sometimes with comparisons of MyTuple(A) and MyTuple(B1, B2), we still want to perform reachability + // analysis on A, B1 and B2; but we can't make any sensible comparisons between them. + let LocalTypeId::SchemaLocalIndex(root_base_local_index) = root_base_type_id else { + return; + }; + let mut base_reachability_work_list = vec![*root_base_local_index]; + while let Some(base_type_index) = base_reachability_work_list.pop() { + match self.base_local_types_reachable_from_a_root.entry(base_type_index) { + hash_map::Entry::Occupied(_) => continue, + hash_map::Entry::Vacant(vacant_entry) => vacant_entry.insert(()), + }; + let type_id = LocalTypeId::SchemaLocalIndex(base_type_index); + let type_kind = self.base_schema.resolve_type_kind(type_id) + .unwrap_or_else(|| panic!("Invalid base schema - type kind for {type_id:?} not found")); + visit_type_kind_children(type_kind, |_child_locator, child_type_kind| { + if let LocalTypeId::SchemaLocalIndex(local_index) = child_type_kind { + base_reachability_work_list.push(local_index); + }; + }) + } + } + + fn mark_root_reachable_compared_types( + &mut self, + root_compared_type_id: &LocalTypeId, + ) { + let LocalTypeId::SchemaLocalIndex(root_compared_local_index) = root_compared_type_id else { + return; + }; + let mut compared_reachability_work_list = vec![*root_compared_local_index]; + while let Some(compared_local_index) = compared_reachability_work_list.pop() { + match self.compared_local_types_reachable_from_a_root.entry(compared_local_index) { + hash_map::Entry::Occupied(_) => continue, + hash_map::Entry::Vacant(vacant_entry) => vacant_entry.insert(()), + }; + let type_id = LocalTypeId::SchemaLocalIndex(compared_local_index); + let type_kind = self.compared_schema.resolve_type_kind(type_id) + .unwrap_or_else(|| panic!("Invalid compared schema - type kind for {type_id:?} not found")); + visit_type_kind_children(type_kind, |_child_locator, child_type_kind| { + if let LocalTypeId::SchemaLocalIndex(local_index) = child_type_kind { + compared_reachability_work_list.push(local_index); + }; + }) + } + } + + fn run_single_type_comparison(&mut self, request: PendingComparisonRequest) { + let PendingComparisonRequest { + base_type_id, + compared_type_id, + ancestor_path: example_location, + } = request; + let status_key = (base_type_id, compared_type_id); + + if self + .cached_located_type_comparisons + .contains_key(&status_key) + { + return; + } + + let result = self.compare_types_internal(&example_location, base_type_id, compared_type_id); + for (child_locator, child_base_type_id, child_compared_type_id) in result.child_checks_required { + if self + .cached_located_type_comparisons + .contains_key(&(child_base_type_id, child_compared_type_id)) + { + continue; + } + let child_example_location = TypeAncestorPath { + root_type_identifier: example_location.root_type_identifier.clone(), + ancestor_path: { + let mut path = example_location.ancestor_path.clone(); + path.push(SchemaComparisonPathSegment::of( + &base_type_id, + &compared_type_id, + child_locator, + )); + path + }, + }; + self.pending_comparison_work_list + .push(PendingComparisonRequest { + base_type_id: child_base_type_id, + compared_type_id: child_compared_type_id, + ancestor_path: child_example_location, + }) + } + let located_result = LocatedTypeComparisonResult { + shallow_status: result.shallow_status, + example_location, + }; + self.cached_located_type_comparisons + .insert(status_key, located_result); + } + + fn compare_types_internal( + &mut self, + example_location: &TypeAncestorPath, + base_type_id: LocalTypeId, + compared_type_id: LocalTypeId, + ) -> ShallowTypeComparisonResult { + // Quick short-circuit when comparing equal well-known types + match (base_type_id, compared_type_id) { + ( + LocalTypeId::WellKnown(base_well_known), + LocalTypeId::WellKnown(compared_well_known), + ) => { + if base_well_known == compared_well_known { + return ShallowTypeComparisonResult::no_child_checks_required( + TypeComparisonStatus::Pass, + ); + } + } + _ => {} + } + + // Load type data from each schema + let (base_type_kind, base_type_metadata, base_type_validation) = + self.base_schema.resolve_type_data(base_type_id) + .unwrap_or_else(|| panic!("Base schema was not valid - no type data for {base_type_id:?}")); + let (compared_type_kind, compared_type_metadata, compared_type_validation) = + self.compared_schema.resolve_type_data(compared_type_id) + .unwrap_or_else(|| panic!("Compared schema was not valid - no type data for {compared_type_id:?}")); + + // Type Kind Comparison + let further_checks_required = { + let TypeKindComparisonResult { + errors, + children_needing_checking, + } = self.compare_type_kind_internal(base_type_kind, compared_type_kind); + + if errors.len() > 0 { + for error in errors { + self.errors.record_error( + error, + example_location, + base_type_id, + compared_type_id, + ); + } + // If the type kind comparison fails, then the metadata and validation comparisons aren't helpful information, + // so we can abort here without further tests. + return ShallowTypeComparisonResult { + shallow_status: TypeComparisonStatus::Failure, + child_checks_required: children_needing_checking, + }; + } + + children_needing_checking + }; + + let mut error_recorded = false; + + // Type Metadata Comparison + { + let TypeMetadataComparisonResult { errors } = + self.compare_type_metadata_internal( + base_type_kind, + base_type_metadata, + compared_type_metadata, + ); + + for error in errors { + error_recorded = true; + self.errors.record_error( + error, + example_location, + base_type_id, + compared_type_id, + ); + } + } + + // Type Validation Comparison + { + let TypeValidationComparisonResult { errors } = self + .compare_type_validation_internal(base_type_validation, compared_type_validation); + + for error in errors { + error_recorded = true; + self.errors.record_error( + error, + example_location, + base_type_id, + compared_type_id, + ); + } + } + + return ShallowTypeComparisonResult { + shallow_status: if error_recorded { + TypeComparisonStatus::Failure + } else { + TypeComparisonStatus::Pass + }, + child_checks_required: further_checks_required, + }; + } + + fn compare_type_kind_internal( + &self, + base_type_kind: &TypeKind, LocalTypeId>, + compared_type_kind: &TypeKind, LocalTypeId>, + ) -> TypeKindComparisonResult { + // The returned children to check should be driven from the base type kind, + // because these are the children where we have to maintain backwards-compatibility + + let mut result = TypeKindComparisonResult::new(); + let settings = self.settings.structure; + if *compared_type_kind == TypeKind::Any + && *base_type_kind != TypeKind::Any + && settings.allow_replacing_with_any + { + // If we allow replacing any type with TypeKind::Any, and the new schema is Any, then the check is valid. + // + // That said, we should still check any children against Any: + // * In case they fail other checks (e.g. ancestor types on the base side required particular type names, + // which have now disappeared because the Compared side is Any) + // * To ensure we pass completeness checks on the base side + visit_type_kind_children(&base_type_kind, |child_type_locator, child_type_kind| { + result.add_child_to_check(child_type_locator, child_type_kind, LocalTypeId::WellKnown(ANY_TYPE)); + }); + return result; + } + + match base_type_kind { + TypeKind::Any + | TypeKind::Bool + | TypeKind::I8 + | TypeKind::I16 + | TypeKind::I32 + | TypeKind::I64 + | TypeKind::I128 + | TypeKind::U8 + | TypeKind::U16 + | TypeKind::U32 + | TypeKind::U64 + | TypeKind::U128 + | TypeKind::String => { + if compared_type_kind != base_type_kind { + return result.with_mismatch_error( + base_type_kind, + compared_type_kind, + ); + } + } + TypeKind::Array { + element_type: base_element_type, + } => { + let TypeKind::Array { + element_type: compared_element_type, + } = compared_type_kind + else { + return result.with_mismatch_error( + base_type_kind, + compared_type_kind, + ); + }; + result.add_child_to_check( + ChildTypeLocator::Array { }, + *base_element_type, + *compared_element_type, + ); + } + TypeKind::Tuple { + field_types: base_field_types, + } => { + let TypeKind::Tuple { + field_types: compared_field_types, + } = compared_type_kind + else { + return result.with_mismatch_error( + base_type_kind, + compared_type_kind, + ); + }; + if base_field_types.len() != compared_field_types.len() { + return result.with_error( + SchemaComparisonErrorDetail::TupleFieldCountMismatch { + base_field_count: base_field_types.len(), + compared_field_count: compared_field_types.len(), + }, + ); + } + let matched_field_types = base_field_types + .iter() + .cloned() + .zip(compared_field_types.iter().cloned()) + .enumerate(); + for (field_index, (base, compared)) in matched_field_types { + result.add_child_to_check(ChildTypeLocator::Tuple { field_index }, base, compared); + } + } + TypeKind::Enum { + variants: base_variants, + } => { + let TypeKind::Enum { + variants: compared_variants, + } = compared_type_kind + else { + return result.with_mismatch_error( + base_type_kind, + compared_type_kind, + ); + }; + + let base_variants_missing_in_compared: IndexSet<_> = base_variants + .keys() + .filter(|base_variant_id| !compared_variants.contains_key(*base_variant_id)) + .cloned() + .collect(); + let compared_variants_missing_in_base: IndexSet<_> = compared_variants + .keys() + .filter(|compared_variant_id| !base_variants.contains_key(*compared_variant_id)) + .cloned() + .collect(); + + if base_variants_missing_in_compared.len() > 0 || (compared_variants_missing_in_base.len() > 0 && !settings.allow_new_enum_variants){ + result.add_error(SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { + base_variants_missing_in_compared, + compared_variants_missing_in_base, + }); + } + + for (discriminator, base_field_type_ids) in base_variants.iter() { + let Some(compared_field_type_ids) = compared_variants.get(discriminator) else { + // We have already output a EnumSupportedVariantsMismatch error above for this. + // But let's continue to see if we can match / compare further variants structurally, + // to get as many errors as we can. + continue; + }; + let discriminator = *discriminator; + + if base_field_type_ids.len() != compared_field_type_ids.len() { + result.add_error(SchemaComparisonErrorDetail::EnumVariantFieldCountMismatch { + variant_discriminator: discriminator, + base_field_count: base_field_type_ids.len(), + compared_field_count: compared_field_type_ids.len(), + }); + } else { + let paired_child_ids = base_field_type_ids + .iter() + .zip(compared_field_type_ids.iter()) + .enumerate(); + for (field_index, (base_child_type_id, compared_child_type_id)) in paired_child_ids { + result.add_child_to_check( + ChildTypeLocator::EnumVariant { discriminator, field_index, }, + *base_child_type_id, + *compared_child_type_id + ); + } + } + } + } + TypeKind::Map { + key_type: base_key_type, + value_type: base_value_type, + } => { + let TypeKind::Map { + key_type: compared_key_type, + value_type: compared_value_type, + } = compared_type_kind + else { + return result.with_mismatch_error( + base_type_kind, + compared_type_kind, + ); + }; + + result.add_child_to_check(ChildTypeLocator::Map { entry_part: MapEntryPart::Key }, *base_key_type, *compared_key_type); + result.add_child_to_check(ChildTypeLocator::Map { entry_part: MapEntryPart::Value }, *base_value_type, *compared_value_type); + } + // Assume for now that custom types are leaf types. + // Therefore we can directly run equality on the types, like the simple types. + TypeKind::Custom(_) => { + if compared_type_kind != base_type_kind { + return result.with_mismatch_error( + base_type_kind, + compared_type_kind, + ); + } + } + } + + result + } + + fn compare_type_metadata_internal( + &self, + base_type_kind: &TypeKind, LocalTypeId>, + base_type_metadata: &TypeMetadata, + compared_type_metadata: &TypeMetadata, + ) -> TypeMetadataComparisonResult { + let settings = self.settings.metadata; + let mut result = TypeMetadataComparisonResult::new(); + if !settings.checks_required() { + return result; + } + if let Err(error) = NameChange::of_changed_option( + base_type_metadata.type_name.as_deref(), + compared_type_metadata.type_name.as_deref(), + ).validate(settings.type_name_changes) { + result.add_error(SchemaComparisonErrorDetail::TypeNameChangeError(error)); + } + + // NOTE: For these tests, we assume that the schema is valid - that is, that the type metadata + // aligns with the underlying type kinds. + // Also, we have already tested for consistency of the compared type kind against the base type kind. + // So we can drive field/variant metadata iteration off the base type kind. + match base_type_kind { + TypeKind::Tuple { field_types } => { + for field_index in 0..field_types.len() { + if let Err(error) = NameChange::of_changed_option( + base_type_metadata.get_field_name(field_index), + compared_type_metadata.get_field_name(field_index), + ).validate(settings.field_name_changes) { + result.add_error(SchemaComparisonErrorDetail::FieldNameChangeError { + field_index, + error, + }); + } + } + }, + TypeKind::Enum { variants } => { + for (variant_discriminator, base_variant_types) in variants.iter() { + let variant_discriminator = *variant_discriminator; + let base_variant_metadata = base_type_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Base schema was not valid - base did not have enum child names for an enum variant"); + let compared_variant_metadata = compared_type_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Compared schema was not valid - base and compared agreed on structural equality of an enum, but compared did not have variant metadata for a base variant"); + + if let Err(error) = NameChange::of_changed_option( + base_variant_metadata.type_name.as_deref(), + compared_variant_metadata.type_name.as_deref(), + ).validate(settings.field_name_changes) { + result.add_error(SchemaComparisonErrorDetail::EnumVariantNameChangeError { + variant_discriminator, + error, + }); + } + + for field_index in 0..base_variant_types.len() { + if let Err(error) = NameChange::of_changed_option( + base_variant_metadata.get_field_name(field_index), + compared_variant_metadata.get_field_name(field_index), + ).validate(settings.field_name_changes) { + result.add_error(SchemaComparisonErrorDetail::EnumVariantFieldNameChangeError { + variant_discriminator, + field_index, + error, + }); + } + + } + } + }, + _ => { + // We can assume the schema is valid, therefore the only valid value is ChildNames::None + // So validation passes trivially + } + } + + result + } + + fn compare_type_validation_internal( + &self, + base_type_validation: &TypeValidation, + compared_type_validation: &TypeValidation, + ) -> TypeValidationComparisonResult { + let settings = self.settings.validation; + let mut result = TypeValidationComparisonResult::new(); + + let validation_change = match (base_type_validation, compared_type_validation) { + (TypeValidation::None, TypeValidation::None) => ValidationChange::Unchanged, + // Strictly a provided validation might be equivalent to None, for example: + // (for example NumericValidation { min: None, max: None } or NumericValidation:: { min: 0, max: 255 }) + // but for now assume that it's different + (_, TypeValidation::None) => ValidationChange::Weakened, + (TypeValidation::None, _) => ValidationChange::Strengthened, + // Now test equal validations + (TypeValidation::I8(base), TypeValidation::I8(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::I16(base), TypeValidation::I16(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::I32(base), TypeValidation::I32(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::I64(base), TypeValidation::I64(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::I128(base), TypeValidation::I128(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::U8(base), TypeValidation::U8(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::U16(base), TypeValidation::U16(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::U32(base), TypeValidation::U32(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::U64(base), TypeValidation::U64(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::U128(base), TypeValidation::U128(compared)) => NumericValidation::compare(base, compared), + (TypeValidation::String(base), TypeValidation::String(compared)) => LengthValidation::compare(base, compared), + (TypeValidation::Array(base), TypeValidation::Array(compared)) => LengthValidation::compare(base, compared), + (TypeValidation::Map(base), TypeValidation::Map(compared)) => LengthValidation::compare(base, compared), + (TypeValidation::Custom(base), TypeValidation::Custom(compared)) => { + <::CustomTypeValidation as CustomTypeValidation>::compare(base, compared) + }, + // Otherwise assume they are incomparable + _ => ValidationChange::Incomparable, + }; + let is_valid = match validation_change { + ValidationChange::Unchanged => true, + ValidationChange::Strengthened => false, + ValidationChange::Weakened => settings.allow_validation_weakening, + ValidationChange::Incomparable => false, + }; + if !is_valid { + result.add_error(SchemaComparisonErrorDetail::TypeValidationChangeError { + change: validation_change, + old: base_type_validation.clone(), + new: compared_type_validation.clone(), + }) + } + result + } + + fn check_for_completeness(&mut self) { + if !self.settings.completeness.allow_root_unreachable_types_in_base_schema { + if self.base_local_types_reachable_from_a_root.len() < self.base_schema.type_metadata.len() { + for (local_type_index, metadata) in self.base_schema.type_metadata.iter().enumerate() { + if !self.base_local_types_reachable_from_a_root.contains_key(&local_type_index) { + let type_name = metadata.type_name.as_ref().map(|n| n.clone().into_owned()); + self.errors.record_error_with_unvisited_location( + SchemaComparisonErrorDetail::TypeUnreachableFromRootInBaseSchema { + local_type_index, + type_name, + } + ) + } + } + } + } + if !self.settings.completeness.allow_root_unreachable_types_in_compared_schema { + if self.compared_local_types_reachable_from_a_root.len() < self.compared_schema.type_metadata.len() { + for (local_type_index, metadata) in self.compared_schema.type_metadata.iter().enumerate() { + if !self.compared_local_types_reachable_from_a_root.contains_key(&local_type_index) { + let type_name = metadata.type_name.as_ref().map(|n| n.clone().into_owned()); + self.errors.record_error_with_unvisited_location( + SchemaComparisonErrorDetail::TypeUnreachableFromRootInComparedSchema { + local_type_index, + type_name, + } + ) + } + } + } + } + } + + fn into_result(self) -> SchemaComparisonResult<'s, S> { + SchemaComparisonResult { + base_schema: self.base_schema, + compared_schema: self.compared_schema, + errors: self.errors.errors, + } + } +} + +fn visit_type_kind_children>( + type_kind: &TypeKind, + mut visitor: impl FnMut(ChildTypeLocator, LocalTypeId), +) { + return match type_kind { + TypeKind::Any + | TypeKind::Bool + | TypeKind::I8 + | TypeKind::I16 + | TypeKind::I32 + | TypeKind::I64 + | TypeKind::I128 + | TypeKind::U8 + | TypeKind::U16 + | TypeKind::U32 + | TypeKind::U64 + | TypeKind::U128 + | TypeKind::String => {}, + TypeKind::Array { element_type } => { + visitor(ChildTypeLocator::Array {}, *element_type); + } + TypeKind::Tuple { field_types } => { + for (field_index, field_type) in field_types.iter().enumerate() { + visitor(ChildTypeLocator::Tuple { field_index }, *field_type) + } + } + TypeKind::Enum { variants } => { + for (discriminator, field_types) in variants { + for (field_index, field_type) in field_types.iter().enumerate() { + visitor( + ChildTypeLocator::EnumVariant { discriminator: *discriminator, field_index, }, + *field_type, + ) + } + } + } + TypeKind::Map { + key_type, + value_type, + } => { + visitor(ChildTypeLocator::Map { entry_part: MapEntryPart::Key }, *key_type); + visitor(ChildTypeLocator::Map { entry_part: MapEntryPart::Value }, *value_type); + } + // At present, assume that custom types are leaf types. + TypeKind::Custom(_) => {}, + }; +} + + +#[derive(Debug, Clone, PartialEq, Eq)] +struct PendingComparisonRequest { + base_type_id: LocalTypeId, + compared_type_id: LocalTypeId, + ancestor_path: TypeAncestorPath, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TypeAncestorPath { + root_type_identifier: String, + ancestor_path: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TypeFullPath { + root_type_identifier: String, + ancestor_path: Vec, + leaf_base_type_id: LocalTypeId, + leaf_compared_type_id: LocalTypeId, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct LocatedTypeComparisonResult { + shallow_status: TypeComparisonStatus, + example_location: TypeAncestorPath, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ShallowTypeComparisonResult { + shallow_status: TypeComparisonStatus, + child_checks_required: Vec<(ChildTypeLocator, LocalTypeId, LocalTypeId)>, +} + +impl ShallowTypeComparisonResult { + pub fn no_child_checks_required(status: TypeComparisonStatus) -> Self { + Self { + shallow_status: status, + child_checks_required: vec![], + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum TypeComparisonStatus { + Pass, + Failure, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ComparisonTypeRoot { + name: String, + base_type_id: LocalTypeId, + compared_type_id: LocalTypeId, +} + +pub struct NamedSchemaVersions> { + ordered_versions: IndexMap, + extension: PhantomData, +} + +impl> NamedSchemaVersions { + pub fn new() -> Self { + Self { + ordered_versions: Default::default(), + extension: Default::default(), + } + } + + pub fn register_version( + mut self, + name: impl AsRef, + version: impl IntoSchema, + ) -> Self { + self.ordered_versions.insert(name.as_ref().to_string(), version.into_schema()); + self + } + + pub fn get_versions(&self) -> &IndexMap { + &self.ordered_versions + } +} + +/// Designed for ensuring a type is only altered in ways which ensure +/// backwards compatibility in SBOR serialization (i.e. that old payloads +/// can be deserialized correctly by the latest type). +/// +/// This function: +/// * Checks that the type's current schema is equal to the latest version +/// * Checks that each schema is consistent with the previous schema - but +/// can be an extension (e.g. enums can have new variants) +/// +/// The indexmap should be a map from a version name to a hex-encoded +/// basic-sbor-encoded `SingleTypeSchemaVersion` (with a single type). +/// The version name is only used for a more useful message on error. +/// +/// ## Example usage +/// ```no_run +/// # use radix_rust::prelude::*; +/// # use sbor::NoCustomSchema; +/// # use sbor::SchemaComparison::*; +/// # type ScryptoCustomSchema = NoCustomSchema; +/// # struct MyType; +/// assert_type_backwards_compatibility::( +/// SchemaComparisonSettings::allow_structural_extension(), +/// indexmap! { +/// "babylon_launch" => "5b...", +/// "bottlenose" => "5b...", +/// } +/// ); +/// ``` +/// ## Setup +/// To generate the encoded schema, just run the method with an empty `indexmap!` +/// and the assertion will include the encoded schemas, for copying into the assertion. +/// +/// ``` +pub fn assert_type_backwards_compatibility< + E: CustomExtension, + T: Describe<::CustomTypeKind>, +>( + versions_builder: impl FnOnce(NamedSchemaVersions>) -> NamedSchemaVersions>, +) +where + SingleTypeSchema: ComparisonSchema, +{ + assert_type_compatibility::( + &SchemaComparisonSettings::allow_structural_extension(), + versions_builder, + ) +} + +pub fn assert_type_compatibility< + E: CustomExtension, + T: Describe<::CustomTypeKind>, +>( + comparison_settings: &SchemaComparisonSettings, + versions_builder: impl FnOnce(NamedSchemaVersions>) -> NamedSchemaVersions>, +) +where + SingleTypeSchema: ComparisonSchema, +{ + let current = { + let (type_id, schema) = generate_full_schema_from_single_type::(); + SingleTypeSchema { schema, type_id } + }; + assert_schema_compatibility(comparison_settings, ¤t, &versions_builder(NamedSchemaVersions::new())) +} + +pub fn assert_type_collection_backwards_compatibility< + E: CustomExtension +>( + comparison_settings: &SchemaComparisonSettings, + current: NamedTypesSchema, + versions_builder: impl FnOnce(NamedSchemaVersions>) -> NamedSchemaVersions>, +) +where + NamedTypesSchema: ComparisonSchema, +{ + assert_schema_compatibility(comparison_settings, ¤t, &versions_builder(NamedSchemaVersions::new())) +} + +fn assert_schema_compatibility< + E: CustomExtension, + C: ComparisonSchema, +>( + schema_comparison_settings: &SchemaComparisonSettings, + current: &C, + named_versions: &NamedSchemaVersions, +) { + let named_versions = named_versions.get_versions(); + + // Part 0 - Check that there is at least one named_historic_schema_versions, + // if not, output latest encoded. + let Some((latest_version_name, latest_schema_version)) = named_versions.last() else { + let mut error = String::new(); + writeln!(&mut error, "You must provide at least one named versioned schema to use this method.").unwrap(); + writeln!(&mut error, "Use a relevant name (for example, the current software version name), and save the current schema as follows:").unwrap(); + writeln!(&mut error, "{}", current.encode_to_hex()).unwrap(); + panic!("{error}"); + }; + + // Part 1 - Check that latest is equal to the last historic schema version + let result = latest_schema_version + .compare_with(¤t, &SchemaComparisonSettings::require_equality()); + + if let Some(error_message) = result.error_message(latest_version_name, "current") { + let mut error = String::new(); + writeln!(&mut error, "The most recent named version ({latest_version_name}) DOES NOT MATCH the current version.").unwrap(); + writeln!(&mut error).unwrap(); + write!(&mut error, "{error_message}").unwrap(); + writeln!(&mut error).unwrap(); + writeln!(&mut error, "You will either want to:").unwrap(); + writeln!(&mut error, "(A) Add a new named version to the list, to be supported going forward.").unwrap(); + writeln!(&mut error, "(B) Replace the latest version. ONLY do this if the version has not yet been in use.").unwrap(); + writeln!(&mut error).unwrap(); + writeln!(&mut error, "The latest version is:").unwrap(); + writeln!(&mut error, "{}", current.encode_to_hex()).unwrap(); + panic!("{error}"); + } + + // Part 2 - Check that (N, N + 1) schemas respect the comparison settings, pairwise + for i in 0..named_versions.len() - 1 { + let (previous_version_name, previous_schema) = named_versions.get_index(i).unwrap(); + let (next_version_name, next_schema) = named_versions.get_index(i + 1).unwrap(); + + previous_schema + .compare_with(next_schema, schema_comparison_settings) + .assert_valid(previous_version_name, &next_version_name); + } +} + + +/// A serializable record of the schema of a single type. +/// Intended for historical backwards compatibility checking of a single type. +#[derive(Debug, Clone, Sbor)] +#[sbor(child_types = "S::CustomTypeKind, S::CustomTypeValidation")] +pub struct SingleTypeSchema { + pub schema: VersionedSchema, + pub type_id: LocalTypeId, +} + +impl SingleTypeSchema { + pub fn new(schema: VersionedSchema, type_id: LocalTypeId) -> Self { + Self { + schema, + type_id, + } + } + + pub fn from> + ?Sized>() -> Self { + generate_single_type_schema::() + } +} + +impl ComparisonSchema for SingleTypeSchema + where + ::CustomTypeKind: VecSbor, + ::CustomTypeValidation: VecSbor, +{ + fn compare_with<'s>(&'s self, compared: &'s Self, settings: &SchemaComparisonSettings) -> SchemaComparisonResult<'s, E::CustomSchema> { + SchemaComparisonKernel::new( + &self.schema.as_unique_version(), + &compared.schema.as_unique_version(), + settings, + ) + .compare_using_fixed_type_roots(&[ComparisonTypeRoot { + name: "root".to_string(), + base_type_id: self.type_id, + compared_type_id: compared.type_id, + }]) + } +} + +impl IntoSchema for SingleTypeSchema + where Self: ComparisonSchema +{ + fn into_schema(&self) -> Self { + self.clone() + } +} + +/// A serializable record of the schema of a set of named types. +/// Intended for historical backwards compatibility of a collection +/// of types in a single schema. +/// +/// For example, traits, or blueprint interfaces. +#[derive(Debug, Clone, Sbor)] +#[sbor(child_types = "S::CustomTypeKind, S::CustomTypeValidation")] +pub struct NamedTypesSchema { + pub schema: VersionedSchema, + pub type_ids: IndexMap, +} + +impl NamedTypesSchema { + pub fn new(schema: VersionedSchema, type_ids: IndexMap) -> Self { + Self { + schema, + type_ids, + } + } + + pub fn from(aggregator: TypeAggregator>) -> Self { + aggregator.generate_named_types_schema::() + } +} + +impl ComparisonSchema for NamedTypesSchema + where + ::CustomTypeKind: VecSbor, + ::CustomTypeValidation: VecSbor, +{ + fn compare_with<'s>(&'s self, compared: &'s Self, settings: &SchemaComparisonSettings) -> SchemaComparisonResult<'s, E::CustomSchema> { + SchemaComparisonKernel::new( + &self.schema.as_unique_version(), + &compared.schema.as_unique_version(), + settings, + ).compare_using_named_type_roots( + &self.type_ids, + &compared.type_ids, + ) + } +} + +impl IntoSchema for NamedTypesSchema + where Self: ComparisonSchema +{ + fn into_schema(&self) -> Self { + self.clone() + } +} + +// Marker trait +pub trait ComparisonSchema: Clone + VecSbor { + fn encode_to_bytes(&self) -> Vec { + vec_encode::(self, BASIC_SBOR_V1_MAX_DEPTH).unwrap() + } + + fn encode_to_hex(&self) -> String { + hex::encode(&self.encode_to_bytes()) + } + + fn decode_from_bytes(bytes: &[u8]) -> Self { + vec_decode::(bytes, BASIC_SBOR_V1_MAX_DEPTH).unwrap() + } + + fn decode_from_hex(hex: &str) -> Self { + Self::decode_from_bytes(&hex::decode(hex).unwrap()) + } + + fn compare_with<'s>(&'s self, compared: &'s Self, settings: &SchemaComparisonSettings) -> SchemaComparisonResult<'s, E::CustomSchema>; +} + +pub trait IntoSchema, E: CustomExtension> { + fn into_schema(&self) -> C; +} + +impl<'a, C: ComparisonSchema, E: CustomExtension, T: IntoSchema + ?Sized> IntoSchema for &'a T { + fn into_schema(&self) -> C { + >::into_schema(*self) + } +} + +impl, E: CustomExtension> IntoSchema for [u8] { + fn into_schema(&self) -> C { + C::decode_from_bytes(self) + } +} + +impl, E: CustomExtension> IntoSchema for Vec { + fn into_schema(&self) -> C { + C::decode_from_bytes(self) + } +} + +impl, E: CustomExtension> IntoSchema for String { + fn into_schema(&self) -> C { + C::decode_from_hex(self) + } +} + +impl, E: CustomExtension> IntoSchema for str { + fn into_schema(&self) -> C { + C::decode_from_hex(self) + } +} + +// NOTE: Types are in sbor-tests/tests/schema_comparison.rs \ No newline at end of file diff --git a/sbor/src/schema/type_aggregator.rs b/sbor/src/schema/type_aggregator.rs index fae7db35503..3ea538d4b1c 100644 --- a/sbor/src/schema/type_aggregator.rs +++ b/sbor/src/schema/type_aggregator.rs @@ -1,8 +1,9 @@ use super::*; +use indexmap::IndexMap; use sbor::rust::prelude::*; pub fn generate_full_schema_from_single_type< - T: Describe>, + T: Describe> + ?Sized, S: CustomSchema, >() -> (LocalTypeId, VersionedSchema) { let mut aggregator = TypeAggregator::new(); @@ -10,16 +11,34 @@ pub fn generate_full_schema_from_single_type< (type_id, generate_full_schema(aggregator)) } +pub fn generate_single_type_schema< + T: Describe> + ?Sized, + S: CustomSchema, +>() -> SingleTypeSchema { + let (type_id, schema) = generate_full_schema_from_single_type::(); + SingleTypeSchema::new(schema, type_id) +} + +/// You may wish to use the newer `schema.generate_named_types_schema_version()` +/// which, in tandom with `add_named_root_type_and_descendents` +/// also captures named root types to give more structure to enable schema +/// comparisons over time. pub fn generate_full_schema( aggregator: TypeAggregator>, ) -> VersionedSchema { - let type_count = aggregator.types.len(); - let type_indices = IndexSet::from_iter(aggregator.types.keys().map(|k| k.clone())); + generate_schema_from_types(aggregator.types) +} + +fn generate_schema_from_types( + types: IndexMap, RustTypeId>> +) -> VersionedSchema { + let type_count = types.len(); + let type_indices = IndexSet::from_iter(types.keys().map(|k| k.clone())); let mut type_kinds = Vec::with_capacity(type_count); let mut type_metadata = Vec::with_capacity(type_count); let mut type_validations = Vec::with_capacity(type_count); - for (_type_hash, type_data) in aggregator.types { + for (_type_hash, type_data) in types { type_kinds.push(linearize::(type_data.kind, &type_indices)); type_metadata.push(type_data.metadata); type_validations.push(type_data.validation); @@ -129,6 +148,7 @@ fn resolve_index(type_indices: &IndexSet, type_hash: &TypeHash) -> usi pub struct TypeAggregator> { already_read_dependencies: IndexSet, + named_root_types: IndexMap, types: IndexMap>, } @@ -136,12 +156,24 @@ impl> TypeAggregator { pub fn new() -> Self { Self { already_read_dependencies: index_set_new(), + named_root_types: IndexMap::default(), types: IndexMap::default(), } } + /// Adds the type (and its dependencies) to the `TypeAggregator`. + /// Also tracks it as a named root type, which can be used e.g. in schema comparisons. + /// + /// This is only intended for use when adding root types to schemas, + /// /and should not be called from inside Describe macros. + pub fn add_named_root_type_and_descendents + ?Sized>(&mut self, name: impl Into) -> LocalTypeId { + let local_type_id = self.add_child_type_and_descendents::(); + self.named_root_types.insert(name.into(), local_type_id); + local_type_id + } + /// Adds the dependent type (and its dependencies) to the `TypeAggregator`. - pub fn add_child_type_and_descendents>(&mut self) -> LocalTypeId { + pub fn add_child_type_and_descendents + ?Sized>(&mut self) -> LocalTypeId { let schema_type_id = self.add_child_type(T::TYPE_ID, || T::type_data()); self.add_schema_descendents::(); schema_type_id @@ -188,7 +220,7 @@ impl> TypeAggregator { /// [`add_child_type`]: #method.add_child_type /// [`add_schema_descendents`]: #method.add_schema_descendents /// [`add_child_type_and_descendents`]: #method.add_child_type_and_descendents - pub fn add_schema_descendents>(&mut self) -> bool { + pub fn add_schema_descendents + ?Sized>(&mut self) -> bool { let RustTypeId::Novel(complex_type_hash) = T::TYPE_ID else { return false; }; @@ -203,4 +235,12 @@ impl> TypeAggregator { return true; } + + pub fn generate_named_types_schema = C>>(self) + -> NamedTypesSchema { + NamedTypesSchema::new( + generate_schema_from_types(self.types), + self.named_root_types, + ) + } } diff --git a/sbor/src/vec_traits.rs b/sbor/src/vec_traits.rs new file mode 100644 index 00000000000..c9dd6b80aa8 --- /dev/null +++ b/sbor/src/vec_traits.rs @@ -0,0 +1,33 @@ +use crate::{internal_prelude::*, CustomExtension, CustomSchema, Decoder as _, Encoder as _, Describe, VecDecoder, VecEncoder}; + +pub trait VecEncode: for<'a> Encode> {} +impl Encode> + ?Sized> VecEncode for T {} + +pub fn vec_encode + ?Sized>(value: &T, max_depth: usize) -> Result, EncodeError> { + let mut buf = Vec::with_capacity(512); + let encoder = VecEncoder::<'_, E::CustomValueKind>::new(&mut buf, max_depth); + encoder.encode_payload(value, E::PAYLOAD_PREFIX)?; + Ok(buf) +} + +pub trait VecDecode: for<'a> Decode> {} +impl Decode>> VecDecode for T {} + +pub fn vec_decode>(buf: &[u8], max_depth: usize) -> Result { + VecDecoder::<'_, E::CustomValueKind>::new(buf, max_depth).decode_payload(E::PAYLOAD_PREFIX) +} + +pub trait VecSbor: + Categorize + + VecEncode + + VecDecode + + Describe<::CustomTypeKind> + {} + +impl VecSbor for T + where + T: Categorize, + T: VecEncode, + T: VecDecode, + T: Describe<::CustomTypeKind>, +{} \ No newline at end of file From f6e47874b21396601eedbc2db4f03b227d953c3a Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 11:50:25 +0100 Subject: [PATCH 05/19] fix: Run code formatting --- .../src/data/scrypto/custom_schema.rs | 40 +- .../src/types/indexed_value.rs | 2 +- sbor-tests/tests/schema_comparison.rs | 68 +- sbor/src/basic.rs | 2 +- sbor/src/lib.rs | 6 +- .../payload_validation/payload_validator.rs | 13 +- sbor/src/schema/schema_comparison.rs | 760 +++++++++++------- sbor/src/schema/type_aggregator.rs | 12 +- sbor/src/schema/type_data/type_kind.rs | 3 +- sbor/src/schema/type_data/type_validation.rs | 5 +- sbor/src/traversal/typed/full_location.rs | 9 +- sbor/src/vec_traits.rs | 31 +- 12 files changed, 592 insertions(+), 359 deletions(-) diff --git a/radix-common/src/data/scrypto/custom_schema.rs b/radix-common/src/data/scrypto/custom_schema.rs index 2f55e6ce72a..b532bcdec44 100644 --- a/radix-common/src/data/scrypto/custom_schema.rs +++ b/radix-common/src/data/scrypto/custom_schema.rs @@ -45,10 +45,18 @@ impl ReferenceValidation { fn compare(base: &Self, compared: &Self) -> ValidationChange { match (base, compared) { (base, compared) if base == compared => ValidationChange::Unchanged, - (ReferenceValidation::IsGlobal, compared) if compared.requires_global() => ValidationChange::Strengthened, - (base, ReferenceValidation::IsGlobal) if base.requires_global() => ValidationChange::Weakened, - (ReferenceValidation::IsInternal, compared) if compared.requires_internal() => ValidationChange::Strengthened, - (base, ReferenceValidation::IsInternal) if base.requires_internal() => ValidationChange::Weakened, + (ReferenceValidation::IsGlobal, compared) if compared.requires_global() => { + ValidationChange::Strengthened + } + (base, ReferenceValidation::IsGlobal) if base.requires_global() => { + ValidationChange::Weakened + } + (ReferenceValidation::IsInternal, compared) if compared.requires_internal() => { + ValidationChange::Strengthened + } + (base, ReferenceValidation::IsInternal) if base.requires_internal() => { + ValidationChange::Weakened + } (_, _) => ValidationChange::Incomparable, } } @@ -152,14 +160,16 @@ impl ReferenceValidation { impl CustomTypeKind for ScryptoCustomTypeKind { type CustomTypeValidation = ScryptoCustomTypeValidation; type CustomTypeKindLabel = ScryptoCustomTypeKindLabel; - + fn label(&self) -> Self::CustomTypeKindLabel { match self { ScryptoCustomTypeKind::Reference => ScryptoCustomTypeKindLabel::Reference, ScryptoCustomTypeKind::Own => ScryptoCustomTypeKindLabel::Own, ScryptoCustomTypeKind::Decimal => ScryptoCustomTypeKindLabel::Decimal, ScryptoCustomTypeKind::PreciseDecimal => ScryptoCustomTypeKindLabel::PreciseDecimal, - ScryptoCustomTypeKind::NonFungibleLocalId => ScryptoCustomTypeKindLabel::NonFungibleLocalId, + ScryptoCustomTypeKind::NonFungibleLocalId => { + ScryptoCustomTypeKindLabel::NonFungibleLocalId + } } } } @@ -179,10 +189,20 @@ impl CustomTypeKindLabel for ScryptoCustomTypeKindLabel { impl CustomTypeValidation for ScryptoCustomTypeValidation { fn compare(base: &Self, compared: &Self) -> ValidationChange { match (base, compared) { - (ScryptoCustomTypeValidation::Reference(base), ScryptoCustomTypeValidation::Reference(compared)) => ReferenceValidation::compare(base, compared), - (ScryptoCustomTypeValidation::Reference(_), ScryptoCustomTypeValidation::Own(_)) => ValidationChange::Incomparable, - (ScryptoCustomTypeValidation::Own(_), ScryptoCustomTypeValidation::Reference(_)) => ValidationChange::Incomparable, - (ScryptoCustomTypeValidation::Own(base), ScryptoCustomTypeValidation::Own(compared)) => OwnValidation::compare(base, compared), + ( + ScryptoCustomTypeValidation::Reference(base), + ScryptoCustomTypeValidation::Reference(compared), + ) => ReferenceValidation::compare(base, compared), + (ScryptoCustomTypeValidation::Reference(_), ScryptoCustomTypeValidation::Own(_)) => { + ValidationChange::Incomparable + } + (ScryptoCustomTypeValidation::Own(_), ScryptoCustomTypeValidation::Reference(_)) => { + ValidationChange::Incomparable + } + ( + ScryptoCustomTypeValidation::Own(base), + ScryptoCustomTypeValidation::Own(compared), + ) => OwnValidation::compare(base, compared), } } } diff --git a/radix-engine-interface/src/types/indexed_value.rs b/radix-engine-interface/src/types/indexed_value.rs index 4a4e55842a5..9fb0b83e80e 100644 --- a/radix-engine-interface/src/types/indexed_value.rs +++ b/radix-engine-interface/src/types/indexed_value.rs @@ -25,7 +25,7 @@ impl IndexedScryptoValue { VecTraverserConfig { max_depth: SCRYPTO_SBOR_V1_MAX_DEPTH, check_exact_end: true, - } + }, ); let mut references = Vec::::new(); let mut owned_nodes = Vec::::new(); diff --git a/sbor-tests/tests/schema_comparison.rs b/sbor-tests/tests/schema_comparison.rs index 92fea705296..a70a51b021c 100644 --- a/sbor-tests/tests/schema_comparison.rs +++ b/sbor-tests/tests/schema_comparison.rs @@ -1,17 +1,18 @@ use sbor::prelude::*; use sbor::schema::*; use sbor::BasicValue; +use sbor::ComparisonSchema; use sbor::NoCustomExtension; use sbor::NoCustomSchema; use sbor::NoCustomTypeKind; -use sbor::ComparisonSchema; //===================== // HELPER CODE / TRAITS //===================== trait DerivableTypeSchema: Describe { fn single_type_schema_version() -> String { - let single_type_schema_version: SingleTypeSchema = SingleTypeSchema::::from::(); + let single_type_schema_version: SingleTypeSchema = + SingleTypeSchema::::from::(); ComparisonSchema::::encode_to_hex(&single_type_schema_version) } } @@ -29,10 +30,10 @@ impl> RegisterType for NamedSchemaVer } fn assert_extension() { - assert_type_backwards_compatibility::(|v| v - .register_schema_of::("base") - .register_schema_of::("latest") - ) + assert_type_backwards_compatibility::(|v| { + v.register_schema_of::("base") + .register_schema_of::("latest") + }) } fn assert_extension_ignoring_name_changes() { @@ -51,13 +52,13 @@ fn assert_equality_ignoring_name_changes(&settings); } -fn assert_comparison_succeeds(settings: &SchemaComparisonSettings) { - assert_type_compatibility::( - settings, - |v| v - .register_schema_of::("base") +fn assert_comparison_succeeds( + settings: &SchemaComparisonSettings, +) { + assert_type_compatibility::(settings, |v| { + v.register_schema_of::("base") .register_schema_of::("latest") - ) + }) } fn assert_extension_multi( @@ -84,9 +85,10 @@ fn assert_multi_comparison_succeeds( assert_type_collection_backwards_compatibility::( settings, latest.clone(), - |v| v - .register_version("base", base) - .register_version("latest", latest), + |v| { + v.register_version("base", base) + .register_version("latest", latest) + }, ) } @@ -132,7 +134,7 @@ enum MyEnum { Variant1, Variant2, Variant3(u8, u16), - Variant4 { my_val: i32, my_struct: MyStruct, }, + Variant4 { my_val: i32, my_struct: MyStruct }, } #[derive(Sbor)] @@ -141,7 +143,7 @@ enum MyEnumVariantRenamed { Variant1, Variant2V2, Variant3(u8, u16), - Variant4 { my_val: i32, my_struct: MyStruct, }, + Variant4 { my_val: i32, my_struct: MyStruct }, } #[derive(Sbor)] @@ -150,7 +152,10 @@ enum MyEnumVariantFieldRenamed { Variant1, Variant2, Variant3(u8, u16), - Variant4 { my_val_renamed: i32, my_struct: MyStruct, }, + Variant4 { + my_val_renamed: i32, + my_struct: MyStruct, + }, } #[derive(Sbor)] @@ -159,7 +164,7 @@ enum MyEnumNewVariant { Variant1, Variant2, Variant3(u8, u16), - Variant4 { my_val: i32, my_struct: MyStruct, }, + Variant4 { my_val: i32, my_struct: MyStruct }, Variant5, } @@ -169,7 +174,7 @@ enum MyEnumVariantFieldAdded { Variant1, Variant2, Variant3(u8, u16, u32), - Variant4 { my_val: i32, my_struct: MyStruct, }, + Variant4 { my_val: i32, my_struct: MyStruct }, } #[derive(BasicDescribe)] @@ -187,7 +192,7 @@ enum MyMultiRecursiveTypeForm1 { Nothing, Opposite(Option>), Me(Option>), - Form1(Option>), + Form1(Option>), } #[derive(Sbor)] @@ -209,17 +214,17 @@ fn asserting_backwards_compatibility_requires_a_named_schema() { #[test] fn asserting_backwards_compatibility_with_a_single_latest_schema_version_succeeds() { - assert_type_backwards_compatibility::(|v| v - .register_schema_of::("latest") - ) + assert_type_backwards_compatibility::(|v| { + v.register_schema_of::("latest") + }) } #[test] #[should_panic] fn asserting_backwards_compatibility_with_incorrect_latest_schema_version_succeeds() { - assert_type_backwards_compatibility::(|v| v - .register_schema_of::("latest") - ) + assert_type_backwards_compatibility::(|v| { + v.register_schema_of::("latest") + }) } #[test] @@ -329,7 +334,12 @@ fn all_name_changes_allowed_succeeds() { assert_equality_ignoring_name_changes::(); assert_equality_ignoring_name_changes::< (MyStruct, MyStruct, MyEnum, MyEnum), - (MyStructFieldRenamed, MyStructTypeRenamed, MyEnumVariantRenamed, MyEnumVariantFieldRenamed), + ( + MyStructFieldRenamed, + MyStructTypeRenamed, + MyEnumVariantRenamed, + MyEnumVariantFieldRenamed, + ), >(); } @@ -451,4 +461,4 @@ fn under_equality_added_root_type_fails() { }; assert_equality_multi(base_schema, compared_schema); -} \ No newline at end of file +} diff --git a/sbor/src/basic.rs b/sbor/src/basic.rs index b0d17531099..824661871df 100644 --- a/sbor/src/basic.rs +++ b/sbor/src/basic.rs @@ -183,7 +183,7 @@ impl CustomTypeValidation for NoCustomTypeValidation { impl CustomTypeKind for NoCustomTypeKind { type CustomTypeValidation = NoCustomTypeValidation; type CustomTypeKindLabel = NoCustomTypeKindLabel; - + fn label(&self) -> Self::CustomTypeKindLabel { unreachable!("No custom type kinds exist") } diff --git a/sbor/src/lib.rs b/sbor/src/lib.rs index d57bfb20eee..da186fefaf0 100644 --- a/sbor/src/lib.rs +++ b/sbor/src/lib.rs @@ -19,8 +19,6 @@ pub mod decode; pub mod decoder; /// SBOR encode trait. pub mod encode; -/// Simpler traits specific to encodability/decodability against vec-based encoders/decoders -pub mod vec_traits; /// SBOR payload wrappers. /// These are new types around an encoded payload or sub-payload, with helper methods / traits implemented. /// They can be used as a more efficient wrapper a ScryptoValue if the content of that value is not needed. @@ -44,6 +42,8 @@ pub mod traversal; pub mod value; /// SBOR value kinds - ie the types of value that are supported. pub mod value_kind; +/// Simpler traits specific to encodability/decodability against vec-based encoders/decoders +pub mod vec_traits; /// Data model versioning helper macro pub mod versioned; @@ -108,6 +108,7 @@ pub mod prelude { pub use crate::schema::prelude::*; pub use crate::value::{CustomValue as SborCustomValue, Value as SborValue}; pub use crate::value_kind::*; + pub use crate::vec_traits::*; pub use crate::versioned::*; pub use crate::{ basic_decode, basic_encode, BasicCategorize, BasicDecode, BasicDescribe, BasicEncode, @@ -115,7 +116,6 @@ pub mod prelude { }; pub use crate::{define_single_versioned, define_versioned}; pub use crate::{Categorize, Decode, Encode, Sbor, SborEnum, SborTuple}; - pub use crate::vec_traits::*; pub use crate::{DecodeError, EncodeError}; } diff --git a/sbor/src/payload_validation/payload_validator.rs b/sbor/src/payload_validation/payload_validator.rs index 018c5c4da69..50a12a288e3 100644 --- a/sbor/src/payload_validation/payload_validator.rs +++ b/sbor/src/payload_validation/payload_validator.rs @@ -628,8 +628,12 @@ mod tests { check_location_path_with_depth_limit::( basic_encode(&MyStruct2 { field1: 3, - field2: MyStruct2Inner { inner1: 1, inner2: 2 }, - }).unwrap(), + field2: MyStruct2Inner { + inner1: 1, + inner2: 2, + }, + }) + .unwrap(), 2, "MyStruct2.[1|field2]->MyStruct2Inner.[0|inner1]", "DecodeError(MaxDepthExceeded(2))", @@ -664,7 +668,10 @@ mod tests { basic_encode(&BasicValue::Tuple { fields: vec![ BasicValue::U16 { value: 1 }, - BasicValue::Array { element_value_kind: ValueKind::U8, elements: vec![], }, + BasicValue::Array { + element_value_kind: ValueKind::U8, + elements: vec![], + }, ], }) .unwrap(), diff --git a/sbor/src/schema/schema_comparison.rs b/sbor/src/schema/schema_comparison.rs index 45975642477..931f096106a 100644 --- a/sbor/src/schema/schema_comparison.rs +++ b/sbor/src/schema/schema_comparison.rs @@ -51,7 +51,10 @@ impl SchemaComparisonSettings { } } - pub const fn completeness_settings(mut self, checks: SchemaComparisonCompletenessSettings) -> Self { + pub const fn completeness_settings( + mut self, + checks: SchemaComparisonCompletenessSettings, + ) -> Self { self.completeness = checks; self } @@ -183,9 +186,16 @@ impl NameChangeRule { pub enum NameChange<'a> { Unchanged, - NameAdded { new_name: &'a str }, - NameRemoved { old_name: &'a str, }, - NameChanged { old_name: &'a str, new_name: &'a str, }, + NameAdded { + new_name: &'a str, + }, + NameRemoved { + old_name: &'a str, + }, + NameChanged { + old_name: &'a str, + new_name: &'a str, + }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -199,17 +209,14 @@ pub enum OwnedNameChange { Unchanged, NameAdded { new_name: String }, NameRemoved { old_name: String }, - NameChanged { old_name: String, new_name: String, }, + NameChanged { old_name: String, new_name: String }, } impl<'a> NameChange<'a> { pub fn of_changed_option(from: Option<&'a str>, to: Option<&'a str>) -> Self { match (from, to) { (Some(old_name), Some(new_name)) if old_name == new_name => NameChange::Unchanged, - (Some(old_name), Some(new_name)) => NameChange::NameChanged { - old_name, - new_name, - }, + (Some(old_name), Some(new_name)) => NameChange::NameChanged { old_name, new_name }, (Some(old_name), None) => NameChange::NameRemoved { old_name }, (None, Some(new_name)) => NameChange::NameAdded { new_name }, (None, None) => NameChange::Unchanged, @@ -238,9 +245,16 @@ impl<'a> NameChange<'a> { fn into_owned(&self) -> OwnedNameChange { match *self { NameChange::Unchanged => OwnedNameChange::Unchanged, - NameChange::NameAdded { new_name } => OwnedNameChange::NameAdded { new_name: new_name.to_string() }, - NameChange::NameRemoved { old_name } => OwnedNameChange::NameRemoved { old_name: old_name.to_string() }, - NameChange::NameChanged { old_name, new_name } => OwnedNameChange::NameChanged { old_name: old_name.to_string(), new_name: new_name.to_string(), }, + NameChange::NameAdded { new_name } => OwnedNameChange::NameAdded { + new_name: new_name.to_string(), + }, + NameChange::NameRemoved { old_name } => OwnedNameChange::NameRemoved { + old_name: old_name.to_string(), + }, + NameChange::NameChanged { old_name, new_name } => OwnedNameChange::NameChanged { + old_name: old_name.to_string(), + new_name: new_name.to_string(), + }, } } } @@ -279,9 +293,15 @@ impl ValidationChange { (_, ValidationChange::Incomparable) => ValidationChange::Incomparable, (ValidationChange::Unchanged, other) => other, (other, ValidationChange::Unchanged) => other, - (ValidationChange::Strengthened, ValidationChange::Strengthened) => ValidationChange::Strengthened, - (ValidationChange::Strengthened, ValidationChange::Weakened) => ValidationChange::Incomparable, - (ValidationChange::Weakened, ValidationChange::Strengthened) => ValidationChange::Incomparable, + (ValidationChange::Strengthened, ValidationChange::Strengthened) => { + ValidationChange::Strengthened + } + (ValidationChange::Strengthened, ValidationChange::Weakened) => { + ValidationChange::Incomparable + } + (ValidationChange::Weakened, ValidationChange::Strengthened) => { + ValidationChange::Incomparable + } (ValidationChange::Weakened, ValidationChange::Weakened) => ValidationChange::Weakened, } } @@ -299,7 +319,11 @@ impl<'s, S: CustomSchema> SchemaComparisonResult<'s, S> { self.errors.len() == 0 } - pub fn error_message(&self, base_schema_name: &str, compared_schema_name: &str) -> Option { + pub fn error_message( + &self, + base_schema_name: &str, + compared_schema_name: &str, + ) -> Option { if self.errors.len() == 0 { return None; } @@ -313,7 +337,9 @@ impl<'s, S: CustomSchema> SchemaComparisonResult<'s, S> { ).unwrap(); for error in &self.errors { write!(&mut output, "- ").unwrap(); - error.write_against_schemas(&mut output, &self.base_schema, &self.compared_schema).unwrap(); + error + .write_against_schemas(&mut output, &self.base_schema, &self.compared_schema) + .unwrap(); writeln!(&mut output).unwrap(); } Some(output) @@ -343,8 +369,7 @@ impl SchemaComparisonError { write!( f, "{:?} under {} at type path ", - &self.error_detail, - location.root_type_identifier, + &self.error_detail, location.root_type_identifier, )?; (location, base_schema, compared_schema).write_path(f)?; } else { @@ -356,7 +381,9 @@ impl SchemaComparisonError { fn combine_names(base_name: Option<&str>, compared_name: Option<&str>) -> Option { match (base_name, compared_name) { - (Some(base_name), Some(compared_name)) if base_name == compared_name => Some(base_name.to_string()), + (Some(base_name), Some(compared_name)) if base_name == compared_name => { + Some(base_name.to_string()) + } (Some(base_name), Some(compared_name)) => Some(format!("{base_name}|{compared_name}")), (Some(base_name), None) => Some(format!("{base_name}|anon")), (None, Some(compared_name)) => Some(format!("anon|{compared_name}")), @@ -364,80 +391,87 @@ fn combine_names(base_name: Option<&str>, compared_name: Option<&str>) -> Option } } -impl<'s, 'a, S: CustomSchema> PathAnnotate - for (&'a TypeFullPath, &'a Schema, &'a Schema) -{ +impl<'s, 'a, S: CustomSchema> PathAnnotate for (&'a TypeFullPath, &'a Schema, &'a Schema) { fn iter_ancestor_path(&self) -> Box> + '_> { let (full_path, base_schema, compared_schema) = *self; - let iterator = full_path.ancestor_path - .iter() - .map(|path_segment| { - let base_metadata = base_schema.resolve_type_metadata(path_segment.parent_base_type_id) - .expect("Invalid base schema - Could not find metadata for base type"); - let compared_metadata = compared_schema.resolve_type_metadata(path_segment.parent_compared_type_id) - .expect("Invalid compared schema - Could not find metadata for compared type"); - - let name = Cow::Owned(combine_names( - base_metadata.get_name(), - compared_metadata.get_name() - ).unwrap_or_else(|| { - combine_names( - Some(base_schema.resolve_type_kind(path_segment.parent_base_type_id).unwrap().category_name()), - Some(compared_schema.resolve_type_kind(path_segment.parent_compared_type_id).unwrap().category_name()), - ).unwrap() - })); - - let container = match path_segment.child_locator { - ChildTypeLocator::Tuple { field_index } => { - let field_name = combine_names( - base_metadata.get_field_name(field_index), - compared_metadata.get_field_name(field_index), - ).map(Cow::Owned); - AnnotatedSborAncestorContainer::Tuple { - field_index, - field_name, - } - }, - ChildTypeLocator::EnumVariant { + let iterator = full_path.ancestor_path.iter().map(|path_segment| { + let base_metadata = base_schema + .resolve_type_metadata(path_segment.parent_base_type_id) + .expect("Invalid base schema - Could not find metadata for base type"); + let compared_metadata = compared_schema + .resolve_type_metadata(path_segment.parent_compared_type_id) + .expect("Invalid compared schema - Could not find metadata for compared type"); + + let name = Cow::Owned( + combine_names(base_metadata.get_name(), compared_metadata.get_name()) + .unwrap_or_else(|| { + combine_names( + Some( + base_schema + .resolve_type_kind(path_segment.parent_base_type_id) + .unwrap() + .category_name(), + ), + Some( + compared_schema + .resolve_type_kind(path_segment.parent_compared_type_id) + .unwrap() + .category_name(), + ), + ) + .unwrap() + }), + ); + + let container = match path_segment.child_locator { + ChildTypeLocator::Tuple { field_index } => { + let field_name = combine_names( + base_metadata.get_field_name(field_index), + compared_metadata.get_field_name(field_index), + ) + .map(Cow::Owned); + AnnotatedSborAncestorContainer::Tuple { + field_index, + field_name, + } + } + ChildTypeLocator::EnumVariant { + discriminator, + field_index, + } => { + let base_variant_metadata = base_metadata + .get_enum_variant_data(discriminator) + .expect("Base schema has variant names"); + let compared_variant_metadata = compared_metadata + .get_enum_variant_data(discriminator) + .expect("Compared schema has variant names"); + let variant_name = combine_names( + base_variant_metadata.get_name(), + compared_variant_metadata.get_name(), + ) + .map(Cow::Owned); + let field_name = combine_names( + base_variant_metadata.get_field_name(field_index), + compared_variant_metadata.get_field_name(field_index), + ) + .map(Cow::Owned); + AnnotatedSborAncestorContainer::EnumVariant { discriminator, + variant_name, field_index, - } => { - let base_variant_metadata = base_metadata.get_enum_variant_data(discriminator).expect("Base schema has variant names"); - let compared_variant_metadata = compared_metadata.get_enum_variant_data(discriminator).expect("Compared schema has variant names"); - let variant_name = combine_names( - base_variant_metadata.get_name(), - compared_variant_metadata.get_name(), - ).map(Cow::Owned); - let field_name = combine_names( - base_variant_metadata.get_field_name(field_index), - compared_variant_metadata.get_field_name(field_index), - ).map(Cow::Owned); - AnnotatedSborAncestorContainer::EnumVariant { - discriminator, - variant_name, - field_index, - field_name, - } - }, - ChildTypeLocator::Array { } => { - AnnotatedSborAncestorContainer::Array { - index: None, - } - }, - ChildTypeLocator::Map { entry_part } => { - AnnotatedSborAncestorContainer::Map { - index: None, - entry_part, - } - }, - }; - - AnnotatedSborAncestor { - name, - container, + field_name, + } } - }); + ChildTypeLocator::Array {} => AnnotatedSborAncestorContainer::Array { index: None }, + ChildTypeLocator::Map { entry_part } => AnnotatedSborAncestorContainer::Map { + index: None, + entry_part, + }, + }; + + AnnotatedSborAncestor { name, container } + }); Box::new(iterator) } @@ -447,20 +481,34 @@ impl<'s, 'a, S: CustomSchema> PathAnnotate let base_type_id = full_path.leaf_base_type_id; let compared_type_id = full_path.leaf_compared_type_id; - let base_metadata = base_schema.resolve_type_metadata(base_type_id) + let base_metadata = base_schema + .resolve_type_metadata(base_type_id) .expect("Invalid base schema - Could not find metadata for base type"); - let compared_metadata = compared_schema.resolve_type_metadata(compared_type_id) + let compared_metadata = compared_schema + .resolve_type_metadata(compared_type_id) .expect("Invalid compared schema - Could not find metadata for compared type"); - let name = Cow::Owned(combine_names( - base_metadata.get_name(), - compared_metadata.get_name() - ).unwrap_or_else(|| { - combine_names( - Some(base_schema.resolve_type_kind(base_type_id).unwrap().category_name()), - Some(compared_schema.resolve_type_kind(compared_type_id).unwrap().category_name()), - ).unwrap() - })); + let name = Cow::Owned( + combine_names(base_metadata.get_name(), compared_metadata.get_name()).unwrap_or_else( + || { + combine_names( + Some( + base_schema + .resolve_type_kind(base_type_id) + .unwrap() + .category_name(), + ), + Some( + compared_schema + .resolve_type_kind(compared_type_id) + .unwrap() + .category_name(), + ), + ) + .unwrap() + }, + ), + ); Some(AnnotatedSborPartialLeaf { name, @@ -477,7 +525,11 @@ struct SchemaComparisonPathSegment { } impl SchemaComparisonPathSegment { - pub fn of(parent_base_type_id: &LocalTypeId, parent_compared_type_id: &LocalTypeId, child_locator: ChildTypeLocator) -> Self { + pub fn of( + parent_base_type_id: &LocalTypeId, + parent_compared_type_id: &LocalTypeId, + child_locator: ChildTypeLocator, + ) -> Self { Self { parent_base_type_id: *parent_base_type_id, parent_compared_type_id: *parent_compared_type_id, @@ -551,10 +603,17 @@ struct TypeKindComparisonResult { #[derive(Debug, Clone, PartialEq, Eq)] enum ChildTypeLocator { - Tuple { field_index: usize, }, - EnumVariant { discriminator: u8, field_index: usize, }, - Array { }, // Unlike values, we don't have an index - Map { entry_part: MapEntryPart, } // Unlike values, we don't have an index + Tuple { + field_index: usize, + }, + EnumVariant { + discriminator: u8, + field_index: usize, + }, + Array {}, // Unlike values, we don't have an index + Map { + entry_part: MapEntryPart, + }, // Unlike values, we don't have an index } impl TypeKindComparisonResult { @@ -581,16 +640,19 @@ impl TypeKindComparisonResult { self } - fn with_error( - mut self, - error: SchemaComparisonErrorDetail, - ) -> Self { + fn with_error(mut self, error: SchemaComparisonErrorDetail) -> Self { self.add_error(error); self } - fn add_child_to_check(&mut self, child_locator: ChildTypeLocator, base_type_id: LocalTypeId, compared_type_id: LocalTypeId) { - self.children_needing_checking.push((child_locator, base_type_id, compared_type_id)); + fn add_child_to_check( + &mut self, + child_locator: ChildTypeLocator, + base_type_id: LocalTypeId, + compared_type_id: LocalTypeId, + ) { + self.children_needing_checking + .push((child_locator, base_type_id, compared_type_id)); } } @@ -741,9 +803,11 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { self.mark_root_reachable_base_types(base_type_id); self.mark_root_reachable_compared_types(compared_type_id); } else { - self.errors.record_error_with_unvisited_location(SchemaComparisonErrorDetail::NamedRootTypeMissingInComparedSchema { - root_type_name: base_root_type_name.clone(), - }); + self.errors.record_error_with_unvisited_location( + SchemaComparisonErrorDetail::NamedRootTypeMissingInComparedSchema { + root_type_name: base_root_type_name.clone(), + }, + ); self.mark_root_reachable_base_types(base_type_id); } } @@ -751,10 +815,16 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { // We now loop through the compared types not covered in the above loop over base types for (compared_root_type_name, compared_type_id) in compared_type_roots.iter() { if !base_type_roots.contains_key(compared_root_type_name) { - if !self.settings.completeness.allow_compared_to_have_more_root_types { - self.errors.record_error_with_unvisited_location(SchemaComparisonErrorDetail::DisallowedNewRootTypeInComparedSchema { - root_type_name: compared_root_type_name.clone(), - }); + if !self + .settings + .completeness + .allow_compared_to_have_more_root_types + { + self.errors.record_error_with_unvisited_location( + SchemaComparisonErrorDetail::DisallowedNewRootTypeInComparedSchema { + root_type_name: compared_root_type_name.clone(), + }, + ); } self.mark_root_reachable_compared_types(compared_type_id); } @@ -786,10 +856,7 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { } } - fn mark_root_reachable_base_types( - &mut self, - root_base_type_id: &LocalTypeId, - ) { + fn mark_root_reachable_base_types(&mut self, root_base_type_id: &LocalTypeId) { // Due to the cache, we do max O(TypesInBase) work. // Note that reachability analysis needs to be performed separately to comparison analysis, because // sometimes with comparisons of MyTuple(A) and MyTuple(B1, B2), we still want to perform reachability @@ -799,13 +866,20 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { }; let mut base_reachability_work_list = vec![*root_base_local_index]; while let Some(base_type_index) = base_reachability_work_list.pop() { - match self.base_local_types_reachable_from_a_root.entry(base_type_index) { + match self + .base_local_types_reachable_from_a_root + .entry(base_type_index) + { hash_map::Entry::Occupied(_) => continue, hash_map::Entry::Vacant(vacant_entry) => vacant_entry.insert(()), }; let type_id = LocalTypeId::SchemaLocalIndex(base_type_index); - let type_kind = self.base_schema.resolve_type_kind(type_id) - .unwrap_or_else(|| panic!("Invalid base schema - type kind for {type_id:?} not found")); + let type_kind = self + .base_schema + .resolve_type_kind(type_id) + .unwrap_or_else(|| { + panic!("Invalid base schema - type kind for {type_id:?} not found") + }); visit_type_kind_children(type_kind, |_child_locator, child_type_kind| { if let LocalTypeId::SchemaLocalIndex(local_index) = child_type_kind { base_reachability_work_list.push(local_index); @@ -814,22 +888,26 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { } } - fn mark_root_reachable_compared_types( - &mut self, - root_compared_type_id: &LocalTypeId, - ) { + fn mark_root_reachable_compared_types(&mut self, root_compared_type_id: &LocalTypeId) { let LocalTypeId::SchemaLocalIndex(root_compared_local_index) = root_compared_type_id else { return; }; let mut compared_reachability_work_list = vec![*root_compared_local_index]; while let Some(compared_local_index) = compared_reachability_work_list.pop() { - match self.compared_local_types_reachable_from_a_root.entry(compared_local_index) { + match self + .compared_local_types_reachable_from_a_root + .entry(compared_local_index) + { hash_map::Entry::Occupied(_) => continue, hash_map::Entry::Vacant(vacant_entry) => vacant_entry.insert(()), }; let type_id = LocalTypeId::SchemaLocalIndex(compared_local_index); - let type_kind = self.compared_schema.resolve_type_kind(type_id) - .unwrap_or_else(|| panic!("Invalid compared schema - type kind for {type_id:?} not found")); + let type_kind = self + .compared_schema + .resolve_type_kind(type_id) + .unwrap_or_else(|| { + panic!("Invalid compared schema - type kind for {type_id:?} not found") + }); visit_type_kind_children(type_kind, |_child_locator, child_type_kind| { if let LocalTypeId::SchemaLocalIndex(local_index) = child_type_kind { compared_reachability_work_list.push(local_index); @@ -854,7 +932,9 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { } let result = self.compare_types_internal(&example_location, base_type_id, compared_type_id); - for (child_locator, child_base_type_id, child_compared_type_id) in result.child_checks_required { + for (child_locator, child_base_type_id, child_compared_type_id) in + result.child_checks_required + { if self .cached_located_type_comparisons .contains_key(&(child_base_type_id, child_compared_type_id)) @@ -910,12 +990,18 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { } // Load type data from each schema - let (base_type_kind, base_type_metadata, base_type_validation) = - self.base_schema.resolve_type_data(base_type_id) - .unwrap_or_else(|| panic!("Base schema was not valid - no type data for {base_type_id:?}")); - let (compared_type_kind, compared_type_metadata, compared_type_validation) = - self.compared_schema.resolve_type_data(compared_type_id) - .unwrap_or_else(|| panic!("Compared schema was not valid - no type data for {compared_type_id:?}")); + let (base_type_kind, base_type_metadata, base_type_validation) = self + .base_schema + .resolve_type_data(base_type_id) + .unwrap_or_else(|| { + panic!("Base schema was not valid - no type data for {base_type_id:?}") + }); + let (compared_type_kind, compared_type_metadata, compared_type_validation) = self + .compared_schema + .resolve_type_data(compared_type_id) + .unwrap_or_else(|| { + panic!("Compared schema was not valid - no type data for {compared_type_id:?}") + }); // Type Kind Comparison let further_checks_required = { @@ -948,21 +1034,16 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { // Type Metadata Comparison { - let TypeMetadataComparisonResult { errors } = - self.compare_type_metadata_internal( - base_type_kind, - base_type_metadata, - compared_type_metadata, - ); + let TypeMetadataComparisonResult { errors } = self.compare_type_metadata_internal( + base_type_kind, + base_type_metadata, + compared_type_metadata, + ); for error in errors { error_recorded = true; - self.errors.record_error( - error, - example_location, - base_type_id, - compared_type_id, - ); + self.errors + .record_error(error, example_location, base_type_id, compared_type_id); } } @@ -973,12 +1054,8 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { for error in errors { error_recorded = true; - self.errors.record_error( - error, - example_location, - base_type_id, - compared_type_id, - ); + self.errors + .record_error(error, example_location, base_type_id, compared_type_id); } } @@ -1013,7 +1090,11 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { // which have now disappeared because the Compared side is Any) // * To ensure we pass completeness checks on the base side visit_type_kind_children(&base_type_kind, |child_type_locator, child_type_kind| { - result.add_child_to_check(child_type_locator, child_type_kind, LocalTypeId::WellKnown(ANY_TYPE)); + result.add_child_to_check( + child_type_locator, + child_type_kind, + LocalTypeId::WellKnown(ANY_TYPE), + ); }); return result; } @@ -1033,10 +1114,7 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { | TypeKind::U128 | TypeKind::String => { if compared_type_kind != base_type_kind { - return result.with_mismatch_error( - base_type_kind, - compared_type_kind, - ); + return result.with_mismatch_error(base_type_kind, compared_type_kind); } } TypeKind::Array { @@ -1046,13 +1124,10 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { element_type: compared_element_type, } = compared_type_kind else { - return result.with_mismatch_error( - base_type_kind, - compared_type_kind, - ); + return result.with_mismatch_error(base_type_kind, compared_type_kind); }; result.add_child_to_check( - ChildTypeLocator::Array { }, + ChildTypeLocator::Array {}, *base_element_type, *compared_element_type, ); @@ -1064,10 +1139,7 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { field_types: compared_field_types, } = compared_type_kind else { - return result.with_mismatch_error( - base_type_kind, - compared_type_kind, - ); + return result.with_mismatch_error(base_type_kind, compared_type_kind); }; if base_field_types.len() != compared_field_types.len() { return result.with_error( @@ -1083,7 +1155,11 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { .zip(compared_field_types.iter().cloned()) .enumerate(); for (field_index, (base, compared)) in matched_field_types { - result.add_child_to_check(ChildTypeLocator::Tuple { field_index }, base, compared); + result.add_child_to_check( + ChildTypeLocator::Tuple { field_index }, + base, + compared, + ); } } TypeKind::Enum { @@ -1093,10 +1169,7 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { variants: compared_variants, } = compared_type_kind else { - return result.with_mismatch_error( - base_type_kind, - compared_type_kind, - ); + return result.with_mismatch_error(base_type_kind, compared_type_kind); }; let base_variants_missing_in_compared: IndexSet<_> = base_variants @@ -1110,7 +1183,10 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { .cloned() .collect(); - if base_variants_missing_in_compared.len() > 0 || (compared_variants_missing_in_base.len() > 0 && !settings.allow_new_enum_variants){ + if base_variants_missing_in_compared.len() > 0 + || (compared_variants_missing_in_base.len() > 0 + && !settings.allow_new_enum_variants) + { result.add_error(SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { base_variants_missing_in_compared, compared_variants_missing_in_base, @@ -1127,21 +1203,28 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { let discriminator = *discriminator; if base_field_type_ids.len() != compared_field_type_ids.len() { - result.add_error(SchemaComparisonErrorDetail::EnumVariantFieldCountMismatch { - variant_discriminator: discriminator, - base_field_count: base_field_type_ids.len(), - compared_field_count: compared_field_type_ids.len(), - }); + result.add_error( + SchemaComparisonErrorDetail::EnumVariantFieldCountMismatch { + variant_discriminator: discriminator, + base_field_count: base_field_type_ids.len(), + compared_field_count: compared_field_type_ids.len(), + }, + ); } else { let paired_child_ids = base_field_type_ids .iter() .zip(compared_field_type_ids.iter()) .enumerate(); - for (field_index, (base_child_type_id, compared_child_type_id)) in paired_child_ids { + for (field_index, (base_child_type_id, compared_child_type_id)) in + paired_child_ids + { result.add_child_to_check( - ChildTypeLocator::EnumVariant { discriminator, field_index, }, + ChildTypeLocator::EnumVariant { + discriminator, + field_index, + }, *base_child_type_id, - *compared_child_type_id + *compared_child_type_id, ); } } @@ -1156,27 +1239,33 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { value_type: compared_value_type, } = compared_type_kind else { - return result.with_mismatch_error( - base_type_kind, - compared_type_kind, - ); + return result.with_mismatch_error(base_type_kind, compared_type_kind); }; - result.add_child_to_check(ChildTypeLocator::Map { entry_part: MapEntryPart::Key }, *base_key_type, *compared_key_type); - result.add_child_to_check(ChildTypeLocator::Map { entry_part: MapEntryPart::Value }, *base_value_type, *compared_value_type); + result.add_child_to_check( + ChildTypeLocator::Map { + entry_part: MapEntryPart::Key, + }, + *base_key_type, + *compared_key_type, + ); + result.add_child_to_check( + ChildTypeLocator::Map { + entry_part: MapEntryPart::Value, + }, + *base_value_type, + *compared_value_type, + ); } // Assume for now that custom types are leaf types. // Therefore we can directly run equality on the types, like the simple types. TypeKind::Custom(_) => { if compared_type_kind != base_type_kind { - return result.with_mismatch_error( - base_type_kind, - compared_type_kind, - ); + return result.with_mismatch_error(base_type_kind, compared_type_kind); } } } - + result } @@ -1194,7 +1283,9 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { if let Err(error) = NameChange::of_changed_option( base_type_metadata.type_name.as_deref(), compared_type_metadata.type_name.as_deref(), - ).validate(settings.type_name_changes) { + ) + .validate(settings.type_name_changes) + { result.add_error(SchemaComparisonErrorDetail::TypeNameChangeError(error)); } @@ -1208,14 +1299,16 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { if let Err(error) = NameChange::of_changed_option( base_type_metadata.get_field_name(field_index), compared_type_metadata.get_field_name(field_index), - ).validate(settings.field_name_changes) { + ) + .validate(settings.field_name_changes) + { result.add_error(SchemaComparisonErrorDetail::FieldNameChangeError { field_index, error, }); } } - }, + } TypeKind::Enum { variants } => { for (variant_discriminator, base_variant_types) in variants.iter() { let variant_discriminator = *variant_discriminator; @@ -1229,7 +1322,9 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { if let Err(error) = NameChange::of_changed_option( base_variant_metadata.type_name.as_deref(), compared_variant_metadata.type_name.as_deref(), - ).validate(settings.field_name_changes) { + ) + .validate(settings.field_name_changes) + { result.add_error(SchemaComparisonErrorDetail::EnumVariantNameChangeError { variant_discriminator, error, @@ -1240,17 +1335,20 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { if let Err(error) = NameChange::of_changed_option( base_variant_metadata.get_field_name(field_index), compared_variant_metadata.get_field_name(field_index), - ).validate(settings.field_name_changes) { - result.add_error(SchemaComparisonErrorDetail::EnumVariantFieldNameChangeError { - variant_discriminator, - field_index, - error, - }); + ) + .validate(settings.field_name_changes) + { + result.add_error( + SchemaComparisonErrorDetail::EnumVariantFieldNameChangeError { + variant_discriminator, + field_index, + error, + }, + ); } - } } - }, + } _ => { // We can assume the schema is valid, therefore the only valid value is ChildNames::None // So validation passes trivially @@ -1276,22 +1374,50 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { (_, TypeValidation::None) => ValidationChange::Weakened, (TypeValidation::None, _) => ValidationChange::Strengthened, // Now test equal validations - (TypeValidation::I8(base), TypeValidation::I8(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::I16(base), TypeValidation::I16(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::I32(base), TypeValidation::I32(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::I64(base), TypeValidation::I64(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::I128(base), TypeValidation::I128(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::U8(base), TypeValidation::U8(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::U16(base), TypeValidation::U16(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::U32(base), TypeValidation::U32(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::U64(base), TypeValidation::U64(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::U128(base), TypeValidation::U128(compared)) => NumericValidation::compare(base, compared), - (TypeValidation::String(base), TypeValidation::String(compared)) => LengthValidation::compare(base, compared), - (TypeValidation::Array(base), TypeValidation::Array(compared)) => LengthValidation::compare(base, compared), - (TypeValidation::Map(base), TypeValidation::Map(compared)) => LengthValidation::compare(base, compared), + (TypeValidation::I8(base), TypeValidation::I8(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::I16(base), TypeValidation::I16(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::I32(base), TypeValidation::I32(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::I64(base), TypeValidation::I64(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::I128(base), TypeValidation::I128(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U8(base), TypeValidation::U8(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U16(base), TypeValidation::U16(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U32(base), TypeValidation::U32(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U64(base), TypeValidation::U64(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U128(base), TypeValidation::U128(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::String(base), TypeValidation::String(compared)) => { + LengthValidation::compare(base, compared) + } + (TypeValidation::Array(base), TypeValidation::Array(compared)) => { + LengthValidation::compare(base, compared) + } + (TypeValidation::Map(base), TypeValidation::Map(compared)) => { + LengthValidation::compare(base, compared) + } (TypeValidation::Custom(base), TypeValidation::Custom(compared)) => { - <::CustomTypeValidation as CustomTypeValidation>::compare(base, compared) - }, + <::CustomTypeValidation as CustomTypeValidation>::compare( + base, compared, + ) + } // Otherwise assume they are incomparable _ => ValidationChange::Incomparable, }; @@ -1312,31 +1438,53 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { } fn check_for_completeness(&mut self) { - if !self.settings.completeness.allow_root_unreachable_types_in_base_schema { - if self.base_local_types_reachable_from_a_root.len() < self.base_schema.type_metadata.len() { - for (local_type_index, metadata) in self.base_schema.type_metadata.iter().enumerate() { - if !self.base_local_types_reachable_from_a_root.contains_key(&local_type_index) { + if !self + .settings + .completeness + .allow_root_unreachable_types_in_base_schema + { + if self.base_local_types_reachable_from_a_root.len() + < self.base_schema.type_metadata.len() + { + for (local_type_index, metadata) in + self.base_schema.type_metadata.iter().enumerate() + { + if !self + .base_local_types_reachable_from_a_root + .contains_key(&local_type_index) + { let type_name = metadata.type_name.as_ref().map(|n| n.clone().into_owned()); self.errors.record_error_with_unvisited_location( SchemaComparisonErrorDetail::TypeUnreachableFromRootInBaseSchema { local_type_index, type_name, - } + }, ) } } } } - if !self.settings.completeness.allow_root_unreachable_types_in_compared_schema { - if self.compared_local_types_reachable_from_a_root.len() < self.compared_schema.type_metadata.len() { - for (local_type_index, metadata) in self.compared_schema.type_metadata.iter().enumerate() { - if !self.compared_local_types_reachable_from_a_root.contains_key(&local_type_index) { + if !self + .settings + .completeness + .allow_root_unreachable_types_in_compared_schema + { + if self.compared_local_types_reachable_from_a_root.len() + < self.compared_schema.type_metadata.len() + { + for (local_type_index, metadata) in + self.compared_schema.type_metadata.iter().enumerate() + { + if !self + .compared_local_types_reachable_from_a_root + .contains_key(&local_type_index) + { let type_name = metadata.type_name.as_ref().map(|n| n.clone().into_owned()); self.errors.record_error_with_unvisited_location( SchemaComparisonErrorDetail::TypeUnreachableFromRootInComparedSchema { local_type_index, type_name, - } + }, ) } } @@ -1370,7 +1518,7 @@ fn visit_type_kind_children>( | TypeKind::U32 | TypeKind::U64 | TypeKind::U128 - | TypeKind::String => {}, + | TypeKind::String => {} TypeKind::Array { element_type } => { visitor(ChildTypeLocator::Array {}, *element_type); } @@ -1383,7 +1531,10 @@ fn visit_type_kind_children>( for (discriminator, field_types) in variants { for (field_index, field_type) in field_types.iter().enumerate() { visitor( - ChildTypeLocator::EnumVariant { discriminator: *discriminator, field_index, }, + ChildTypeLocator::EnumVariant { + discriminator: *discriminator, + field_index, + }, *field_type, ) } @@ -1393,15 +1544,24 @@ fn visit_type_kind_children>( key_type, value_type, } => { - visitor(ChildTypeLocator::Map { entry_part: MapEntryPart::Key }, *key_type); - visitor(ChildTypeLocator::Map { entry_part: MapEntryPart::Value }, *value_type); + visitor( + ChildTypeLocator::Map { + entry_part: MapEntryPart::Key, + }, + *key_type, + ); + visitor( + ChildTypeLocator::Map { + entry_part: MapEntryPart::Value, + }, + *value_type, + ); } // At present, assume that custom types are leaf types. - TypeKind::Custom(_) => {}, + TypeKind::Custom(_) => {} }; } - #[derive(Debug, Clone, PartialEq, Eq)] struct PendingComparisonRequest { base_type_id: LocalTypeId, @@ -1475,7 +1635,8 @@ impl> NamedSchemaVersions { name: impl AsRef, version: impl IntoSchema, ) -> Self { - self.ordered_versions.insert(name.as_ref().to_string(), version.into_schema()); + self.ordered_versions + .insert(name.as_ref().to_string(), version.into_schema()); self } @@ -1521,9 +1682,10 @@ pub fn assert_type_backwards_compatibility< E: CustomExtension, T: Describe<::CustomTypeKind>, >( - versions_builder: impl FnOnce(NamedSchemaVersions>) -> NamedSchemaVersions>, -) -where + versions_builder: impl FnOnce( + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) where SingleTypeSchema: ComparisonSchema, { assert_type_compatibility::( @@ -1537,35 +1699,40 @@ pub fn assert_type_compatibility< T: Describe<::CustomTypeKind>, >( comparison_settings: &SchemaComparisonSettings, - versions_builder: impl FnOnce(NamedSchemaVersions>) -> NamedSchemaVersions>, -) -where + versions_builder: impl FnOnce( + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) where SingleTypeSchema: ComparisonSchema, { let current = { let (type_id, schema) = generate_full_schema_from_single_type::(); SingleTypeSchema { schema, type_id } }; - assert_schema_compatibility(comparison_settings, ¤t, &versions_builder(NamedSchemaVersions::new())) + assert_schema_compatibility( + comparison_settings, + ¤t, + &versions_builder(NamedSchemaVersions::new()), + ) } -pub fn assert_type_collection_backwards_compatibility< - E: CustomExtension ->( +pub fn assert_type_collection_backwards_compatibility( comparison_settings: &SchemaComparisonSettings, current: NamedTypesSchema, - versions_builder: impl FnOnce(NamedSchemaVersions>) -> NamedSchemaVersions>, -) -where + versions_builder: impl FnOnce( + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) where NamedTypesSchema: ComparisonSchema, { - assert_schema_compatibility(comparison_settings, ¤t, &versions_builder(NamedSchemaVersions::new())) + assert_schema_compatibility( + comparison_settings, + ¤t, + &versions_builder(NamedSchemaVersions::new()), + ) } -fn assert_schema_compatibility< - E: CustomExtension, - C: ComparisonSchema, ->( +fn assert_schema_compatibility>( schema_comparison_settings: &SchemaComparisonSettings, current: &C, named_versions: &NamedSchemaVersions, @@ -1576,15 +1743,19 @@ fn assert_schema_compatibility< // if not, output latest encoded. let Some((latest_version_name, latest_schema_version)) = named_versions.last() else { let mut error = String::new(); - writeln!(&mut error, "You must provide at least one named versioned schema to use this method.").unwrap(); + writeln!( + &mut error, + "You must provide at least one named versioned schema to use this method." + ) + .unwrap(); writeln!(&mut error, "Use a relevant name (for example, the current software version name), and save the current schema as follows:").unwrap(); writeln!(&mut error, "{}", current.encode_to_hex()).unwrap(); panic!("{error}"); }; // Part 1 - Check that latest is equal to the last historic schema version - let result = latest_schema_version - .compare_with(¤t, &SchemaComparisonSettings::require_equality()); + let result = + latest_schema_version.compare_with(¤t, &SchemaComparisonSettings::require_equality()); if let Some(error_message) = result.error_message(latest_version_name, "current") { let mut error = String::new(); @@ -1593,8 +1764,16 @@ fn assert_schema_compatibility< write!(&mut error, "{error_message}").unwrap(); writeln!(&mut error).unwrap(); writeln!(&mut error, "You will either want to:").unwrap(); - writeln!(&mut error, "(A) Add a new named version to the list, to be supported going forward.").unwrap(); - writeln!(&mut error, "(B) Replace the latest version. ONLY do this if the version has not yet been in use.").unwrap(); + writeln!( + &mut error, + "(A) Add a new named version to the list, to be supported going forward." + ) + .unwrap(); + writeln!( + &mut error, + "(B) Replace the latest version. ONLY do this if the version has not yet been in use." + ) + .unwrap(); writeln!(&mut error).unwrap(); writeln!(&mut error, "The latest version is:").unwrap(); writeln!(&mut error, "{}", current.encode_to_hex()).unwrap(); @@ -1612,7 +1791,6 @@ fn assert_schema_compatibility< } } - /// A serializable record of the schema of a single type. /// Intended for historical backwards compatibility checking of a single type. #[derive(Debug, Clone, Sbor)] @@ -1624,10 +1802,7 @@ pub struct SingleTypeSchema { impl SingleTypeSchema { pub fn new(schema: VersionedSchema, type_id: LocalTypeId) -> Self { - Self { - schema, - type_id, - } + Self { schema, type_id } } pub fn from> + ?Sized>() -> Self { @@ -1636,11 +1811,15 @@ impl SingleTypeSchema { } impl ComparisonSchema for SingleTypeSchema - where - ::CustomTypeKind: VecSbor, - ::CustomTypeValidation: VecSbor, +where + ::CustomTypeKind: VecSbor, + ::CustomTypeValidation: VecSbor, { - fn compare_with<'s>(&'s self, compared: &'s Self, settings: &SchemaComparisonSettings) -> SchemaComparisonResult<'s, E::CustomSchema> { + fn compare_with<'s>( + &'s self, + compared: &'s Self, + settings: &SchemaComparisonSettings, + ) -> SchemaComparisonResult<'s, E::CustomSchema> { SchemaComparisonKernel::new( &self.schema.as_unique_version(), &compared.schema.as_unique_version(), @@ -1655,7 +1834,8 @@ impl ComparisonSchema for SingleTypeSchema IntoSchema for SingleTypeSchema - where Self: ComparisonSchema +where + Self: ComparisonSchema, { fn into_schema(&self) -> Self { self.clone() @@ -1676,10 +1856,7 @@ pub struct NamedTypesSchema { impl NamedTypesSchema { pub fn new(schema: VersionedSchema, type_ids: IndexMap) -> Self { - Self { - schema, - type_ids, - } + Self { schema, type_ids } } pub fn from(aggregator: TypeAggregator>) -> Self { @@ -1688,24 +1865,27 @@ impl NamedTypesSchema { } impl ComparisonSchema for NamedTypesSchema - where - ::CustomTypeKind: VecSbor, - ::CustomTypeValidation: VecSbor, +where + ::CustomTypeKind: VecSbor, + ::CustomTypeValidation: VecSbor, { - fn compare_with<'s>(&'s self, compared: &'s Self, settings: &SchemaComparisonSettings) -> SchemaComparisonResult<'s, E::CustomSchema> { + fn compare_with<'s>( + &'s self, + compared: &'s Self, + settings: &SchemaComparisonSettings, + ) -> SchemaComparisonResult<'s, E::CustomSchema> { SchemaComparisonKernel::new( &self.schema.as_unique_version(), &compared.schema.as_unique_version(), settings, - ).compare_using_named_type_roots( - &self.type_ids, - &compared.type_ids, ) + .compare_using_named_type_roots(&self.type_ids, &compared.type_ids) } } impl IntoSchema for NamedTypesSchema - where Self: ComparisonSchema +where + Self: ComparisonSchema, { fn into_schema(&self) -> Self { self.clone() @@ -1730,14 +1910,20 @@ pub trait ComparisonSchema: Clone + VecSbor { Self::decode_from_bytes(&hex::decode(hex).unwrap()) } - fn compare_with<'s>(&'s self, compared: &'s Self, settings: &SchemaComparisonSettings) -> SchemaComparisonResult<'s, E::CustomSchema>; + fn compare_with<'s>( + &'s self, + compared: &'s Self, + settings: &SchemaComparisonSettings, + ) -> SchemaComparisonResult<'s, E::CustomSchema>; } pub trait IntoSchema, E: CustomExtension> { fn into_schema(&self) -> C; } -impl<'a, C: ComparisonSchema, E: CustomExtension, T: IntoSchema + ?Sized> IntoSchema for &'a T { +impl<'a, C: ComparisonSchema, E: CustomExtension, T: IntoSchema + ?Sized> IntoSchema + for &'a T +{ fn into_schema(&self) -> C { >::into_schema(*self) } @@ -1767,4 +1953,4 @@ impl, E: CustomExtension> IntoSchema for str { } } -// NOTE: Types are in sbor-tests/tests/schema_comparison.rs \ No newline at end of file +// NOTE: Types are in sbor-tests/tests/schema_comparison.rs diff --git a/sbor/src/schema/type_aggregator.rs b/sbor/src/schema/type_aggregator.rs index 3ea538d4b1c..e2848731be0 100644 --- a/sbor/src/schema/type_aggregator.rs +++ b/sbor/src/schema/type_aggregator.rs @@ -30,7 +30,7 @@ pub fn generate_full_schema( } fn generate_schema_from_types( - types: IndexMap, RustTypeId>> + types: IndexMap, RustTypeId>>, ) -> VersionedSchema { let type_count = types.len(); let type_indices = IndexSet::from_iter(types.keys().map(|k| k.clone())); @@ -166,7 +166,10 @@ impl> TypeAggregator { /// /// This is only intended for use when adding root types to schemas, /// /and should not be called from inside Describe macros. - pub fn add_named_root_type_and_descendents + ?Sized>(&mut self, name: impl Into) -> LocalTypeId { + pub fn add_named_root_type_and_descendents + ?Sized>( + &mut self, + name: impl Into, + ) -> LocalTypeId { let local_type_id = self.add_child_type_and_descendents::(); self.named_root_types.insert(name.into(), local_type_id); local_type_id @@ -236,8 +239,9 @@ impl> TypeAggregator { return true; } - pub fn generate_named_types_schema = C>>(self) - -> NamedTypesSchema { + pub fn generate_named_types_schema = C>>( + self, + ) -> NamedTypesSchema { NamedTypesSchema::new( generate_schema_from_types(self.types), self.named_root_types, diff --git a/sbor/src/schema/type_data/type_kind.rs b/sbor/src/schema/type_data/type_kind.rs index e1926b9bc56..50e43bf033f 100644 --- a/sbor/src/schema/type_data/type_kind.rs +++ b/sbor/src/schema/type_data/type_kind.rs @@ -92,7 +92,6 @@ pub enum TypeKindLabel { Custom(T), } - impl TypeKindLabel { pub fn name(&self) -> &'static str { match self { @@ -116,4 +115,4 @@ impl TypeKindLabel { TypeKindLabel::Custom(custom) => custom.name(), } } -} \ No newline at end of file +} diff --git a/sbor/src/schema/type_data/type_validation.rs b/sbor/src/schema/type_data/type_validation.rs index f081cb5c4f6..330507112a8 100644 --- a/sbor/src/schema/type_data/type_validation.rs +++ b/sbor/src/schema/type_data/type_validation.rs @@ -63,10 +63,7 @@ pub struct NumericValidation { impl NumericValidation { pub const fn with_bounds(min: Option, max: Option) -> Self { - Self { - min, - max, - } + Self { min, max } } pub const fn none() -> Self { diff --git a/sbor/src/traversal/typed/full_location.rs b/sbor/src/traversal/typed/full_location.rs index 5e68adc8508..e19ec2719d0 100644 --- a/sbor/src/traversal/typed/full_location.rs +++ b/sbor/src/traversal/typed/full_location.rs @@ -35,11 +35,10 @@ impl<'s, 'a, E: CustomExtension> PathAnnotate let container = match header { ContainerHeader::EnumVariant(variant_header) => { let discriminator = variant_header.variant; - let variant_data = metadata - .and_then(|m| m.get_enum_variant_data(discriminator)); - let variant_name = variant_data - .and_then(|d| d.get_name()) - .map(Cow::Borrowed); + let variant_data = + metadata.and_then(|m| m.get_enum_variant_data(discriminator)); + let variant_name = + variant_data.and_then(|d| d.get_name()).map(Cow::Borrowed); let field_index = current_child_index; let field_name = variant_data .and_then(|d| d.get_field_name(field_index)) diff --git a/sbor/src/vec_traits.rs b/sbor/src/vec_traits.rs index c9dd6b80aa8..85985d09b1f 100644 --- a/sbor/src/vec_traits.rs +++ b/sbor/src/vec_traits.rs @@ -1,9 +1,15 @@ -use crate::{internal_prelude::*, CustomExtension, CustomSchema, Decoder as _, Encoder as _, Describe, VecDecoder, VecEncoder}; +use crate::{ + internal_prelude::*, CustomExtension, CustomSchema, Decoder as _, Describe, Encoder as _, + VecDecoder, VecEncoder, +}; pub trait VecEncode: for<'a> Encode> {} impl Encode> + ?Sized> VecEncode for T {} -pub fn vec_encode + ?Sized>(value: &T, max_depth: usize) -> Result, EncodeError> { +pub fn vec_encode + ?Sized>( + value: &T, + max_depth: usize, +) -> Result, EncodeError> { let mut buf = Vec::with_capacity(512); let encoder = VecEncoder::<'_, E::CustomValueKind>::new(&mut buf, max_depth); encoder.encode_payload(value, E::PAYLOAD_PREFIX)?; @@ -13,7 +19,10 @@ pub fn vec_encode + ?Sized> pub trait VecDecode: for<'a> Decode> {} impl Decode>> VecDecode for T {} -pub fn vec_decode>(buf: &[u8], max_depth: usize) -> Result { +pub fn vec_decode>( + buf: &[u8], + max_depth: usize, +) -> Result { VecDecoder::<'_, E::CustomValueKind>::new(buf, max_depth).decode_payload(E::PAYLOAD_PREFIX) } @@ -22,12 +31,14 @@ pub trait VecSbor: + VecEncode + VecDecode + Describe<::CustomTypeKind> - {} +{ +} impl VecSbor for T - where - T: Categorize, - T: VecEncode, - T: VecDecode, - T: Describe<::CustomTypeKind>, -{} \ No newline at end of file +where + T: Categorize, + T: VecEncode, + T: VecDecode, + T: Describe<::CustomTypeKind>, +{ +} From 2922b67af0761ced2ee7119e8acaee12c8f2df1c Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 12:05:15 +0100 Subject: [PATCH 06/19] fix: Fix no_std build --- sbor/src/schema/schema_comparison.rs | 2 +- sbor/src/schema/type_aggregator.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sbor/src/schema/schema_comparison.rs b/sbor/src/schema/schema_comparison.rs index 931f096106a..d6905c6eefa 100644 --- a/sbor/src/schema/schema_comparison.rs +++ b/sbor/src/schema/schema_comparison.rs @@ -1655,7 +1655,7 @@ impl> NamedSchemaVersions { /// can be an extension (e.g. enums can have new variants) /// /// The indexmap should be a map from a version name to a hex-encoded -/// basic-sbor-encoded `SingleTypeSchemaVersion` (with a single type). +/// basic-sbor-encoded `SingleTypeSchema` (with a single type). /// The version name is only used for a more useful message on error. /// /// ## Example usage diff --git a/sbor/src/schema/type_aggregator.rs b/sbor/src/schema/type_aggregator.rs index e2848731be0..8b83e32f639 100644 --- a/sbor/src/schema/type_aggregator.rs +++ b/sbor/src/schema/type_aggregator.rs @@ -1,5 +1,4 @@ use super::*; -use indexmap::IndexMap; use sbor::rust::prelude::*; pub fn generate_full_schema_from_single_type< From cdf64a3b8f30c9fb27232ab09bad1763daea5a80 Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 12:26:52 +0100 Subject: [PATCH 07/19] fix: Fix Radix Engine test asserting on exact payload validation error message --- radix-engine-tests/tests/system/data_validation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radix-engine-tests/tests/system/data_validation.rs b/radix-engine-tests/tests/system/data_validation.rs index 6d5ed3d1f47..7bd0e6fddee 100644 --- a/radix-engine-tests/tests/system/data_validation.rs +++ b/radix-engine-tests/tests/system/data_validation.rs @@ -366,7 +366,7 @@ fn vec_of_u8_underflow_should_not_cause_panic() { RuntimeError::SystemError(SystemError::TypeCheckError(TypeCheckError::KeyValueStorePayloadValidationError( KeyOrValue::Value, e ))) - if e.eq("[ERROR] byte offset: 7-7, value path: Array, cause: DecodeError(BufferUnderflow { required: 99999993, remaining: 1048569 })") => true, + if e.eq("[ERROR] byte offset: 7-7, value path: Array.[99999992], cause: DecodeError(BufferUnderflow { required: 99999993, remaining: 1048569 })") => true, _ => false, }) } From f9c6fa6994568df3977eb2416116ec9a847e998f Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 13:06:32 +0100 Subject: [PATCH 08/19] fix: Fix scrypto_describe derive tests --- radix-sbor-derive/src/scrypto_describe.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radix-sbor-derive/src/scrypto_describe.rs b/radix-sbor-derive/src/scrypto_describe.rs index c77675541fc..b0215006290 100644 --- a/radix-sbor-derive/src/scrypto_describe.rs +++ b/radix-sbor-derive/src/scrypto_describe.rs @@ -64,7 +64,7 @@ mod tests { { const TYPE_ID: sbor::RustTypeId = sbor::RustTypeId::novel_with_code( "Thing", - &[::TYPE_ID,], + &[>::TYPE_ID,], &#code_hash ); fn type_data() -> sbor::TypeData { @@ -100,7 +100,7 @@ mod tests { { const TYPE_ID: sbor::RustTypeId = sbor::RustTypeId::novel_with_code( "MyEnum", - &[::TYPE_ID,], + &[>::TYPE_ID,], &#code_hash ); fn type_data() -> sbor::TypeData { From ec8ab9c60d2d9860a2789bf7a3225f6eb49d4ca0 Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 13:56:57 +0100 Subject: [PATCH 09/19] refactor: Remove some type hell with SchemaTypeLinks --- radix-clis/src/scrypto_bindgen/mod.rs | 2 +- .../src/data/manifest/custom_extension.rs | 7 ++--- .../src/data/scrypto/custom_extension.rs | 7 ++--- .../src/data/scrypto/custom_schema.rs | 28 +++++++++---------- .../data/scrypto/custom_well_known_types.rs | 2 +- .../tests/system/schema_sanity_check.rs | 4 +-- sbor/src/basic.rs | 26 +++++++++-------- .../payload_validation/payload_validator.rs | 4 +-- sbor/src/schema/custom_traits.rs | 28 ++++++++++--------- sbor/src/schema/schema.rs | 18 ++++-------- sbor/src/schema/schema_comparison.rs | 26 ++++++++--------- sbor/src/schema/schema_validation/mod.rs | 2 +- .../schema_validation/type_kind_validation.rs | 2 +- .../type_metadata_validation.rs | 2 +- .../type_validation_validation.rs | 2 +- sbor/src/schema/type_aggregator.rs | 22 +++++++-------- sbor/src/schema/type_data/mod.rs | 4 +++ sbor/src/schema/type_data/type_kind.rs | 6 +++- sbor/src/schema/type_link.rs | 3 ++ sbor/src/traversal/typed/typed_traverser.rs | 2 +- sbor/src/vec_traits.rs | 4 +-- scrypto-bindgen/src/schema.rs | 2 +- 22 files changed, 102 insertions(+), 101 deletions(-) diff --git a/radix-clis/src/scrypto_bindgen/mod.rs b/radix-clis/src/scrypto_bindgen/mod.rs index 922cca81006..9f755442d79 100644 --- a/radix-clis/src/scrypto_bindgen/mod.rs +++ b/radix-clis/src/scrypto_bindgen/mod.rs @@ -105,7 +105,7 @@ where fn resolve_type_kind( &self, type_identifier: &ScopedTypeId, - ) -> Result, schema::SchemaError> { + ) -> Result, schema::SchemaError> { self.lookup_schema(&type_identifier.0) .ok_or(schema::SchemaError::FailedToGetSchemaFromSchemaHash)? .as_latest_version() diff --git a/radix-common/src/data/manifest/custom_extension.rs b/radix-common/src/data/manifest/custom_extension.rs index b2324d77217..aeb7475cf99 100644 --- a/radix-common/src/data/manifest/custom_extension.rs +++ b/radix-common/src/data/manifest/custom_extension.rs @@ -14,10 +14,7 @@ impl CustomExtension for ManifestCustomExtension { fn custom_value_kind_matches_type_kind( schema: &Schema, custom_value_kind: Self::CustomValueKind, - type_kind: &TypeKind< - ::CustomTypeKind, - LocalTypeId, - >, + type_kind: &LocalTypeKind, ) -> bool { match custom_value_kind { ManifestCustomValueKind::Address => matches!( @@ -67,7 +64,7 @@ impl CustomExtension for ManifestCustomExtension { fn custom_type_kind_matches_non_custom_value_kind( _: &Schema, - _: &::CustomTypeKind, + _: &::CustomLocalTypeKind, _: ValueKind, ) -> bool { // No custom type kinds can match non-custom value kinds diff --git a/radix-common/src/data/scrypto/custom_extension.rs b/radix-common/src/data/scrypto/custom_extension.rs index 97374552508..998eedb83fc 100644 --- a/radix-common/src/data/scrypto/custom_extension.rs +++ b/radix-common/src/data/scrypto/custom_extension.rs @@ -13,10 +13,7 @@ impl CustomExtension for ScryptoCustomExtension { fn custom_value_kind_matches_type_kind( _: &Schema, custom_value_kind: Self::CustomValueKind, - type_kind: &TypeKind< - ::CustomTypeKind, - LocalTypeId, - >, + type_kind: &LocalTypeKind, ) -> bool { match custom_value_kind { ScryptoCustomValueKind::Reference => matches!( @@ -42,7 +39,7 @@ impl CustomExtension for ScryptoCustomExtension { fn custom_type_kind_matches_non_custom_value_kind( _: &Schema, - _: &::CustomTypeKind, + _: &::CustomLocalTypeKind, _: ValueKind, ) -> bool { // It's not possible for a custom type kind to match a non-custom value kind diff --git a/radix-common/src/data/scrypto/custom_schema.rs b/radix-common/src/data/scrypto/custom_schema.rs index b532bcdec44..b4b0fa83404 100644 --- a/radix-common/src/data/scrypto/custom_schema.rs +++ b/radix-common/src/data/scrypto/custom_schema.rs @@ -1,9 +1,14 @@ use crate::internal_prelude::*; pub type ScryptoTypeKind = TypeKind; +pub type ScryptoLocalTypeKind = LocalTypeKind; +pub type ScryptoAggregatorTypeKind = AggregatorTypeKind; pub type VersionedScryptoSchema = VersionedSchema; pub type ScryptoSchema = Schema; pub type ScryptoTypeData = TypeData; +pub type ScryptoLocalTypeData = LocalTypeData; +pub type ScryptoAggregatorTypeData = AggregatorTypeData; +pub type ScryptoTypeValidation = TypeValidation; /// A schema for the values that a codec can decode / views as valid #[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoSbor)] @@ -217,32 +222,27 @@ lazy_static::lazy_static! { } impl CustomSchema for ScryptoCustomSchema { - type CustomTypeKind = ScryptoCustomTypeKind; + type CustomLocalTypeKind = ScryptoCustomTypeKind; + type CustomAggregatorTypeKind = ScryptoCustomTypeKind; type CustomTypeKindLabel = ScryptoCustomTypeKindLabel; type CustomTypeValidation = ScryptoCustomTypeValidation; fn linearize_type_kind( - type_kind: Self::CustomTypeKind, + type_kind: Self::CustomLocalTypeKind, _type_indices: &IndexSet, - ) -> Self::CustomTypeKind { - match type_kind { - ScryptoCustomTypeKind::Reference => ScryptoCustomTypeKind::Reference, - ScryptoCustomTypeKind::Own => ScryptoCustomTypeKind::Own, - ScryptoCustomTypeKind::Decimal => ScryptoCustomTypeKind::Decimal, - ScryptoCustomTypeKind::PreciseDecimal => ScryptoCustomTypeKind::PreciseDecimal, - ScryptoCustomTypeKind::NonFungibleLocalId => ScryptoCustomTypeKind::NonFungibleLocalId, - } + ) -> Self::CustomAggregatorTypeKind { + type_kind } fn resolve_well_known_type( well_known_id: WellKnownTypeId, - ) -> Option<&'static TypeData, LocalTypeId>> { + ) -> Option<&'static LocalTypeData> { resolve_scrypto_well_known_type(well_known_id) } fn validate_custom_type_kind( _context: &SchemaContext, - type_kind: &Self::CustomTypeKind, + type_kind: &Self::CustomLocalTypeKind, ) -> Result<(), SchemaValidationError> { match type_kind { ScryptoCustomTypeKind::Reference @@ -258,7 +258,7 @@ impl CustomSchema for ScryptoCustomSchema { fn validate_type_metadata_with_custom_type_kind( _: &SchemaContext, - type_kind: &Self::CustomTypeKind, + type_kind: &Self::CustomLocalTypeKind, type_metadata: &TypeMetadata, ) -> Result<(), SchemaValidationError> { // Even though they all map to the same thing, we keep the explicit match statement so that @@ -277,7 +277,7 @@ impl CustomSchema for ScryptoCustomSchema { fn validate_custom_type_validation( _context: &SchemaContext, - custom_type_kind: &Self::CustomTypeKind, + custom_type_kind: &Self::CustomLocalTypeKind, custom_type_validation: &Self::CustomTypeValidation, ) -> Result<(), SchemaValidationError> { match custom_type_kind { diff --git a/radix-common/src/data/scrypto/custom_well_known_types.rs b/radix-common/src/data/scrypto/custom_well_known_types.rs index bdef76f0a21..08274873e77 100644 --- a/radix-common/src/data/scrypto/custom_well_known_types.rs +++ b/radix-common/src/data/scrypto/custom_well_known_types.rs @@ -572,7 +572,7 @@ create_well_known_lookup!( pub fn resolve_scrypto_well_known_type( well_known_index: WellKnownTypeId, -) -> Option<&'static TypeData> { +) -> Option<&'static ScryptoLocalTypeData> { WELL_KNOWN_LOOKUP .get(well_known_index.as_index()) .and_then(|x| x.as_ref()) diff --git a/radix-engine-tests/tests/system/schema_sanity_check.rs b/radix-engine-tests/tests/system/schema_sanity_check.rs index d5b2d74a118..00d7b0285fb 100644 --- a/radix-engine-tests/tests/system/schema_sanity_check.rs +++ b/radix-engine-tests/tests/system/schema_sanity_check.rs @@ -465,11 +465,11 @@ fn native_blueprints_with_typed_addresses_have_expected_schema() { assert!(matches!( type_kind, - TypeKind::::Custom(ScryptoCustomTypeKind::Reference) + ScryptoLocalTypeKind::Custom(ScryptoCustomTypeKind::Reference) )); assert!(matches!( type_validation, - TypeValidation::::Custom( + ScryptoTypeValidation::Custom( ScryptoCustomTypeValidation::Reference(ReferenceValidation::IsGlobalTyped( Some(ACCOUNT_PACKAGE), bp_name diff --git a/sbor/src/basic.rs b/sbor/src/basic.rs index 824661871df..1a4206c2e66 100644 --- a/sbor/src/basic.rs +++ b/sbor/src/basic.rs @@ -205,20 +205,21 @@ lazy_static::lazy_static! { pub enum NoCustomSchema {} impl CustomSchema for NoCustomSchema { - type CustomTypeKind = NoCustomTypeKind; + type CustomLocalTypeKind = NoCustomTypeKind; + type CustomAggregatorTypeKind = NoCustomTypeKind; type CustomTypeKindLabel = NoCustomTypeKindLabel; type CustomTypeValidation = NoCustomTypeValidation; fn linearize_type_kind( - _: Self::CustomTypeKind, + _: Self::CustomAggregatorTypeKind, _: &IndexSet, - ) -> Self::CustomTypeKind { + ) -> Self::CustomLocalTypeKind { unreachable!("No custom type kinds exist") } fn resolve_well_known_type( well_known_id: WellKnownTypeId, - ) -> Option<&'static TypeData, LocalTypeId>> { + ) -> Option<&'static LocalTypeData> { WELL_KNOWN_LOOKUP .get(well_known_id.as_index()) .and_then(|x| x.as_ref()) @@ -226,7 +227,7 @@ impl CustomSchema for NoCustomSchema { fn validate_custom_type_validation( _: &SchemaContext, - _: &Self::CustomTypeKind, + _: &Self::CustomLocalTypeKind, _: &Self::CustomTypeValidation, ) -> Result<(), SchemaValidationError> { unreachable!("No custom type validation") @@ -234,14 +235,14 @@ impl CustomSchema for NoCustomSchema { fn validate_custom_type_kind( _: &SchemaContext, - _: &Self::CustomTypeKind, + _: &Self::CustomLocalTypeKind, ) -> Result<(), SchemaValidationError> { unreachable!("No custom type kinds exist") } fn validate_type_metadata_with_custom_type_kind( _: &SchemaContext, - _: &Self::CustomTypeKind, + _: &Self::CustomLocalTypeKind, _: &TypeMetadata, ) -> Result<(), SchemaValidationError> { unreachable!("No custom type kinds exist") @@ -271,17 +272,14 @@ impl CustomExtension for NoCustomExtension { fn custom_value_kind_matches_type_kind( _: &Schema, _: Self::CustomValueKind, - _: &TypeKind< - ::CustomTypeKind, - LocalTypeId, - >, + _: &TypeKind<::CustomLocalTypeKind, LocalTypeId>, ) -> bool { unreachable!("No custom value kinds exist") } fn custom_type_kind_matches_non_custom_value_kind( _: &Schema, - _: &::CustomTypeKind, + _: &::CustomLocalTypeKind, _: ValueKind, ) -> bool { unreachable!("No custom type kinds exist") @@ -293,9 +291,13 @@ pub type BasicOwnedRawPayload = RawPayload<'static, NoCustomExtension>; pub type BasicRawValue<'a> = RawValue<'a, NoCustomExtension>; pub type BasicOwnedRawValue = RawValue<'static, NoCustomExtension>; pub type BasicTypeKind = TypeKind; +pub type BasicLocalTypeKind = LocalTypeKind; +pub type BasicAggregatorTypeKind = AggregatorTypeKind; pub type BasicSchema = Schema; pub type BasicVersionedSchema = VersionedSchema; pub type BasicTypeData = TypeData; +pub type BasicLocalTypeData = LocalTypeData; +pub type BasicAggregatorTypeData = LocalTypeData; impl<'a> CustomDisplayContext<'a> for () { type CustomExtension = NoCustomExtension; diff --git a/sbor/src/payload_validation/payload_validator.rs b/sbor/src/payload_validation/payload_validator.rs index 50a12a288e3..4292de33181 100644 --- a/sbor/src/payload_validation/payload_validator.rs +++ b/sbor/src/payload_validation/payload_validator.rs @@ -445,7 +445,7 @@ mod tests { #[test] pub fn test_vec_u8_with_min_max() { - let t0 = BasicTypeData { + let t0 = BasicLocalTypeData { kind: BasicTypeKind::Array { element_type: LocalTypeId::SchemaLocalIndex(1), }, @@ -455,7 +455,7 @@ mod tests { max: 1.into(), }), }; - let t1 = BasicTypeData { + let t1 = BasicLocalTypeData { kind: BasicTypeKind::U8, metadata: TypeMetadata::unnamed(), validation: TypeValidation::U8(NumericValidation { diff --git a/sbor/src/schema/custom_traits.rs b/sbor/src/schema/custom_traits.rs index 3b3c31c7e9a..401f7e648a0 100644 --- a/sbor/src/schema/custom_traits.rs +++ b/sbor/src/schema/custom_traits.rs @@ -19,22 +19,27 @@ pub trait CustomTypeValidation: Debug + Clone + PartialEq + Eq { pub trait CustomSchema: Debug + Clone + Copy + PartialEq + Eq + 'static { type CustomTypeValidation: CustomTypeValidation; - type CustomTypeKind: CustomTypeKind< - L, + type CustomLocalTypeKind: CustomTypeKind< + LocalTypeId, + CustomTypeValidation = Self::CustomTypeValidation, + CustomTypeKindLabel = Self::CustomTypeKindLabel, + >; + type CustomAggregatorTypeKind: CustomTypeKind< + RustTypeId, CustomTypeValidation = Self::CustomTypeValidation, CustomTypeKindLabel = Self::CustomTypeKindLabel, >; type CustomTypeKindLabel: CustomTypeKindLabel; fn linearize_type_kind( - type_kind: Self::CustomTypeKind, + type_kind: Self::CustomAggregatorTypeKind, type_indices: &IndexSet, - ) -> Self::CustomTypeKind; + ) -> Self::CustomLocalTypeKind; // Note - each custom type extension should have its own cache fn resolve_well_known_type( well_known_id: WellKnownTypeId, - ) -> Option<&'static TypeData, LocalTypeId>>; + ) -> Option<&'static LocalTypeData>; /// Used when validating schemas are self-consistent. /// @@ -42,7 +47,7 @@ pub trait CustomSchema: Debug + Clone + Copy + PartialEq + Eq + 'static { /// e.g. to check if an offset is out of bounds. fn validate_custom_type_kind( context: &SchemaContext, - custom_type_kind: &Self::CustomTypeKind, + custom_type_kind: &Self::CustomLocalTypeKind, ) -> Result<(), SchemaValidationError>; /// Used when validating schemas are self-consistent. @@ -51,7 +56,7 @@ pub trait CustomSchema: Debug + Clone + Copy + PartialEq + Eq + 'static { /// Note that custom type validation can only be associated with custom type kind. fn validate_custom_type_validation( context: &SchemaContext, - custom_type_kind: &Self::CustomTypeKind, + custom_type_kind: &Self::CustomLocalTypeKind, custom_type_validation: &Self::CustomTypeValidation, ) -> Result<(), SchemaValidationError>; @@ -60,7 +65,7 @@ pub trait CustomSchema: Debug + Clone + Copy + PartialEq + Eq + 'static { /// Verifies if the metadata is appropriate for the custom type kind. fn validate_type_metadata_with_custom_type_kind( context: &SchemaContext, - custom_type_kind: &Self::CustomTypeKind, + custom_type_kind: &Self::CustomLocalTypeKind, type_metadata: &TypeMetadata, ) -> Result<(), SchemaValidationError>; @@ -82,10 +87,7 @@ pub trait CustomExtension: Debug + Clone + PartialEq + Eq + 'static { fn custom_value_kind_matches_type_kind( schema: &Schema, custom_value_kind: Self::CustomValueKind, - type_kind: &TypeKind< - ::CustomTypeKind, - LocalTypeId, - >, + type_kind: &LocalTypeKind, ) -> bool; /// Used in the typed traverser @@ -95,7 +97,7 @@ pub trait CustomExtension: Debug + Clone + PartialEq + Eq + 'static { /// value kinds (in most cases there won't be any such cases). fn custom_type_kind_matches_non_custom_value_kind( schema: &Schema, - custom_type_kind: &::CustomTypeKind, + custom_type_kind: &::CustomLocalTypeKind, non_custom_value_kind: ValueKind, ) -> bool; } diff --git a/sbor/src/schema/schema.rs b/sbor/src/schema/schema.rs index 3426ca31464..59250f5b7f8 100644 --- a/sbor/src/schema/schema.rs +++ b/sbor/src/schema/schema.rs @@ -3,7 +3,7 @@ use crate::*; define_single_versioned!( #[derive(Debug, Clone, PartialEq, Eq, Sbor)] - #[sbor(child_types = "S::CustomTypeKind, S::CustomTypeValidation")] + #[sbor(child_types = "S::CustomLocalTypeKind, S::CustomTypeValidation")] pub VersionedSchema(SchemaVersions) => Schema = SchemaV1:: ); @@ -32,19 +32,16 @@ impl Default for VersionedSchema { /// An array of custom type kinds, and associated extra information which can attach to the type kinds #[derive(Debug, Clone, PartialEq, Eq, Sbor)] // NB - the generic parameter E isn't embedded in the value model itself - instead: -// * Via TypeKind, S::CustomTypeKind gets embedded +// * Via TypeKind, S::CustomLocalTypeKind gets embedded // * Via TypeValidation, S::CustomTypeValidation gets embedded // So theses are the child types which need to be registered with the sbor macro for it to compile -#[sbor(child_types = "S::CustomTypeKind, S::CustomTypeValidation")] +#[sbor(child_types = "S::CustomLocalTypeKind, S::CustomTypeValidation")] pub struct SchemaV1 { - pub type_kinds: Vec>, + pub type_kinds: Vec>, pub type_metadata: Vec, // TODO: reconsider adding type hash when it's ready! pub type_validations: Vec>, } -pub type SchemaTypeKind = - TypeKind<::CustomTypeKind, LocalTypeId>; - impl SchemaV1 { pub fn empty() -> Self { Self { @@ -54,10 +51,7 @@ impl SchemaV1 { } } - pub fn resolve_type_kind( - &self, - type_id: LocalTypeId, - ) -> Option<&TypeKind, LocalTypeId>> { + pub fn resolve_type_kind(&self, type_id: LocalTypeId) -> Option<&LocalTypeKind> { match type_id { LocalTypeId::WellKnown(index) => { S::resolve_well_known_type(index).map(|data| &data.kind) @@ -152,7 +146,7 @@ impl SchemaV1 { &self, type_id: LocalTypeId, ) -> Option<( - &TypeKind, LocalTypeId>, + &LocalTypeKind, &TypeMetadata, &TypeValidation, )> { diff --git a/sbor/src/schema/schema_comparison.rs b/sbor/src/schema/schema_comparison.rs index d6905c6eefa..57342ecbe72 100644 --- a/sbor/src/schema/schema_comparison.rs +++ b/sbor/src/schema/schema_comparison.rs @@ -630,8 +630,8 @@ impl TypeKindComparisonResult { fn with_mismatch_error( mut self, - base_type_kind: &TypeKind, LocalTypeId>, - compared_type_kind: &TypeKind, LocalTypeId>, + base_type_kind: &LocalTypeKind, + compared_type_kind: &LocalTypeKind, ) -> Self { self.add_error(SchemaComparisonErrorDetail::TypeKindMismatch { base: base_type_kind.label(), @@ -1071,8 +1071,8 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { fn compare_type_kind_internal( &self, - base_type_kind: &TypeKind, LocalTypeId>, - compared_type_kind: &TypeKind, LocalTypeId>, + base_type_kind: &LocalTypeKind, + compared_type_kind: &LocalTypeKind, ) -> TypeKindComparisonResult { // The returned children to check should be driven from the base type kind, // because these are the children where we have to maintain backwards-compatibility @@ -1271,7 +1271,7 @@ impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { fn compare_type_metadata_internal( &self, - base_type_kind: &TypeKind, LocalTypeId>, + base_type_kind: &LocalTypeKind, base_type_metadata: &TypeMetadata, compared_type_metadata: &TypeMetadata, ) -> TypeMetadataComparisonResult { @@ -1680,7 +1680,7 @@ impl> NamedSchemaVersions { /// ``` pub fn assert_type_backwards_compatibility< E: CustomExtension, - T: Describe<::CustomTypeKind>, + T: Describe<::CustomAggregatorTypeKind>, >( versions_builder: impl FnOnce( NamedSchemaVersions>, @@ -1696,7 +1696,7 @@ pub fn assert_type_backwards_compatibility< pub fn assert_type_compatibility< E: CustomExtension, - T: Describe<::CustomTypeKind>, + T: Describe<::CustomAggregatorTypeKind>, >( comparison_settings: &SchemaComparisonSettings, versions_builder: impl FnOnce( @@ -1794,7 +1794,7 @@ fn assert_schema_compatibility>( /// A serializable record of the schema of a single type. /// Intended for historical backwards compatibility checking of a single type. #[derive(Debug, Clone, Sbor)] -#[sbor(child_types = "S::CustomTypeKind, S::CustomTypeValidation")] +#[sbor(child_types = "S::CustomLocalTypeKind, S::CustomTypeValidation")] pub struct SingleTypeSchema { pub schema: VersionedSchema, pub type_id: LocalTypeId, @@ -1805,14 +1805,14 @@ impl SingleTypeSchema { Self { schema, type_id } } - pub fn from> + ?Sized>() -> Self { + pub fn from + ?Sized>() -> Self { generate_single_type_schema::() } } impl ComparisonSchema for SingleTypeSchema where - ::CustomTypeKind: VecSbor, + ::CustomLocalTypeKind: VecSbor, ::CustomTypeValidation: VecSbor, { fn compare_with<'s>( @@ -1848,7 +1848,7 @@ where /// /// For example, traits, or blueprint interfaces. #[derive(Debug, Clone, Sbor)] -#[sbor(child_types = "S::CustomTypeKind, S::CustomTypeValidation")] +#[sbor(child_types = "S::CustomLocalTypeKind, S::CustomTypeValidation")] pub struct NamedTypesSchema { pub schema: VersionedSchema, pub type_ids: IndexMap, @@ -1859,14 +1859,14 @@ impl NamedTypesSchema { Self { schema, type_ids } } - pub fn from(aggregator: TypeAggregator>) -> Self { + pub fn from(aggregator: TypeAggregator) -> Self { aggregator.generate_named_types_schema::() } } impl ComparisonSchema for NamedTypesSchema where - ::CustomTypeKind: VecSbor, + ::CustomLocalTypeKind: VecSbor, ::CustomTypeValidation: VecSbor, { fn compare_with<'s>( diff --git a/sbor/src/schema/schema_validation/mod.rs b/sbor/src/schema/schema_validation/mod.rs index 272e9e254fb..a80c1e5e487 100644 --- a/sbor/src/schema/schema_validation/mod.rs +++ b/sbor/src/schema/schema_validation/mod.rs @@ -70,7 +70,7 @@ mod tests { use super::*; use crate::rust::prelude::*; - fn create_schema(type_data: Vec>) -> BasicSchema { + fn create_schema(type_data: Vec) -> BasicSchema { let mut type_kinds = vec![]; let mut type_metadata = vec![]; let mut type_validations = vec![]; diff --git a/sbor/src/schema/schema_validation/type_kind_validation.rs b/sbor/src/schema/schema_validation/type_kind_validation.rs index be44ed927eb..d4dc7fde9cb 100644 --- a/sbor/src/schema/schema_validation/type_kind_validation.rs +++ b/sbor/src/schema/schema_validation/type_kind_validation.rs @@ -5,7 +5,7 @@ pub const MAX_NUMBER_OF_FIELDS: usize = 1024; pub fn validate_type_kind<'a, S: CustomSchema>( context: &SchemaContext, - type_kind: &SchemaTypeKind, + type_kind: &LocalTypeKind, ) -> Result<(), SchemaValidationError> { match type_kind { TypeKind::Any diff --git a/sbor/src/schema/schema_validation/type_metadata_validation.rs b/sbor/src/schema/schema_validation/type_metadata_validation.rs index 8cd8266e7fc..5df12d51a9a 100644 --- a/sbor/src/schema/schema_validation/type_metadata_validation.rs +++ b/sbor/src/schema/schema_validation/type_metadata_validation.rs @@ -4,7 +4,7 @@ use crate::schema::*; pub fn validate_type_metadata_with_type_kind<'a, S: CustomSchema>( context: &SchemaContext, - type_kind: &SchemaTypeKind, + type_kind: &LocalTypeKind, type_metadata: &TypeMetadata, ) -> Result<(), SchemaValidationError> { match type_kind { diff --git a/sbor/src/schema/schema_validation/type_validation_validation.rs b/sbor/src/schema/schema_validation/type_validation_validation.rs index d8a500d0fa0..13243833fc9 100644 --- a/sbor/src/schema/schema_validation/type_validation_validation.rs +++ b/sbor/src/schema/schema_validation/type_validation_validation.rs @@ -3,7 +3,7 @@ use crate::schema::*; pub fn validate_custom_type_validation<'a, S: CustomSchema>( context: &SchemaContext, - type_kind: &SchemaTypeKind, + type_kind: &LocalTypeKind, type_validation: &TypeValidation, ) -> Result<(), SchemaValidationError> { // It's always possible to opt into no additional validation. diff --git a/sbor/src/schema/type_aggregator.rs b/sbor/src/schema/type_aggregator.rs index 8b83e32f639..544b7904c28 100644 --- a/sbor/src/schema/type_aggregator.rs +++ b/sbor/src/schema/type_aggregator.rs @@ -2,7 +2,7 @@ use super::*; use sbor::rust::prelude::*; pub fn generate_full_schema_from_single_type< - T: Describe> + ?Sized, + T: Describe + ?Sized, S: CustomSchema, >() -> (LocalTypeId, VersionedSchema) { let mut aggregator = TypeAggregator::new(); @@ -11,7 +11,7 @@ pub fn generate_full_schema_from_single_type< } pub fn generate_single_type_schema< - T: Describe> + ?Sized, + T: Describe + ?Sized, S: CustomSchema, >() -> SingleTypeSchema { let (type_id, schema) = generate_full_schema_from_single_type::(); @@ -23,13 +23,13 @@ pub fn generate_single_type_schema< /// also captures named root types to give more structure to enable schema /// comparisons over time. pub fn generate_full_schema( - aggregator: TypeAggregator>, + aggregator: TypeAggregator, ) -> VersionedSchema { generate_schema_from_types(aggregator.types) } fn generate_schema_from_types( - types: IndexMap, RustTypeId>>, + types: IndexMap>, ) -> VersionedSchema { let type_count = types.len(); let type_indices = IndexSet::from_iter(types.keys().map(|k| k.clone())); @@ -52,8 +52,8 @@ fn generate_schema_from_types( } pub fn localize_well_known_type_data( - type_data: TypeData, RustTypeId>, -) -> TypeData, LocalTypeId> { + type_data: AggregatorTypeData, +) -> LocalTypeData { let TypeData { kind, metadata, @@ -66,16 +66,14 @@ pub fn localize_well_known_type_data( } } -pub fn localize_well_known( - type_kind: TypeKind, RustTypeId>, -) -> TypeKind, LocalTypeId> { +pub fn localize_well_known(type_kind: AggregatorTypeKind) -> LocalTypeKind { linearize::(type_kind, &indexset!()) } fn linearize( - type_kind: TypeKind, RustTypeId>, + type_kind: AggregatorTypeKind, type_indices: &IndexSet, -) -> TypeKind, LocalTypeId> { +) -> LocalTypeKind { match type_kind { TypeKind::Any => TypeKind::Any, TypeKind::Bool => TypeKind::Bool, @@ -238,7 +236,7 @@ impl> TypeAggregator { return true; } - pub fn generate_named_types_schema = C>>( + pub fn generate_named_types_schema>( self, ) -> NamedTypesSchema { NamedTypesSchema::new( diff --git a/sbor/src/schema/type_data/mod.rs b/sbor/src/schema/type_data/mod.rs index 0185a528cd1..b0216e0b05c 100644 --- a/sbor/src/schema/type_data/mod.rs +++ b/sbor/src/schema/type_data/mod.rs @@ -9,6 +9,10 @@ pub use type_kind::*; pub use type_metadata::*; pub use type_validation::*; +pub type LocalTypeData = TypeData<::CustomLocalTypeKind, LocalTypeId>; +pub type AggregatorTypeData = + TypeData<::CustomAggregatorTypeKind, RustTypeId>; + /// Combines all data about a Type: /// * `kind` - The type's [`TypeKind`] - this is essentially the definition of the structure of the type, /// and includes the type's `ValueKind` as well as the [`TypeKind`] of any child types. diff --git a/sbor/src/schema/type_data/type_kind.rs b/sbor/src/schema/type_data/type_kind.rs index 50e43bf033f..746fc962555 100644 --- a/sbor/src/schema/type_data/type_kind.rs +++ b/sbor/src/schema/type_data/type_kind.rs @@ -2,9 +2,13 @@ use super::*; use crate::rust::collections::IndexMap; use crate::rust::vec::Vec; +pub type LocalTypeKind = TypeKind<::CustomLocalTypeKind, LocalTypeId>; +pub type AggregatorTypeKind = + TypeKind<::CustomAggregatorTypeKind, RustTypeId>; + /// A schema for the values that a codec can decode / views as valid #[derive(Debug, Clone, PartialEq, Eq, Sbor)] -#[sbor(child_types = "T,L", categorize_types = "L")] +#[sbor(child_types = "T, L", categorize_types = "L")] pub enum TypeKind, L: SchemaTypeLink> { Any, diff --git a/sbor/src/schema/type_link.rs b/sbor/src/schema/type_link.rs index cc980588481..e9ca554c308 100644 --- a/sbor/src/schema/type_link.rs +++ b/sbor/src/schema/type_link.rs @@ -7,6 +7,9 @@ use sbor::*; /// - [`LocalTypeId`]: A link in the context of a schema (a well known id, or a local type index) pub trait SchemaTypeLink: Debug + Clone + PartialEq + Eq + From {} +/// A newer alias for `RustTypeId`. +pub type AggregatorTypeId = RustTypeId; + /// This is a compile-time identifier for a given type, used by the type aggregator /// to uniquely identify a type. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Sbor)] diff --git a/sbor/src/traversal/typed/typed_traverser.rs b/sbor/src/traversal/typed/typed_traverser.rs index 1c3c5251f3d..ed61d4eb938 100644 --- a/sbor/src/traversal/typed/typed_traverser.rs +++ b/sbor/src/traversal/typed/typed_traverser.rs @@ -522,7 +522,7 @@ impl<'s, E: CustomExtension> TypedTraverserState<'s, E> { fn value_kind_matches_type_kind( schema: &Schema, value_kind: ValueKind, - type_kind: &SchemaTypeKind, + type_kind: &LocalTypeKind, ) -> bool { if matches!(type_kind, TypeKind::Any) { return true; diff --git a/sbor/src/vec_traits.rs b/sbor/src/vec_traits.rs index 85985d09b1f..ac491f3a016 100644 --- a/sbor/src/vec_traits.rs +++ b/sbor/src/vec_traits.rs @@ -30,7 +30,7 @@ pub trait VecSbor: Categorize + VecEncode + VecDecode - + Describe<::CustomTypeKind> + + Describe<::CustomAggregatorTypeKind> { } @@ -39,6 +39,6 @@ where T: Categorize, T: VecEncode, T: VecDecode, - T: Describe<::CustomTypeKind>, + T: Describe<::CustomAggregatorTypeKind>, { } diff --git a/scrypto-bindgen/src/schema.rs b/scrypto-bindgen/src/schema.rs index ee78bc70b4b..0f1287101b6 100644 --- a/scrypto-bindgen/src/schema.rs +++ b/scrypto-bindgen/src/schema.rs @@ -10,7 +10,7 @@ pub trait PackageSchemaResolver { fn resolve_type_kind( &self, type_identifier: &ScopedTypeId, - ) -> Result, SchemaError>; + ) -> Result, SchemaError>; fn resolve_type_metadata( &self, From 12e746a8c4a34cfefd5af692e899b09b67ae916f Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 15:26:35 +0100 Subject: [PATCH 10/19] refactor: Improve type hell in schema comparison --- .../src/data/scrypto/custom_schema.rs | 1 + sbor-tests/tests/schema_comparison.rs | 122 +++++----- sbor/src/basic.rs | 1 + sbor/src/schema/custom_traits.rs | 15 +- sbor/src/schema/schema_comparison.rs | 218 +++++++++++------- 5 files changed, 207 insertions(+), 150 deletions(-) diff --git a/radix-common/src/data/scrypto/custom_schema.rs b/radix-common/src/data/scrypto/custom_schema.rs index b4b0fa83404..109d8c5570b 100644 --- a/radix-common/src/data/scrypto/custom_schema.rs +++ b/radix-common/src/data/scrypto/custom_schema.rs @@ -226,6 +226,7 @@ impl CustomSchema for ScryptoCustomSchema { type CustomAggregatorTypeKind = ScryptoCustomTypeKind; type CustomTypeKindLabel = ScryptoCustomTypeKindLabel; type CustomTypeValidation = ScryptoCustomTypeValidation; + type DefaultCustomExtension = ScryptoCustomExtension; fn linearize_type_kind( type_kind: Self::CustomLocalTypeKind, diff --git a/sbor-tests/tests/schema_comparison.rs b/sbor-tests/tests/schema_comparison.rs index a70a51b021c..0834bb397bf 100644 --- a/sbor-tests/tests/schema_comparison.rs +++ b/sbor-tests/tests/schema_comparison.rs @@ -2,7 +2,6 @@ use sbor::prelude::*; use sbor::schema::*; use sbor::BasicValue; use sbor::ComparisonSchema; -use sbor::NoCustomExtension; use sbor::NoCustomSchema; use sbor::NoCustomTypeKind; @@ -10,86 +9,75 @@ use sbor::NoCustomTypeKind; // HELPER CODE / TRAITS //===================== trait DerivableTypeSchema: Describe { - fn single_type_schema_version() -> String { - let single_type_schema_version: SingleTypeSchema = - SingleTypeSchema::::from::(); - ComparisonSchema::::encode_to_hex(&single_type_schema_version) + fn single_type_schema_version() -> SingleTypeSchema { + SingleTypeSchema::for_type::() } -} - -impl> DerivableTypeSchema for T {} -trait RegisterType { - fn register_schema_of(self, name: &'static str) -> Self; -} - -impl> RegisterType for NamedSchemaVersions { - fn register_schema_of(self, name: &'static str) -> Self { - self.register_version(name, T::single_type_schema_version()) + fn single_type_schema_version_hex() -> String { + Self::single_type_schema_version().encode_to_hex() } } -fn assert_extension() { - assert_type_backwards_compatibility::(|v| { - v.register_schema_of::("base") - .register_schema_of::("latest") - }) -} - -fn assert_extension_ignoring_name_changes() { - let settings = SchemaComparisonSettings::allow_structural_extension() - .metadata_settings(SchemaComparisonMetadataSettings::allow_all_changes()); - assert_comparison_succeeds::(&settings); -} +impl> DerivableTypeSchema for T {} fn assert_equality() { - assert_comparison_succeeds::(&SchemaComparisonSettings::require_equality()); + let settings = SchemaComparisonSettings::require_equality(); + assert_single_type_comparison::( + &settings, + &T1::single_type_schema_version(), + &T2::single_type_schema_version(), + ) } fn assert_equality_ignoring_name_changes() { let settings = SchemaComparisonSettings::require_equality() .metadata_settings(SchemaComparisonMetadataSettings::allow_all_changes()); - assert_comparison_succeeds::(&settings); + assert_single_type_comparison::( + &settings, + &T1::single_type_schema_version(), + &T2::single_type_schema_version(), + ) } -fn assert_comparison_succeeds( - settings: &SchemaComparisonSettings, -) { - assert_type_compatibility::(settings, |v| { - v.register_schema_of::("base") - .register_schema_of::("latest") - }) +fn assert_extension() { + let settings = SchemaComparisonSettings::allow_extension(); + assert_single_type_comparison::( + &settings, + &T1::single_type_schema_version(), + &T2::single_type_schema_version(), + ) } -fn assert_extension_multi( - base: NamedTypesSchema, - latest: NamedTypesSchema, -) { - let settings = SchemaComparisonSettings::allow_structural_extension(); - assert_multi_comparison_succeeds(&settings, base, latest); +fn assert_extension_ignoring_name_changes() { + let settings = SchemaComparisonSettings::allow_extension() + .metadata_settings(SchemaComparisonMetadataSettings::allow_all_changes()); + assert_single_type_comparison::( + &settings, + &T1::single_type_schema_version(), + &T2::single_type_schema_version(), + ) } -fn assert_equality_multi( +fn assert_type_collection_equality( base: NamedTypesSchema, - latest: NamedTypesSchema, + compared: NamedTypesSchema, ) { - let settings = SchemaComparisonSettings::require_equality(); - assert_multi_comparison_succeeds(&settings, base, latest); + assert_type_collection_comparison( + &SchemaComparisonSettings::require_equality(), + &base, + &compared, + ); } -fn assert_multi_comparison_succeeds( - settings: &SchemaComparisonSettings, +fn assert_type_collection_extension( base: NamedTypesSchema, - latest: NamedTypesSchema, + compared: NamedTypesSchema, ) { - assert_type_collection_backwards_compatibility::( - settings, - latest.clone(), - |v| { - v.register_version("base", base) - .register_version("latest", latest) - }, - ) + assert_type_collection_comparison( + &SchemaComparisonSettings::allow_extension(), + &base, + &compared, + ); } //============= @@ -209,21 +197,21 @@ enum MyMultiRecursiveTypeForm2 { #[test] #[should_panic] fn asserting_backwards_compatibility_requires_a_named_schema() { - assert_type_backwards_compatibility::(|v| v) + assert_type_backwards_compatibility::(|v| v) } #[test] fn asserting_backwards_compatibility_with_a_single_latest_schema_version_succeeds() { - assert_type_backwards_compatibility::(|v| { - v.register_schema_of::("latest") + assert_type_backwards_compatibility::(|v| { + v.register_version("latest", MyStruct::single_type_schema_version()) }) } #[test] #[should_panic] fn asserting_backwards_compatibility_with_incorrect_latest_schema_version_succeeds() { - assert_type_backwards_compatibility::(|v| { - v.register_schema_of::("latest") + assert_type_backwards_compatibility::(|v| { + v.register_version("latest", MyStructFieldRenamed::single_type_schema_version()) }) } @@ -384,7 +372,7 @@ fn base_schema_not_covered_by_root_types_fails() { // Forget about a root type - this leaves the schema not fully covered. base_schema.type_ids.swap_remove("enum"); - assert_equality_multi(base_schema, compared_schema); + assert_type_collection_equality(base_schema, compared_schema); } #[test] @@ -407,7 +395,7 @@ fn compared_schema_not_covered_by_root_types_fails() { // Note - the process starts by comparing "latest" with "current" // (i.e. compared_schema against itself) - so we get both a // TypeUnreachableFromRootInBaseSchema and a TypeUnreachableFromRootInComparedSchema - assert_equality_multi(base_schema, compared_schema); + assert_type_collection_equality(base_schema, compared_schema); } #[test] @@ -425,7 +413,7 @@ fn removed_root_type_fails_comparison() { aggregator.generate_named_types_schema() }; - assert_extension_multi(base_schema, compared_schema); + assert_type_collection_extension(base_schema, compared_schema); } #[test] @@ -442,7 +430,7 @@ fn under_extension_added_root_type_succeeds() { aggregator.generate_named_types_schema() }; - assert_extension_multi(base_schema, compared_schema); + assert_type_collection_extension(base_schema, compared_schema); } #[test] @@ -460,5 +448,5 @@ fn under_equality_added_root_type_fails() { aggregator.generate_named_types_schema() }; - assert_equality_multi(base_schema, compared_schema); + assert_type_collection_equality(base_schema, compared_schema); } diff --git a/sbor/src/basic.rs b/sbor/src/basic.rs index 1a4206c2e66..b36478b5f72 100644 --- a/sbor/src/basic.rs +++ b/sbor/src/basic.rs @@ -209,6 +209,7 @@ impl CustomSchema for NoCustomSchema { type CustomAggregatorTypeKind = NoCustomTypeKind; type CustomTypeKindLabel = NoCustomTypeKindLabel; type CustomTypeValidation = NoCustomTypeValidation; + type DefaultCustomExtension = NoCustomExtension; fn linearize_type_kind( _: Self::CustomAggregatorTypeKind, diff --git a/sbor/src/schema/custom_traits.rs b/sbor/src/schema/custom_traits.rs index 401f7e648a0..b02c840627a 100644 --- a/sbor/src/schema/custom_traits.rs +++ b/sbor/src/schema/custom_traits.rs @@ -1,3 +1,5 @@ +use vec_traits::VecSbor; + use crate::rust::prelude::*; use crate::traversal::*; use crate::*; @@ -18,18 +20,21 @@ pub trait CustomTypeValidation: Debug + Clone + PartialEq + Eq { } pub trait CustomSchema: Debug + Clone + Copy + PartialEq + Eq + 'static { - type CustomTypeValidation: CustomTypeValidation; + type CustomTypeValidation: CustomTypeValidation + VecSbor; type CustomLocalTypeKind: CustomTypeKind< - LocalTypeId, - CustomTypeValidation = Self::CustomTypeValidation, - CustomTypeKindLabel = Self::CustomTypeKindLabel, - >; + LocalTypeId, + CustomTypeValidation = Self::CustomTypeValidation, + CustomTypeKindLabel = Self::CustomTypeKindLabel, + > + VecSbor; type CustomAggregatorTypeKind: CustomTypeKind< RustTypeId, CustomTypeValidation = Self::CustomTypeValidation, CustomTypeKindLabel = Self::CustomTypeKindLabel, >; type CustomTypeKindLabel: CustomTypeKindLabel; + /// Should only be used for default encoding of a schema, where it's required. + /// Typically you should start from a CustomExtension and not use this. + type DefaultCustomExtension: CustomExtension; fn linearize_type_kind( type_kind: Self::CustomAggregatorTypeKind, diff --git a/sbor/src/schema/schema_comparison.rs b/sbor/src/schema/schema_comparison.rs index 57342ecbe72..d6503e8574c 100644 --- a/sbor/src/schema/schema_comparison.rs +++ b/sbor/src/schema/schema_comparison.rs @@ -42,7 +42,7 @@ impl SchemaComparisonSettings { /// * Types must be structurally identical on their intersection, except new enum variants can be added /// * Type metadata (e.g. names) must be identical on their intersection /// * Type validation must be equal or strictly weaker in the new schema - pub const fn allow_structural_extension() -> Self { + pub const fn allow_extension() -> Self { Self { completeness: SchemaComparisonCompletenessSettings::enforce_type_roots_cover_schema_allow_new_root_types(), structure: SchemaComparisonStructureSettings::allow_extension(), @@ -1617,23 +1617,23 @@ pub struct ComparisonTypeRoot { compared_type_id: LocalTypeId, } -pub struct NamedSchemaVersions> { +pub struct NamedSchemaVersions> { ordered_versions: IndexMap, - extension: PhantomData, + custom_schema: PhantomData, } -impl> NamedSchemaVersions { +impl> NamedSchemaVersions { pub fn new() -> Self { Self { ordered_versions: Default::default(), - extension: Default::default(), + custom_schema: Default::default(), } } pub fn register_version( mut self, name: impl AsRef, - version: impl IntoSchema, + version: impl IntoSchema, ) -> Self { self.ordered_versions .insert(name.as_ref().to_string(), version.into_schema()); @@ -1645,6 +1645,32 @@ impl> NamedSchemaVersions { } } +/// Designed for basic comparisons between two types, e.g. equality checking. +/// +/// ## Example usage +/// ```no_run +/// # use radix_rust::prelude::*; +/// # use sbor::NoCustomSchema; +/// # use sbor::SchemaComparison::*; +/// # type ScryptoCustomSchema = NoCustomSchema; +/// # struct MyType; +/// let base = SingleTypeSchema::from("5b...."); +/// let current = SingleTypeSchema::of_type::(); +/// assert_single_type_comparison::( +/// SchemaComparisonSettings::equality(), +/// &base, +/// ¤t, +/// ); +/// ``` +pub fn assert_single_type_comparison( + comparison_settings: &SchemaComparisonSettings, + base: &SingleTypeSchema, + compared: &SingleTypeSchema, +) { + base.compare_with(compared, comparison_settings) + .assert_valid("base", "compared"); +} + /// Designed for ensuring a type is only altered in ways which ensure /// backwards compatibility in SBOR serialization (i.e. that old payloads /// can be deserialized correctly by the latest type). @@ -1654,8 +1680,8 @@ impl> NamedSchemaVersions { /// * Checks that each schema is consistent with the previous schema - but /// can be an extension (e.g. enums can have new variants) /// -/// The indexmap should be a map from a version name to a hex-encoded -/// basic-sbor-encoded `SingleTypeSchema` (with a single type). +/// The version registry is be a map from a version name to some encoding +/// of a `SingleTypeSchema` - including as-is, or hex-encoded sbor-encoded. /// The version name is only used for a more useful message on error. /// /// ## Example usage @@ -1666,11 +1692,10 @@ impl> NamedSchemaVersions { /// # type ScryptoCustomSchema = NoCustomSchema; /// # struct MyType; /// assert_type_backwards_compatibility::( -/// SchemaComparisonSettings::allow_structural_extension(), -/// indexmap! { -/// "babylon_launch" => "5b...", -/// "bottlenose" => "5b...", -/// } +/// |v| { +/// v.register_version("babylon_launch", "5b...") +/// .register_version("bottlenose", "5b...") +/// }, /// ); /// ``` /// ## Setup @@ -1679,36 +1704,59 @@ impl> NamedSchemaVersions { /// /// ``` pub fn assert_type_backwards_compatibility< - E: CustomExtension, - T: Describe<::CustomAggregatorTypeKind>, + S: CustomSchema, + T: Describe, >( versions_builder: impl FnOnce( - NamedSchemaVersions>, - ) -> NamedSchemaVersions>, -) where - SingleTypeSchema: ComparisonSchema, -{ - assert_type_compatibility::( - &SchemaComparisonSettings::allow_structural_extension(), + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) { + assert_type_compatibility::( + &SchemaComparisonSettings::allow_extension(), versions_builder, ) } -pub fn assert_type_compatibility< - E: CustomExtension, - T: Describe<::CustomAggregatorTypeKind>, ->( +/// Designed for ensuring a type is only altered in ways which ensure +/// backwards compatibility in SBOR serialization (i.e. that old payloads +/// can be deserialized correctly by the latest type). +/// +/// This function: +/// * Checks that the type's current schema is equal to the latest version +/// * Checks that each schema is consistent with the previous schema - but +/// can be an extension (e.g. enums can have new variants) +/// +/// The version registry is be a map from a version name to some encoding +/// of a `SingleTypeSchema` - including as-is, or hex-encoded sbor-encoded. +/// The version name is only used for a more useful message on error. +/// +/// ## Example usage +/// ```no_run +/// # use radix_rust::prelude::*; +/// # use sbor::NoCustomSchema; +/// # use sbor::SchemaComparison::*; +/// # type ScryptoCustomSchema = NoCustomSchema; +/// # struct MyType; +/// assert_type_compatibility::( +/// SchemaComparisonSettings::allow_extension(), +/// |v| { +/// v.register_version("babylon_launch", "5b...") +/// .register_version("bottlenose", "5b...") +/// }, +/// ); +/// ``` +/// ## Setup +/// To generate the encoded schema, just run the method with an empty `indexmap!` +/// and the assertion will include the encoded schemas, for copying into the assertion. +/// +/// ``` +pub fn assert_type_compatibility>( comparison_settings: &SchemaComparisonSettings, versions_builder: impl FnOnce( - NamedSchemaVersions>, - ) -> NamedSchemaVersions>, -) where - SingleTypeSchema: ComparisonSchema, -{ - let current = { - let (type_id, schema) = generate_full_schema_from_single_type::(); - SingleTypeSchema { schema, type_id } - }; + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) { + let current = generate_single_type_schema::(); assert_schema_compatibility( comparison_settings, ¤t, @@ -1716,15 +1764,35 @@ pub fn assert_type_compatibility< ) } -pub fn assert_type_collection_backwards_compatibility( +pub fn assert_type_collection_comparison( comparison_settings: &SchemaComparisonSettings, - current: NamedTypesSchema, + base: &NamedTypesSchema, + compared: &NamedTypesSchema, +) { + base.compare_with(compared, comparison_settings) + .assert_valid("base", "compared"); +} + +pub fn assert_type_collection_backwards_compatibility( + current: NamedTypesSchema, versions_builder: impl FnOnce( - NamedSchemaVersions>, - ) -> NamedSchemaVersions>, -) where - NamedTypesSchema: ComparisonSchema, -{ + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) { + assert_type_collection_compatibility( + &SchemaComparisonSettings::allow_extension(), + current, + versions_builder, + ) +} + +pub fn assert_type_collection_compatibility( + comparison_settings: &SchemaComparisonSettings, + current: NamedTypesSchema, + versions_builder: impl FnOnce( + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) { assert_schema_compatibility( comparison_settings, ¤t, @@ -1732,10 +1800,10 @@ pub fn assert_type_collection_backwards_compatibility( ) } -fn assert_schema_compatibility>( +fn assert_schema_compatibility>( schema_comparison_settings: &SchemaComparisonSettings, current: &C, - named_versions: &NamedSchemaVersions, + named_versions: &NamedSchemaVersions, ) { let named_versions = named_versions.get_versions(); @@ -1759,7 +1827,7 @@ fn assert_schema_compatibility>( if let Some(error_message) = result.error_message(latest_version_name, "current") { let mut error = String::new(); - writeln!(&mut error, "The most recent named version ({latest_version_name}) DOES NOT MATCH the current version.").unwrap(); + writeln!(&mut error, "The most recent named version ({latest_version_name}) DOES NOT PASS CHECKS, likely because it is not equal to the current version.").unwrap(); writeln!(&mut error).unwrap(); write!(&mut error, "{error_message}").unwrap(); writeln!(&mut error).unwrap(); @@ -1805,21 +1873,21 @@ impl SingleTypeSchema { Self { schema, type_id } } - pub fn from + ?Sized>() -> Self { + pub fn from>(from: &T) -> Self { + from.into_schema() + } + + pub fn for_type + ?Sized>() -> Self { generate_single_type_schema::() } } -impl ComparisonSchema for SingleTypeSchema -where - ::CustomLocalTypeKind: VecSbor, - ::CustomTypeValidation: VecSbor, -{ +impl ComparisonSchema for SingleTypeSchema { fn compare_with<'s>( &'s self, compared: &'s Self, settings: &SchemaComparisonSettings, - ) -> SchemaComparisonResult<'s, E::CustomSchema> { + ) -> SchemaComparisonResult<'s, S> { SchemaComparisonKernel::new( &self.schema.as_unique_version(), &compared.schema.as_unique_version(), @@ -1833,10 +1901,7 @@ where } } -impl IntoSchema for SingleTypeSchema -where - Self: ComparisonSchema, -{ +impl IntoSchema for SingleTypeSchema { fn into_schema(&self) -> Self { self.clone() } @@ -1859,21 +1924,21 @@ impl NamedTypesSchema { Self { schema, type_ids } } - pub fn from(aggregator: TypeAggregator) -> Self { + pub fn from>(from: &T) -> Self { + from.into_schema() + } + + pub fn from_aggregator(aggregator: TypeAggregator) -> Self { aggregator.generate_named_types_schema::() } } -impl ComparisonSchema for NamedTypesSchema -where - ::CustomLocalTypeKind: VecSbor, - ::CustomTypeValidation: VecSbor, -{ +impl ComparisonSchema for NamedTypesSchema { fn compare_with<'s>( &'s self, compared: &'s Self, settings: &SchemaComparisonSettings, - ) -> SchemaComparisonResult<'s, E::CustomSchema> { + ) -> SchemaComparisonResult<'s, S> { SchemaComparisonKernel::new( &self.schema.as_unique_version(), &compared.schema.as_unique_version(), @@ -1883,19 +1948,16 @@ where } } -impl IntoSchema for NamedTypesSchema -where - Self: ComparisonSchema, -{ +impl IntoSchema for NamedTypesSchema { fn into_schema(&self) -> Self { self.clone() } } // Marker trait -pub trait ComparisonSchema: Clone + VecSbor { +pub trait ComparisonSchema: Clone + VecSbor { fn encode_to_bytes(&self) -> Vec { - vec_encode::(self, BASIC_SBOR_V1_MAX_DEPTH).unwrap() + vec_encode::(self, BASIC_SBOR_V1_MAX_DEPTH).unwrap() } fn encode_to_hex(&self) -> String { @@ -1903,7 +1965,7 @@ pub trait ComparisonSchema: Clone + VecSbor { } fn decode_from_bytes(bytes: &[u8]) -> Self { - vec_decode::(bytes, BASIC_SBOR_V1_MAX_DEPTH).unwrap() + vec_decode::(bytes, BASIC_SBOR_V1_MAX_DEPTH).unwrap() } fn decode_from_hex(hex: &str) -> Self { @@ -1914,40 +1976,40 @@ pub trait ComparisonSchema: Clone + VecSbor { &'s self, compared: &'s Self, settings: &SchemaComparisonSettings, - ) -> SchemaComparisonResult<'s, E::CustomSchema>; + ) -> SchemaComparisonResult<'s, S>; } -pub trait IntoSchema, E: CustomExtension> { +pub trait IntoSchema, S: CustomSchema> { fn into_schema(&self) -> C; } -impl<'a, C: ComparisonSchema, E: CustomExtension, T: IntoSchema + ?Sized> IntoSchema +impl<'a, C: ComparisonSchema, S: CustomSchema, T: IntoSchema + ?Sized> IntoSchema for &'a T { fn into_schema(&self) -> C { - >::into_schema(*self) + >::into_schema(*self) } } -impl, E: CustomExtension> IntoSchema for [u8] { +impl, S: CustomSchema> IntoSchema for [u8] { fn into_schema(&self) -> C { C::decode_from_bytes(self) } } -impl, E: CustomExtension> IntoSchema for Vec { +impl, S: CustomSchema> IntoSchema for Vec { fn into_schema(&self) -> C { C::decode_from_bytes(self) } } -impl, E: CustomExtension> IntoSchema for String { +impl, S: CustomSchema> IntoSchema for String { fn into_schema(&self) -> C { C::decode_from_hex(self) } } -impl, E: CustomExtension> IntoSchema for str { +impl, S: CustomSchema> IntoSchema for str { fn into_schema(&self) -> C { C::decode_from_hex(self) } From 8853487a368d26e05d4fc5b8abf9f6ed787d118f Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 25 Jun 2024 17:20:37 +0100 Subject: [PATCH 11/19] tweak: Minor naming and alias changes --- .../src/data/scrypto/custom_schema.rs | 1 + sbor-tests/tests/schema_comparison.rs | 55 +++++++++---------- sbor/src/basic.rs | 1 + sbor/src/schema/type_aggregator.rs | 2 +- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/radix-common/src/data/scrypto/custom_schema.rs b/radix-common/src/data/scrypto/custom_schema.rs index 109d8c5570b..16032de59ca 100644 --- a/radix-common/src/data/scrypto/custom_schema.rs +++ b/radix-common/src/data/scrypto/custom_schema.rs @@ -9,6 +9,7 @@ pub type ScryptoTypeData = TypeData; pub type ScryptoLocalTypeData = LocalTypeData; pub type ScryptoAggregatorTypeData = AggregatorTypeData; pub type ScryptoTypeValidation = TypeValidation; +pub type ScryptoTypeAggregator = TypeAggregator; /// A schema for the values that a codec can decode / views as valid #[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoSbor)] diff --git a/sbor-tests/tests/schema_comparison.rs b/sbor-tests/tests/schema_comparison.rs index 0834bb397bf..e1cc29b78f5 100644 --- a/sbor-tests/tests/schema_comparison.rs +++ b/sbor-tests/tests/schema_comparison.rs @@ -1,9 +1,6 @@ use sbor::prelude::*; use sbor::schema::*; -use sbor::BasicValue; -use sbor::ComparisonSchema; -use sbor::NoCustomSchema; -use sbor::NoCustomTypeKind; +use sbor::{BasicTypeAggregator, BasicValue, ComparisonSchema, NoCustomSchema, NoCustomTypeKind}; //===================== // HELPER CODE / TRAITS @@ -359,14 +356,14 @@ fn equality_removing_length_validation_fails() { #[should_panic(expected = "TypeUnreachableFromRootInBaseSchema")] fn base_schema_not_covered_by_root_types_fails() { let mut base_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); - aggregator.add_named_root_type_and_descendents::("enum"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); + aggregator.add_root_type::("enum"); aggregator.generate_named_types_schema() }; let compared_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); aggregator.generate_named_types_schema() }; // Forget about a root type - this leaves the schema not fully covered. @@ -379,14 +376,14 @@ fn base_schema_not_covered_by_root_types_fails() { #[should_panic(expected = "TypeUnreachableFromRootInComparedSchema")] fn compared_schema_not_covered_by_root_types_fails() { let base_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); aggregator.generate_named_types_schema() }; let mut compared_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); - aggregator.add_named_root_type_and_descendents::("enum"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); + aggregator.add_root_type::("enum"); aggregator.generate_named_types_schema() }; // Forget about a root type - this leaves the schema not fully covered. @@ -402,14 +399,14 @@ fn compared_schema_not_covered_by_root_types_fails() { #[should_panic(expected = "NamedRootTypeMissingInComparedSchema")] fn removed_root_type_fails_comparison() { let base_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); - aggregator.add_named_root_type_and_descendents::("enum"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); + aggregator.add_root_type::("enum"); aggregator.generate_named_types_schema() }; let compared_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); aggregator.generate_named_types_schema() }; @@ -419,14 +416,14 @@ fn removed_root_type_fails_comparison() { #[test] fn under_extension_added_root_type_succeeds() { let base_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); aggregator.generate_named_types_schema() }; let compared_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); - aggregator.add_named_root_type_and_descendents::("enum"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); + aggregator.add_root_type::("enum"); aggregator.generate_named_types_schema() }; @@ -437,14 +434,14 @@ fn under_extension_added_root_type_succeeds() { #[should_panic(expected = "DisallowedNewRootTypeInComparedSchema")] fn under_equality_added_root_type_fails() { let base_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); aggregator.generate_named_types_schema() }; let compared_schema = { - let mut aggregator = TypeAggregator::::new(); - aggregator.add_named_root_type_and_descendents::("struct"); - aggregator.add_named_root_type_and_descendents::("enum"); + let mut aggregator = BasicTypeAggregator::new(); + aggregator.add_root_type::("struct"); + aggregator.add_root_type::("enum"); aggregator.generate_named_types_schema() }; diff --git a/sbor/src/basic.rs b/sbor/src/basic.rs index b36478b5f72..cd64e9253b1 100644 --- a/sbor/src/basic.rs +++ b/sbor/src/basic.rs @@ -299,6 +299,7 @@ pub type BasicVersionedSchema = VersionedSchema; pub type BasicTypeData = TypeData; pub type BasicLocalTypeData = LocalTypeData; pub type BasicAggregatorTypeData = LocalTypeData; +pub type BasicTypeAggregator = TypeAggregator; impl<'a> CustomDisplayContext<'a> for () { type CustomExtension = NoCustomExtension; diff --git a/sbor/src/schema/type_aggregator.rs b/sbor/src/schema/type_aggregator.rs index 544b7904c28..2ac551b774e 100644 --- a/sbor/src/schema/type_aggregator.rs +++ b/sbor/src/schema/type_aggregator.rs @@ -163,7 +163,7 @@ impl> TypeAggregator { /// /// This is only intended for use when adding root types to schemas, /// /and should not be called from inside Describe macros. - pub fn add_named_root_type_and_descendents + ?Sized>( + pub fn add_root_type + ?Sized>( &mut self, name: impl Into, ) -> LocalTypeId { From cdebe633bf38148e79482239a705408144664993 Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 2 Jul 2024 16:32:58 +0100 Subject: [PATCH 12/19] feature: Add BasicSborAssertion and ScryptoSborAssertion macros These macros output tests which check for a fixed type schema, or a backwards compatible type schema. --- radix-common/src/data/manifest/definitions.rs | 48 ++- radix-common/src/data/scrypto/definitions.rs | 45 ++- radix-common/src/lib.rs | 4 +- radix-sbor-derive/src/lib.rs | 11 + sbor-derive-common/src/lib.rs | 1 + sbor-derive-common/src/sbor_assert.rs | 308 ++++++++++++++++++ sbor-derive-common/src/utils.rs | 212 +++++++----- sbor-derive/src/lib.rs | 12 + sbor-tests/tests/sbor_assert.rs | 47 +++ sbor-tests/tests/schema_comparison.rs | 52 +-- sbor/src/basic.rs | 31 +- sbor/src/lib.rs | 10 +- sbor/src/schema/custom_traits.rs | 2 +- sbor/src/schema/schema_comparison.rs | 113 ++++--- sbor/src/schema/type_aggregator.rs | 6 +- sbor/src/traversal/untyped/traverser.rs | 1 + sbor/src/vec_traits.rs | 36 +- 17 files changed, 736 insertions(+), 203 deletions(-) create mode 100644 sbor-derive-common/src/sbor_assert.rs create mode 100644 sbor-tests/tests/sbor_assert.rs diff --git a/radix-common/src/data/manifest/definitions.rs b/radix-common/src/data/manifest/definitions.rs index a7bbb80ff6a..f2ccaa1e150 100644 --- a/radix-common/src/data/manifest/definitions.rs +++ b/radix-common/src/data/manifest/definitions.rs @@ -1,3 +1,5 @@ +use vec_traits::vec_decode_with_nice_error; + use crate::internal_prelude::*; pub use crate::constants::MANIFEST_SBOR_V1_MAX_DEPTH; @@ -103,44 +105,40 @@ pub fn manifest_decode(buf: &[u8]) -> Result manifest_decode_with_depth_limit(buf, MANIFEST_SBOR_V1_MAX_DEPTH) } +pub fn manifest_decode_with_depth_limit( + buf: &[u8], + depth_limit: usize, +) -> Result { + ManifestDecoder::new(buf, depth_limit).decode_payload(MANIFEST_SBOR_V1_PAYLOAD_PREFIX) +} + /// Decodes a data structure from a byte array. /// /// If an error occurs, the type's schema is exported and used to give a better error message. /// /// NOTE: /// * The error path runs very slowly. This should only be used where errors are NOT expected. +/// * This should not be used where the size of compiled code is an issue, as it will pull +/// in the schema aggregation code which is large. pub fn manifest_decode_with_nice_error( buf: &[u8], ) -> Result { - match manifest_decode(buf) { - Ok(value) => Ok(value), - Err(err) => { - let (local_type_id, schema) = - generate_full_schema_from_single_type::(); - let schema = schema.as_unique_version(); - match validate_payload_against_schema::( - buf, - schema, - local_type_id, - &(), - SCRYPTO_SBOR_V1_MAX_DEPTH, - ) { - Ok(()) => { - // This case is unexpected. We got a decode error, but it's valid against the schema. - // In this case, let's just debug-print the DecodeError. - Err(format!("{err:?}")) - } - Err(err) => Err(err.error_message(schema)), - } - } - } + vec_decode_with_nice_error::(buf, MANIFEST_SBOR_V1_MAX_DEPTH) } -pub fn manifest_decode_with_depth_limit( +/// Decodes a data structure from a byte array. +/// +/// If an error occurs, the type's schema is exported and used to give a better error message. +/// +/// NOTE: +/// * The error path runs very slowly. This should only be used where errors are NOT expected. +/// * This should not be used where the size of compiled code is an issue, as it will pull +/// in the schema aggregation code which is large. +pub fn manifest_decode_with_depth_limit_and_nice_error( buf: &[u8], depth_limit: usize, -) -> Result { - ManifestDecoder::new(buf, depth_limit).decode_payload(MANIFEST_SBOR_V1_PAYLOAD_PREFIX) +) -> Result { + vec_decode_with_nice_error::(buf, depth_limit) } pub fn to_manifest_value( diff --git a/radix-common/src/data/scrypto/definitions.rs b/radix-common/src/data/scrypto/definitions.rs index 35a494e73cd..18780ee707a 100644 --- a/radix-common/src/data/scrypto/definitions.rs +++ b/radix-common/src/data/scrypto/definitions.rs @@ -1,3 +1,5 @@ +use vec_traits::vec_decode_with_nice_error; + use crate::internal_prelude::*; pub use crate::constants::SCRYPTO_SBOR_V1_MAX_DEPTH; @@ -63,6 +65,13 @@ pub fn scrypto_decode(buf: &[u8]) -> Result { scrypto_decode_with_depth_limit(buf, SCRYPTO_SBOR_V1_MAX_DEPTH) } +pub fn scrypto_decode_with_depth_limit( + buf: &[u8], + depth_limit: usize, +) -> Result { + ScryptoDecoder::new(buf, depth_limit).decode_payload(SCRYPTO_SBOR_V1_PAYLOAD_PREFIX) +} + /// Decodes a data structure from a byte array. /// /// If an error occurs, the type's schema is exported and used to give a better error message. @@ -73,33 +82,19 @@ pub fn scrypto_decode(buf: &[u8]) -> Result { pub fn scrypto_decode_with_nice_error( buf: &[u8], ) -> Result { - match scrypto_decode(buf) { - Ok(value) => Ok(value), - Err(err) => { - let (local_type_id, schema) = - generate_full_schema_from_single_type::(); - let schema = schema.as_unique_version(); - match validate_payload_against_schema::( - buf, - schema, - local_type_id, - &(), - SCRYPTO_SBOR_V1_MAX_DEPTH, - ) { - Ok(()) => { - // This case is unexpected. We got a decode error, but it's valid against the schema. - // In this case, let's just debug-print the DecodeError. - Err(format!("{err:?}")) - } - Err(err) => Err(err.error_message(schema)), - } - } - } + vec_decode_with_nice_error::(buf, SCRYPTO_SBOR_V1_MAX_DEPTH) } -pub fn scrypto_decode_with_depth_limit( +/// Decodes a data structure from a byte array. +/// +/// If an error occurs, the type's schema is exported and used to give a better error message. +/// +/// NOTE: +/// * The error path runs very slowly. This should only be used where errors are NOT expected. +/// * This should not be used in Scrypto, as it will pull in the schema aggregation code which is large. +pub fn scrypto_decode_with_depth_limit_and_nice_error( buf: &[u8], depth_limit: usize, -) -> Result { - ScryptoDecoder::new(buf, depth_limit).decode_payload(SCRYPTO_SBOR_V1_PAYLOAD_PREFIX) +) -> Result { + vec_decode_with_nice_error::(buf, depth_limit) } diff --git a/radix-common/src/lib.rs b/radix-common/src/lib.rs index 61f1c2bf296..7ebd8b0c479 100644 --- a/radix-common/src/lib.rs +++ b/radix-common/src/lib.rs @@ -35,7 +35,7 @@ pub use sbor::{Categorize, Decode, Encode, Sbor}; extern crate radix_sbor_derive; pub use radix_sbor_derive::{ ManifestCategorize, ManifestDecode, ManifestEncode, ManifestSbor, ScryptoCategorize, - ScryptoDecode, ScryptoEncode, ScryptoEvent, ScryptoSbor, + ScryptoDecode, ScryptoEncode, ScryptoEvent, ScryptoSbor, ScryptoSborAssertion, }; // extern crate self as X; in lib.rs allows ::X and X to resolve to this crate inside this crate. @@ -60,7 +60,7 @@ pub mod prelude { // Exports from upstream libraries pub use radix_sbor_derive::{ ManifestCategorize, ManifestDecode, ManifestEncode, ManifestSbor, ScryptoCategorize, - ScryptoDecode, ScryptoEncode, ScryptoEvent, ScryptoSbor, + ScryptoDecode, ScryptoEncode, ScryptoEvent, ScryptoSbor, ScryptoSborAssertion, }; pub use sbor::prelude::*; pub use sbor::*; diff --git a/radix-sbor-derive/src/lib.rs b/radix-sbor-derive/src/lib.rs index da02ac4fa1e..dc5742cd456 100644 --- a/radix-sbor-derive/src/lib.rs +++ b/radix-sbor-derive/src/lib.rs @@ -169,6 +169,17 @@ pub fn scrypto_sbor(input: TokenStream) -> TokenStream { .into() } +/// A macro for outputting tests and marker traits to assert that a type has maintained its shape over time. +#[proc_macro_derive(ScryptoSborAssertion, attributes(sbor_assert))] +pub fn scrypto_sbor_assertion(input: TokenStream) -> TokenStream { + sbor_derive_common::sbor_assert::handle_sbor_assert_derive( + proc_macro2::TokenStream::from(input), + "radix_common::data::scrypto::ScryptoCustomSchema", + ) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + /// Derive code for implementing the required logic to mark a type as being an event. /// /// # Example diff --git a/sbor-derive-common/src/lib.rs b/sbor-derive-common/src/lib.rs index de98fef6c83..faa1db0aac7 100644 --- a/sbor-derive-common/src/lib.rs +++ b/sbor-derive-common/src/lib.rs @@ -3,4 +3,5 @@ pub mod decode; pub mod describe; pub mod encode; pub mod sbor; +pub mod sbor_assert; pub mod utils; diff --git a/sbor-derive-common/src/sbor_assert.rs b/sbor-derive-common/src/sbor_assert.rs new file mode 100644 index 00000000000..c37e7807168 --- /dev/null +++ b/sbor-derive-common/src/sbor_assert.rs @@ -0,0 +1,308 @@ +use proc_macro2::{Span, TokenStream}; +use quote::format_ident; +use quote::quote; +use quote::ToTokens; +use spanned::Spanned as _; +use syn::*; + +use crate::utils::*; + +macro_rules! trace { + ($($arg:expr),*) => {{ + #[cfg(feature = "trace")] + println!($($arg),*); + }}; +} + +pub fn handle_sbor_assert_derive( + input: TokenStream, + context_custom_schema: &str, +) -> Result { + trace!("handle_sbor_assert_derive() starts"); + + let parsed: DeriveInput = parse2(input)?; + + if parsed.generics.params.len() > 0 || parsed.generics.where_clause.is_some() { + // In future we could support them by allowing concrete type parameters to be provided in attributes, + // to be used for the purpose of generating the test. + return Err(Error::new( + Span::call_site(), + "Generic types are not presently supported with the SborAssert macros", + )); + }; + + let assertion_variant = extract_assertion_variant(&parsed.attrs)?; + + let output = match assertion_variant { + AssertionVariant::Generate => handle_generate(context_custom_schema, parsed), + AssertionVariant::Fixed(params) => handle_fixed(context_custom_schema, parsed, params), + AssertionVariant::BackwardsCompatible(params) => { + handle_backwards_compatible(context_custom_schema, parsed, params) + } + }?; + + trace!("handle_sbor_assert_derive() finishes"); + Ok(output) +} + +const GENERAL_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(generate)]` OR `#[sbor_assert(fixed(...))]` or `#[sbor_assert(backwards_compatible(...))]`"; +const FIXED_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(fixed(\"\"))]` OR `#[sbor_assert(fixed(CONSTANT))]` where CONSTANT is a string or implements `IntoSchema`"; +const BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(backwards_compatible(version1 = \"...\", version2 = \"...\"))]` where the placeholders are hex-encoded schemas, OR `#[sbor_assert(backwards_compatible(CONSTANT))]` where CONSTANT implements `IntoIterator, K: AsRef, V: IntoSchema`. For example: `const TYPE_X_NAMED_VERSIONS: [(&'static str, &'static str); X] = [(\"version1\", \"...\")]`"; + +fn extract_assertion_variant(attributes: &[Attribute]) -> Result { + // When we come to extract fixed named types, + let inner_attributes = extract_wrapped_root_attributes(attributes, "sbor_assert")?; + let keyed_inner_attributes = + extract_wrapped_inner_attributes(&inner_attributes, GENERAL_PARSE_ERROR_MESSAGE)?; + if keyed_inner_attributes.len() != 1 { + return Err(Error::new(Span::call_site(), GENERAL_PARSE_ERROR_MESSAGE)); + } + let (attribute_name, (attribute_name_ident, attribute_value)) = + keyed_inner_attributes.into_iter().next().unwrap(); + + match attribute_name.as_str() { + "generate" => { + if attribute_value.is_some() { + Err(Error::new( + attribute_name_ident.span(), + GENERAL_PARSE_ERROR_MESSAGE, + )) + } else { + Ok(AssertionVariant::Generate) + } + } + "backwards_compatible" => { + let schema_parameters = match attribute_value { + Some(meta_list) => { + if let [NestedMeta::Meta(Meta::Path(path))] = meta_list.as_slice() { + // Constant + BackwardsCompatibleSchemaParameters::FromConstant { + constant: path.clone(), + } + } else { + // Key-value based + let named_schemas = meta_list + .iter() + .map(|meta| -> Result<_> { + match meta { + NestedMeta::Meta(Meta::NameValue(MetaNameValue { + path, + lit, + .. + })) => { + let Some(ident) = path.get_ident() else { + return Err(Error::new( + path.span(), + BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE, + )); + }; + Ok(NamedSchema { + name: ident.clone(), + schema: lit.into_token_stream(), + }) + } + _ => { + return Err(Error::new( + meta.span(), + BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE, + )) + } + } + }) + .collect::>()?; + BackwardsCompatibleSchemaParameters::NamedSchemas { named_schemas } + } + } + _ => { + return Err(Error::new( + attribute_name_ident.span(), + BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE, + )); + } + }; + Ok(AssertionVariant::BackwardsCompatible(schema_parameters)) + } + "fixed" => { + let fixed_schema_parameters = match attribute_value { + Some(inner) if inner.len() == 1 => match inner[0] { + NestedMeta::Meta(Meta::Path(path)) => FixedSchemaParameters::FromConstant { + constant_path: path.clone(), + }, + NestedMeta::Lit(Lit::Str(lit_str)) => FixedSchemaParameters::FixedSchema { + fixed_schema: lit_str.into_token_stream(), + }, + _ => { + return Err(Error::new( + attribute_name_ident.span(), + FIXED_PARSE_ERROR_MESSAGE, + )); + } + }, + _ => { + return Err(Error::new( + attribute_name_ident.span(), + FIXED_PARSE_ERROR_MESSAGE, + )); + } + }; + Ok(AssertionVariant::Fixed(fixed_schema_parameters)) + } + _ => Err(Error::new( + attribute_name_ident.span(), + GENERAL_PARSE_ERROR_MESSAGE, + )), + } +} + +enum AssertionVariant { + Generate, + Fixed(FixedSchemaParameters), + BackwardsCompatible(BackwardsCompatibleSchemaParameters), +} + +enum FixedSchemaParameters { + FromConstant { constant_path: Path }, + FixedSchema { fixed_schema: TokenStream }, +} + +enum BackwardsCompatibleSchemaParameters { + FromConstant { constant: Path }, + NamedSchemas { named_schemas: Vec }, +} + +struct NamedSchema { + name: Ident, + schema: TokenStream, +} + +fn handle_generate(context_custom_schema: &str, parsed: DeriveInput) -> Result { + let DeriveInput { ident, .. } = &parsed; + + let custom_schema: Path = parse_str(context_custom_schema)?; + let test_ident = format_ident!("test_{}_type_is_generated_in_panic_message", ident); + + // NOTE: Generics are explicitly _NOT_ supported for now, because we need a concrete type + // to generate the schema from. + let output = quote! { + #[cfg(test)] + #[test] + #[allow(non_snake_case)] + pub fn #test_ident() { + let type_schema = sbor::schema::SingleTypeSchema::<#custom_schema>::for_type::<#ident>(); + panic!( + "Copy the below encoded type schema and replace `generate` with `fixed` or `backwards_compatible` in the attribute to receive further instructions.\n The current type schema is:\n{}", type_schema.encode_to_hex() + ); + } + }; + + Ok(output) +} + +fn handle_fixed( + context_custom_schema: &str, + parsed: DeriveInput, + params: FixedSchemaParameters, +) -> Result { + let DeriveInput { ident, .. } = &parsed; + + let fixed_schema = match params { + FixedSchemaParameters::FromConstant { constant_path } => { + quote! { sbor::schema::SingleTypeSchema::from(#constant_path) } + } + FixedSchemaParameters::FixedSchema { fixed_schema } => { + quote! { sbor::schema::SingleTypeSchema::from(#fixed_schema) } + } + }; + + let custom_schema: Path = parse_str(context_custom_schema)?; + let test_ident = format_ident!("test_{}_type_is_fixed", ident); + + // NOTE: Generics are explicitly _NOT_ supported for now, because we need a concrete type + // to generate the schema from. + let output = quote! { + impl sbor::schema::CheckedFixedSchema<#custom_schema> for #ident {} + impl sbor::schema::CheckedBackwardsCompatibleSchema<#custom_schema> for #ident {} + + #[cfg(test)] + #[test] + #[allow(non_snake_case)] + pub fn #test_ident() { + let current = sbor::schema::SingleTypeSchema::for_type::<#ident>(); + let fixed = #fixed_schema; + let result = sbor::schema::compare_single_type_schemas::< + #custom_schema, + >( + &sbor::schema::SchemaComparisonSettings::require_equality(), + &fixed, + ¤t, + ); + if let Some(error_message) = result.error_message("fixed", "current") { + use sbor::rust::fmt::Write; + use sbor::rust::prelude::String; + let mut error = String::new(); + writeln!(&mut error, "{error_message}").unwrap(); + writeln!(&mut error, "If you are sure the fixed version is incorrect, it can be updated to the current version which is:").unwrap(); + writeln!(&mut error, "{}", current.encode_to_hex()).unwrap(); + panic!("{error}"); + } + } + }; + + Ok(output) +} + +fn handle_backwards_compatible( + context_custom_schema: &str, + parsed: DeriveInput, + params: BackwardsCompatibleSchemaParameters, +) -> Result { + let DeriveInput { ident, .. } = &parsed; + + let custom_schema: Path = parse_str(context_custom_schema)?; + let test_ident = format_ident!("test_{}_type_is_backwards_compatible", ident); + + let test_content = match params { + BackwardsCompatibleSchemaParameters::FromConstant { constant } => { + quote! { + sbor::schema::assert_type_backwards_compatibility::< + #custom_schema, + #ident, + >(|v| sbor::schema::NamedSchemaVersions::from(#constant)); + } + } + BackwardsCompatibleSchemaParameters::NamedSchemas { named_schemas } => { + // NOTE: It's okay for these to be empty - the test output will output a correct default schema. + let (version_names, schemas): (Vec<_>, Vec<_>) = named_schemas + .into_iter() + .map(|named_schema| (named_schema.name.to_string(), named_schema.schema)) + .unzip(); + + quote! { + sbor::schema::assert_type_backwards_compatibility::< + #custom_schema, + #ident, + >(|v| { + v + #( + .register_version(#version_names, #schemas) + )* + }); + } + } + }; + + // NOTE: Generics are explicitly _NOT_ supported for now, because we need a concrete type + // to generate the schema from. + let output = quote! { + impl sbor::schema::CheckedBackwardsCompatibleSchema<#custom_schema> for #ident {} + + #[cfg(test)] + #[test] + #[allow(non_snake_case)] + pub fn #test_ident() { + #test_content + } + }; + + Ok(output) +} diff --git a/sbor-derive-common/src/utils.rs b/sbor-derive-common/src/utils.rs index b44515afdc3..8849ac4f4d8 100644 --- a/sbor-derive-common/src/utils.rs +++ b/sbor-derive-common/src/utils.rs @@ -106,95 +106,157 @@ impl AttributeMap for BTreeMap { pub fn extract_sbor_typed_attributes( attributes: &[Attribute], ) -> Result> { - extract_typed_attributes(attributes, "sbor") + extract_wrapped_typed_attributes(attributes, "sbor") } -/// Permits attribute of the form #[{name}(opt1, opt2 = X, opt3(Y))] for some literal X or some path or literal Y. -pub fn extract_typed_attributes( +pub fn extract_wrapped_root_attributes( attributes: &[Attribute], - name: &str, -) -> Result> { + wrapping_prefix: &str, +) -> Result> { + let inner_attributes = attributes.iter() + .filter(|attribute| attribute.path.is_ident(wrapping_prefix)) + .map(|attribute| -> Result<_> { + let Ok(meta) = attribute.parse_meta() else { + return Err(Error::new( + attribute.span(), + format!("Attribute content is not parsable as meta content"), + )); + }; + + let Meta::List(MetaList { + nested, .. + }) = meta + else { + return Err(Error::new( + attribute.span(), + format!("Expected list-based attribute as #[{wrapping_prefix}(..)]"), + )); + }; + + Ok(nested) + }) + .flatten_ok() + .map_ok(|nested_meta| -> Result<_> { + let NestedMeta::Meta(meta) = nested_meta + else { + return Err(Error::new( + nested_meta.span(), + format!("Expected list-based attribute as #[{wrapping_prefix}(...inner_attributes...)]"), + )); + }; + Ok(meta) + }) + .collect::>>()??; + + Ok(inner_attributes) +} + +pub fn extract_wrapped_inner_attributes<'m>( + inner_attributes: &'m [Meta], + error_message: &str, +) -> Result>)>> { let mut fields = BTreeMap::new(); - for attribute in attributes { - if !attribute.path.is_ident(name) { - continue; + for meta in inner_attributes { + let (name, inner) = extract_wrapping_inner_attribute(meta, error_message)?; + fields.insert(name.to_string(), (name.clone(), inner)); + } + Ok(fields) +} + +/// Only matches xyz OR xyz() OR xyz(INNER1) OR xyz(INNER1, INNER2, ..) +pub fn extract_wrapping_inner_attribute<'m>( + meta: &'m Meta, + error_message: &str, +) -> Result<(&'m Ident, Option>)> { + match meta { + Meta::Path(path) => { + if let Some(ident) = path.get_ident() { + Ok((ident, None)) + } else { + Err(Error::new(path.span(), error_message)) + } } - let Ok(meta) = attribute.parse_meta() else { - return Err(Error::new( - attribute.span(), - format!("Attribute content is not valid"), - )); - }; - let Meta::List(MetaList { - nested: options, .. - }) = meta - else { - return Err(Error::new( - attribute.span(), - format!("Expected list-based attribute as #[{name}(..)]"), - )); - }; - let error_message = format!("Expected attribute of the form #[{name}(opt1, opt2 = X, opt3(Y))] for some literal X or some path or literal Y."); - for option in options.into_iter() { - match option { - NestedMeta::Meta(m) => match m { - Meta::Path(path) => { - if let Some(ident) = path.get_ident() { - fields.insert(ident.to_string(), AttributeValue::None(path.span())); - } else { - return Err(Error::new(path.span(), error_message)); - } - } - Meta::NameValue(name_value) => { - if let Some(ident) = name_value.path.get_ident() { - fields.insert(ident.to_string(), AttributeValue::Lit(name_value.lit)); - } else { - return Err(Error::new(name_value.path.span(), error_message)); - } - } - Meta::List(MetaList { nested, path, .. }) => { - if let Some(ident) = path.get_ident() { - if nested.len() == 1 { - match nested.into_iter().next().unwrap() { - NestedMeta::Meta(inner_meta) => match inner_meta { - Meta::Path(path) => { - fields.insert( - ident.to_string(), - AttributeValue::Path(path.clone()), - ); - } - _ => { - return Err(Error::new( - inner_meta.span(), - error_message, - )); - } - }, - NestedMeta::Lit(lit) => { - fields.insert( - ident.to_string(), - AttributeValue::Lit(lit.clone()), - ); - } - } - } else { - return Err(Error::new(nested.span(), error_message)); + Meta::List(MetaList { nested, path, .. }) => { + if let Some(ident) = path.get_ident() { + Ok((ident, Some(nested.iter().collect()))) + } else { + Err(Error::new(path.span(), error_message)) + } + } + _ => Err(Error::new(meta.span(), error_message)), + } +} + +pub fn extract_typed_inner_attribute( + meta: &Meta, + error_message: &str, +) -> Result<(String, AttributeValue)> { + match meta { + Meta::Path(path) => { + if let Some(ident) = path.get_ident() { + Ok((ident.to_string(), AttributeValue::None(path.span()))) + } else { + Err(Error::new(path.span(), error_message)) + } + } + Meta::NameValue(name_value) => { + if let Some(ident) = name_value.path.get_ident() { + Ok(( + ident.to_string(), + AttributeValue::Lit(name_value.lit.clone()), + )) + } else { + Err(Error::new(name_value.path.span(), error_message)) + } + } + Meta::List(MetaList { nested, path, .. }) => { + if let Some(ident) = path.get_ident() { + if nested.len() == 1 { + match nested.into_iter().next().unwrap() { + NestedMeta::Meta(inner_meta) => match inner_meta { + Meta::Path(path) => { + Ok((ident.to_string(), AttributeValue::Path(path.clone()))) } - } else { - return Err(Error::new(path.span(), error_message)); + _ => Err(Error::new(inner_meta.span(), error_message)), + }, + NestedMeta::Lit(lit) => { + Ok((ident.to_string(), AttributeValue::Lit(lit.clone()))) } } - }, - _ => { - return Err(Error::new(option.span(), error_message)); + } else { + Err(Error::new(nested.span(), error_message)) } + } else { + Err(Error::new(path.span(), error_message)) } } } +} +pub fn extract_inner_typed_attributes<'m>( + inner_attributes: impl Iterator, + error_message: &str, +) -> Result> { + let mut fields = BTreeMap::new(); + for meta in inner_attributes { + let (name, value) = extract_typed_inner_attribute(meta, error_message)?; + fields.insert(name, value); + } Ok(fields) } +/// Permits attribute of the form #[{name}(opt1, opt2 = X, opt3(Y))] for some literal X or some path or literal Y. +pub fn extract_wrapped_typed_attributes( + attributes: &[Attribute], + name: &str, +) -> Result> { + let inner_attributes = extract_wrapped_root_attributes(attributes, name)?; + extract_inner_typed_attributes( + inner_attributes.iter(), + &format!("Expected attribute of the form #[{name}(opt1, opt2 = X, opt3(Y))] for some literal X or some path or literal Y."), + ) +} + pub(crate) enum SourceVariantData { Reachable(VariantData), Unreachable(UnreachableVariantData), @@ -241,7 +303,7 @@ pub(crate) fn process_enum_variants( .iter() .enumerate() .map(|(i, variant)| -> Result<_> { - let mut variant_attributes = extract_typed_attributes(&variant.attrs, "sbor")?; + let mut variant_attributes = extract_wrapped_typed_attributes(&variant.attrs, "sbor")?; let fields_data = process_fields(&variant.fields)?; let source_variant = variant.clone(); if let Some(_) = variant_attributes.remove("unreachable") { @@ -1056,7 +1118,7 @@ mod tests { let attr2: Attribute = parse_quote! { #[sbor(skip3)] }; - let extracted = extract_typed_attributes(&[attr, attr2], "sbor").unwrap(); + let extracted = extract_wrapped_typed_attributes(&[attr, attr2], "sbor").unwrap(); assert_eq!(extracted.get_bool_value("skip").unwrap().value(), true); assert_eq!(extracted.get_bool_value("skip2").unwrap().value(), false); assert_eq!(extracted.get_bool_value("skip3").unwrap().value(), true); diff --git a/sbor-derive/src/lib.rs b/sbor-derive/src/lib.rs index 8f4b8ad9d2e..660f537d685 100644 --- a/sbor-derive/src/lib.rs +++ b/sbor-derive/src/lib.rs @@ -155,6 +155,7 @@ pub fn eager_replace(token_stream: TokenStream) -> TokenStream { const BASIC_CUSTOM_VALUE_KIND: &str = "sbor::NoCustomValueKind"; const BASIC_CUSTOM_TYPE_KIND: &str = "sbor::NoCustomTypeKind"; +const BASIC_CUSTOM_SCHEMA: &str = "sbor::NoCustomSchema"; /// Derive code that returns the value kind - specifically for Basic SBOR. #[proc_macro_derive(BasicCategorize, attributes(sbor))] @@ -212,3 +213,14 @@ pub fn basic_sbor(input: TokenStream) -> TokenStream { .unwrap_or_else(|err| err.to_compile_error()) .into() } + +/// A macro for outputting tests and marker traits to assert that a type has maintained its shape over time. +#[proc_macro_derive(BasicSborAssertion, attributes(sbor_assert))] +pub fn basic_sbor_assertion(input: TokenStream) -> TokenStream { + sbor_derive_common::sbor_assert::handle_sbor_assert_derive( + proc_macro2::TokenStream::from(input), + BASIC_CUSTOM_SCHEMA, + ) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/sbor-tests/tests/sbor_assert.rs b/sbor-tests/tests/sbor_assert.rs new file mode 100644 index 00000000000..97bee4e6994 --- /dev/null +++ b/sbor-tests/tests/sbor_assert.rs @@ -0,0 +1,47 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_camel_case_types)] + +use sbor::*; + +#[derive(BasicSborAssertion, BasicSbor)] +#[sbor_assert(fixed("5b210222000121032022010f012307200100220100010709202101022201010c0a4d7954657374456e756d2201012201012307210100022201010c0548656c6c6f220101220001200c0105737461746520220100002201010a0000000000000000"))] +#[sbor(type_name = "MyTestEnum")] +pub enum MyTestEnum_FixedSchema_Test1 { + Hello { state: u32 }, +} + +const TEST_ENUM_SCHEMA: &'static str = "5b210222000121032022010f012307200100220100010709202101022201010c0a4d7954657374456e756d2201012201012307210100022201010c0548656c6c6f220101220001200c0105737461746520220100002201010a0000000000000000"; + +#[derive(BasicSborAssertion, BasicSbor)] +#[sbor_assert(fixed(TEST_ENUM_SCHEMA))] +#[sbor(type_name = "MyTestEnum")] +pub enum MyTestEnum_FixedSchema_Test2 { + Hello { state: u32 }, +} + +const TEST_ENUM_V2_SCHEMA: &'static str = "5b210222000121032022010f012307200200220100010709012200202101022201010c0a4d7954657374456e756d2201012201012307210200022201010c0548656c6c6f220101220001200c0105737461746501022201010c05576f726c6422000020220100002201010a0000000000000000"; + +#[derive(BasicSborAssertion, BasicSbor)] +#[sbor_assert(backwards_compatible( + v1 = "5b210222000121032022010f012307200100220100010709202101022201010c0a4d7954657374456e756d2201012201012307210100022201010c0548656c6c6f220101220001200c0105737461746520220100002201010a0000000000000000", + v2 = "5b210222000121032022010f012307200200220100010709012200202101022201010c0a4d7954657374456e756d2201012201012307210200022201010c0548656c6c6f220101220001200c0105737461746501022201010c05576f726c6422000020220100002201010a0000000000000000", +))] +#[sbor(type_name = "MyTestEnum")] +pub enum MyTestEnum_WhichHasBeenExtended_Test1 { + Hello { state: u32 }, + World, // Extension +} + +const TEST_ENUM_NAMED_SCHEMAS: [(&'static str, &'static str); 2] = [ + // For the purposes of these tests, we use the same schema twice + ("version1", TEST_ENUM_SCHEMA), + ("version2", TEST_ENUM_V2_SCHEMA), +]; + +#[derive(BasicSborAssertion, BasicSbor)] +#[sbor_assert(backwards_compatible(TEST_ENUM_NAMED_SCHEMAS))] +#[sbor(type_name = "MyTestEnum")] +pub enum MyTestEnum_WhichHasBeenExtended_Test2 { + Hello { state: u32 }, + World, // Extension +} diff --git a/sbor-tests/tests/schema_comparison.rs b/sbor-tests/tests/schema_comparison.rs index e1cc29b78f5..9e7ad474169 100644 --- a/sbor-tests/tests/schema_comparison.rs +++ b/sbor-tests/tests/schema_comparison.rs @@ -1,6 +1,6 @@ use sbor::prelude::*; use sbor::schema::*; -use sbor::{BasicTypeAggregator, BasicValue, ComparisonSchema, NoCustomSchema, NoCustomTypeKind}; +use sbor::{BasicTypeAggregator, BasicValue, ComparableSchema, NoCustomSchema, NoCustomTypeKind}; //===================== // HELPER CODE / TRAITS @@ -19,62 +19,68 @@ impl> DerivableTypeSchema for T {} fn assert_equality() { let settings = SchemaComparisonSettings::require_equality(); - assert_single_type_comparison::( + compare_single_type_schemas::( &settings, &T1::single_type_schema_version(), &T2::single_type_schema_version(), ) + .assert_valid("base", "compared") } fn assert_equality_ignoring_name_changes() { let settings = SchemaComparisonSettings::require_equality() .metadata_settings(SchemaComparisonMetadataSettings::allow_all_changes()); - assert_single_type_comparison::( + compare_single_type_schemas::( &settings, &T1::single_type_schema_version(), &T2::single_type_schema_version(), ) + .assert_valid("base", "compared") } fn assert_extension() { let settings = SchemaComparisonSettings::allow_extension(); - assert_single_type_comparison::( + compare_single_type_schemas::( &settings, &T1::single_type_schema_version(), &T2::single_type_schema_version(), ) + .assert_valid("base", "compared") } fn assert_extension_ignoring_name_changes() { let settings = SchemaComparisonSettings::allow_extension() .metadata_settings(SchemaComparisonMetadataSettings::allow_all_changes()); - assert_single_type_comparison::( + compare_single_type_schemas::( &settings, &T1::single_type_schema_version(), &T2::single_type_schema_version(), ) + .assert_valid("base", "compared") } fn assert_type_collection_equality( - base: NamedTypesSchema, - compared: NamedTypesSchema, + base: TypeCollectionSchema, + compared: TypeCollectionSchema, ) { - assert_type_collection_comparison( + compare_type_collection_schemas( &SchemaComparisonSettings::require_equality(), &base, &compared, - ); + ) + .assert_valid("base", "compared"); } fn assert_type_collection_extension( - base: NamedTypesSchema, - compared: NamedTypesSchema, + base: TypeCollectionSchema, + compared: TypeCollectionSchema, ) { - assert_type_collection_comparison( + compare_type_collection_schemas( &SchemaComparisonSettings::allow_extension(), &base, &compared, - ); + ) + .assert_valid("base", "compared"); } //============= @@ -359,12 +365,12 @@ fn base_schema_not_covered_by_root_types_fails() { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); aggregator.add_root_type::("enum"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; let compared_schema = { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; // Forget about a root type - this leaves the schema not fully covered. base_schema.type_ids.swap_remove("enum"); @@ -378,13 +384,13 @@ fn compared_schema_not_covered_by_root_types_fails() { let base_schema = { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; let mut compared_schema = { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); aggregator.add_root_type::("enum"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; // Forget about a root type - this leaves the schema not fully covered. compared_schema.type_ids.swap_remove("enum"); @@ -402,12 +408,12 @@ fn removed_root_type_fails_comparison() { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); aggregator.add_root_type::("enum"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; let compared_schema = { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; assert_type_collection_extension(base_schema, compared_schema); @@ -418,13 +424,13 @@ fn under_extension_added_root_type_succeeds() { let base_schema = { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; let compared_schema = { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); aggregator.add_root_type::("enum"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; assert_type_collection_extension(base_schema, compared_schema); @@ -436,13 +442,13 @@ fn under_equality_added_root_type_fails() { let base_schema = { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; let compared_schema = { let mut aggregator = BasicTypeAggregator::new(); aggregator.add_root_type::("struct"); aggregator.add_root_type::("enum"); - aggregator.generate_named_types_schema() + aggregator.generate_type_collection_schema() }; assert_type_collection_equality(base_schema, compared_schema); diff --git a/sbor/src/basic.rs b/sbor/src/basic.rs index cd64e9253b1..58f47362ceb 100644 --- a/sbor/src/basic.rs +++ b/sbor/src/basic.rs @@ -1,5 +1,5 @@ +use crate::internal_prelude::*; use crate::representations::*; -use crate::rust::prelude::*; use crate::traversal::*; use crate::*; @@ -95,6 +95,35 @@ pub fn basic_decode_with_depth_limit( BasicDecoder::new(buf, depth_limit).decode_payload(BASIC_SBOR_V1_PAYLOAD_PREFIX) } +/// Decodes a data structure from a byte array. +/// +/// If an error occurs, the type's schema is exported and used to give a better error message. +/// +/// NOTE: +/// * The error path runs very slowly. This should only be used where errors are NOT expected. +/// * This should not be used where the size of compiled code is an issue, as it will pull +/// in the schema aggregation code which is large. +pub fn basic_decode_with_nice_error( + buf: &[u8], +) -> Result { + vec_decode_with_nice_error::(buf, BASIC_SBOR_V1_MAX_DEPTH) +} + +/// Decodes a data structure from a byte array. +/// +/// If an error occurs, the type's schema is exported and used to give a better error message. +/// +/// NOTE: +/// * The error path runs very slowly. This should only be used where errors are NOT expected. +/// * This should not be used where the size of compiled code is an issue, as it will pull +/// in the schema aggregation code which is large. +pub fn basic_decode_with_depth_limit_and_nice_error( + buf: &[u8], + depth_limit: usize, +) -> Result { + vec_decode_with_nice_error::(buf, depth_limit) +} + impl CustomValueKind for NoCustomValueKind { fn as_u8(&self) -> u8 { panic!("No custom type") diff --git a/sbor/src/lib.rs b/sbor/src/lib.rs index da186fefaf0..814c86009ff 100644 --- a/sbor/src/lib.rs +++ b/sbor/src/lib.rs @@ -68,8 +68,8 @@ pub use versioned::*; // Re-export derives extern crate sbor_derive; pub use sbor_derive::{ - eager_replace, BasicCategorize, BasicDecode, BasicDescribe, BasicEncode, BasicSbor, Categorize, - Decode, Describe, Encode, PermitSborAttributes, Sbor, + eager_replace, BasicCategorize, BasicDecode, BasicDescribe, BasicEncode, BasicSbor, + BasicSborAssertion, Categorize, Decode, Describe, Encode, PermitSborAttributes, Sbor, }; // extern crate self as X; in lib.rs allows ::X and X to resolve to this crate inside this crate. @@ -108,11 +108,10 @@ pub mod prelude { pub use crate::schema::prelude::*; pub use crate::value::{CustomValue as SborCustomValue, Value as SborValue}; pub use crate::value_kind::*; - pub use crate::vec_traits::*; pub use crate::versioned::*; pub use crate::{ basic_decode, basic_encode, BasicCategorize, BasicDecode, BasicDescribe, BasicEncode, - BasicSbor, + BasicSbor, BasicSborAssertion, }; pub use crate::{define_single_versioned, define_versioned}; pub use crate::{Categorize, Decode, Encode, Sbor, SborEnum, SborTuple}; @@ -121,4 +120,7 @@ pub mod prelude { pub(crate) mod internal_prelude { pub use crate::prelude::*; + // These are mostly used for more advanced use cases, + // so aren't included in the general prelude + pub use crate::vec_traits::*; } diff --git a/sbor/src/schema/custom_traits.rs b/sbor/src/schema/custom_traits.rs index b02c840627a..fb9f3a1f526 100644 --- a/sbor/src/schema/custom_traits.rs +++ b/sbor/src/schema/custom_traits.rs @@ -34,7 +34,7 @@ pub trait CustomSchema: Debug + Clone + Copy + PartialEq + Eq + 'static { type CustomTypeKindLabel: CustomTypeKindLabel; /// Should only be used for default encoding of a schema, where it's required. /// Typically you should start from a CustomExtension and not use this. - type DefaultCustomExtension: CustomExtension; + type DefaultCustomExtension: ValidatableCustomExtension<(), CustomSchema = Self>; fn linearize_type_kind( type_kind: Self::CustomAggregatorTypeKind, diff --git a/sbor/src/schema/schema_comparison.rs b/sbor/src/schema/schema_comparison.rs index d6503e8574c..a8c813992b4 100644 --- a/sbor/src/schema/schema_comparison.rs +++ b/sbor/src/schema/schema_comparison.rs @@ -330,10 +330,11 @@ impl<'s, S: CustomSchema> SchemaComparisonResult<'s, S> { let mut output = String::new(); writeln!( &mut output, - "Schema comparison FAILED between base schema ({}) and compared schema ({}) with {} errors:", + "Schema comparison FAILED between base schema ({}) and compared schema ({}) with {} {}:", base_schema_name, compared_schema_name, self.errors.len(), + if self.errors.len() == 1 { "error" } else { "errors" }, ).unwrap(); for error in &self.errors { write!(&mut output, "- ").unwrap(); @@ -1617,12 +1618,12 @@ pub struct ComparisonTypeRoot { compared_type_id: LocalTypeId, } -pub struct NamedSchemaVersions> { +pub struct NamedSchemaVersions> { ordered_versions: IndexMap, custom_schema: PhantomData, } -impl> NamedSchemaVersions { +impl> NamedSchemaVersions { pub fn new() -> Self { Self { ordered_versions: Default::default(), @@ -1630,6 +1631,18 @@ impl> NamedSchemaVersions { } } + pub fn from, K: AsRef, V: IntoSchema>( + from: F, + ) -> Self { + Self { + ordered_versions: from + .into_iter() + .map(|(name, version)| (name.as_ref().to_string(), version.into_schema())) + .collect(), + custom_schema: Default::default(), + } + } + pub fn register_version( mut self, name: impl AsRef, @@ -1655,20 +1668,19 @@ impl> NamedSchemaVersions { /// # type ScryptoCustomSchema = NoCustomSchema; /// # struct MyType; /// let base = SingleTypeSchema::from("5b...."); -/// let current = SingleTypeSchema::of_type::(); -/// assert_single_type_comparison::( -/// SchemaComparisonSettings::equality(), +/// let current = SingleTypeSchema::for_type::(); +/// compare_single_type_schema::( +/// &SchemaComparisonSettings::require_equality(), /// &base, /// ¤t, -/// ); +/// ).assert_valid("base", "compared"); /// ``` -pub fn assert_single_type_comparison( +pub fn compare_single_type_schemas<'s, S: CustomSchema>( comparison_settings: &SchemaComparisonSettings, - base: &SingleTypeSchema, - compared: &SingleTypeSchema, -) { + base: &'s SingleTypeSchema, + compared: &'s SingleTypeSchema, +) -> SchemaComparisonResult<'s, S> { base.compare_with(compared, comparison_settings) - .assert_valid("base", "compared"); } /// Designed for ensuring a type is only altered in ways which ensure @@ -1764,20 +1776,19 @@ pub fn assert_type_compatibility( +pub fn compare_type_collection_schemas<'s, S: CustomSchema>( comparison_settings: &SchemaComparisonSettings, - base: &NamedTypesSchema, - compared: &NamedTypesSchema, -) { + base: &'s TypeCollectionSchema, + compared: &'s TypeCollectionSchema, +) -> SchemaComparisonResult<'s, S> { base.compare_with(compared, comparison_settings) - .assert_valid("base", "compared"); } pub fn assert_type_collection_backwards_compatibility( - current: NamedTypesSchema, + current: TypeCollectionSchema, versions_builder: impl FnOnce( - NamedSchemaVersions>, - ) -> NamedSchemaVersions>, + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, ) { assert_type_collection_compatibility( &SchemaComparisonSettings::allow_extension(), @@ -1788,10 +1799,10 @@ pub fn assert_type_collection_backwards_compatibility( pub fn assert_type_collection_compatibility( comparison_settings: &SchemaComparisonSettings, - current: NamedTypesSchema, + current: TypeCollectionSchema, versions_builder: impl FnOnce( - NamedSchemaVersions>, - ) -> NamedSchemaVersions>, + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, ) { assert_schema_compatibility( comparison_settings, @@ -1800,7 +1811,7 @@ pub fn assert_type_collection_compatibility( ) } -fn assert_schema_compatibility>( +fn assert_schema_compatibility>( schema_comparison_settings: &SchemaComparisonSettings, current: &C, named_versions: &NamedSchemaVersions, @@ -1813,7 +1824,7 @@ fn assert_schema_compatibility>( let mut error = String::new(); writeln!( &mut error, - "You must provide at least one named versioned schema to use this method." + "You must provide at least one named schema version." ) .unwrap(); writeln!(&mut error, "Use a relevant name (for example, the current software version name), and save the current schema as follows:").unwrap(); @@ -1873,7 +1884,7 @@ impl SingleTypeSchema { Self { schema, type_id } } - pub fn from>(from: &T) -> Self { + pub fn from>(from: T) -> Self { from.into_schema() } @@ -1882,7 +1893,7 @@ impl SingleTypeSchema { } } -impl ComparisonSchema for SingleTypeSchema { +impl ComparableSchema for SingleTypeSchema { fn compare_with<'s>( &'s self, compared: &'s Self, @@ -1914,12 +1925,12 @@ impl IntoSchema for SingleTypeSchema { /// For example, traits, or blueprint interfaces. #[derive(Debug, Clone, Sbor)] #[sbor(child_types = "S::CustomLocalTypeKind, S::CustomTypeValidation")] -pub struct NamedTypesSchema { +pub struct TypeCollectionSchema { pub schema: VersionedSchema, pub type_ids: IndexMap, } -impl NamedTypesSchema { +impl TypeCollectionSchema { pub fn new(schema: VersionedSchema, type_ids: IndexMap) -> Self { Self { schema, type_ids } } @@ -1929,11 +1940,11 @@ impl NamedTypesSchema { } pub fn from_aggregator(aggregator: TypeAggregator) -> Self { - aggregator.generate_named_types_schema::() + aggregator.generate_type_collection_schema::() } } -impl ComparisonSchema for NamedTypesSchema { +impl ComparableSchema for TypeCollectionSchema { fn compare_with<'s>( &'s self, compared: &'s Self, @@ -1948,14 +1959,15 @@ impl ComparisonSchema for NamedTypesSchema { } } -impl IntoSchema for NamedTypesSchema { +impl IntoSchema for TypeCollectionSchema { fn into_schema(&self) -> Self { self.clone() } } -// Marker trait -pub trait ComparisonSchema: Clone + VecSbor { +/// Marker trait for SingleTypeSchema and NamedTypesSchema which includes named pointers to types, +/// which can be used for comparisons of different versions of the same schema. +pub trait ComparableSchema: Clone + VecSbor { fn encode_to_bytes(&self) -> Vec { vec_encode::(self, BASIC_SBOR_V1_MAX_DEPTH).unwrap() } @@ -1965,11 +1977,24 @@ pub trait ComparisonSchema: Clone + VecSbor Self { - vec_decode::(bytes, BASIC_SBOR_V1_MAX_DEPTH).unwrap() + vec_decode_with_nice_error::( + bytes, + BASIC_SBOR_V1_MAX_DEPTH, + ) + .unwrap_or_else(|err| { + panic!( + "Could not SBOR decode bytes into {} with {}: {:?}", + core::any::type_name::(), + core::any::type_name::(), + err, + ) + }) } fn decode_from_hex(hex: &str) -> Self { - Self::decode_from_bytes(&hex::decode(hex).unwrap()) + let bytes = hex::decode(hex) + .unwrap_or_else(|err| panic!("Provided string was not valid hex: {err}")); + Self::decode_from_bytes(&bytes) } fn compare_with<'s>( @@ -1979,11 +2004,11 @@ pub trait ComparisonSchema: Clone + VecSbor SchemaComparisonResult<'s, S>; } -pub trait IntoSchema, S: CustomSchema> { +pub trait IntoSchema, S: CustomSchema> { fn into_schema(&self) -> C; } -impl<'a, C: ComparisonSchema, S: CustomSchema, T: IntoSchema + ?Sized> IntoSchema +impl<'a, C: ComparableSchema, S: CustomSchema, T: IntoSchema + ?Sized> IntoSchema for &'a T { fn into_schema(&self) -> C { @@ -1991,28 +2016,32 @@ impl<'a, C: ComparisonSchema, S: CustomSchema, T: IntoSchema + ?Sized> } } -impl, S: CustomSchema> IntoSchema for [u8] { +impl, S: CustomSchema> IntoSchema for [u8] { fn into_schema(&self) -> C { C::decode_from_bytes(self) } } -impl, S: CustomSchema> IntoSchema for Vec { +impl, S: CustomSchema> IntoSchema for Vec { fn into_schema(&self) -> C { C::decode_from_bytes(self) } } -impl, S: CustomSchema> IntoSchema for String { +impl, S: CustomSchema> IntoSchema for String { fn into_schema(&self) -> C { C::decode_from_hex(self) } } -impl, S: CustomSchema> IntoSchema for str { +impl, S: CustomSchema> IntoSchema for str { fn into_schema(&self) -> C { C::decode_from_hex(self) } } +/// Marker traits intended to be implemented by the SborAssert macros +pub trait CheckedFixedSchema: CheckedBackwardsCompatibleSchema {} +pub trait CheckedBackwardsCompatibleSchema {} + // NOTE: Types are in sbor-tests/tests/schema_comparison.rs diff --git a/sbor/src/schema/type_aggregator.rs b/sbor/src/schema/type_aggregator.rs index 2ac551b774e..d39ab100bbb 100644 --- a/sbor/src/schema/type_aggregator.rs +++ b/sbor/src/schema/type_aggregator.rs @@ -236,10 +236,10 @@ impl> TypeAggregator { return true; } - pub fn generate_named_types_schema>( + pub fn generate_type_collection_schema>( self, - ) -> NamedTypesSchema { - NamedTypesSchema::new( + ) -> TypeCollectionSchema { + TypeCollectionSchema::new( generate_schema_from_types(self.types), self.named_root_types, ) diff --git a/sbor/src/traversal/untyped/traverser.rs b/sbor/src/traversal/untyped/traverser.rs index 4096e98cd51..812222b0944 100644 --- a/sbor/src/traversal/untyped/traverser.rs +++ b/sbor/src/traversal/untyped/traverser.rs @@ -370,6 +370,7 @@ impl<'t, 'd, 'de, T: CustomTraversal> ActionHandler<'t, 'd, 'de, T> { self.complete(TraversalEvent::End, NextAction::Ended) } + #[inline] fn read_value_body( self, value_kind: ValueKind, diff --git a/sbor/src/vec_traits.rs b/sbor/src/vec_traits.rs index ac491f3a016..47559947b58 100644 --- a/sbor/src/vec_traits.rs +++ b/sbor/src/vec_traits.rs @@ -1,6 +1,6 @@ use crate::{ - internal_prelude::*, CustomExtension, CustomSchema, Decoder as _, Describe, Encoder as _, - VecDecoder, VecEncoder, + internal_prelude::*, validate_payload_against_schema, CustomExtension, CustomSchema, + Decoder as _, Describe, Encoder as _, ValidatableCustomExtension, VecDecoder, VecEncoder, }; pub trait VecEncode: for<'a> Encode> {} @@ -26,6 +26,38 @@ pub fn vec_decode>( VecDecoder::<'_, E::CustomValueKind>::new(buf, max_depth).decode_payload(E::PAYLOAD_PREFIX) } +pub fn vec_decode_with_nice_error< + E: ValidatableCustomExtension<()>, + T: VecDecode + + Describe<::CustomAggregatorTypeKind>, +>( + buf: &[u8], + max_depth: usize, +) -> Result { + vec_decode::(buf, max_depth) + .map_err(|err| create_nice_error_following_decode_error::(buf, err, max_depth)) +} + +pub fn create_nice_error_following_decode_error< + E: ValidatableCustomExtension<()>, + T: Describe<::CustomAggregatorTypeKind>, +>( + buf: &[u8], + decode_error: DecodeError, + max_depth: usize, +) -> String { + let (local_type_id, schema) = generate_full_schema_from_single_type::(); + let schema = schema.as_unique_version(); + match validate_payload_against_schema::(buf, schema, local_type_id, &(), max_depth) { + Ok(()) => { + // This case is unexpected. We got a decode error, but it's valid against the schema. + // In this case, let's just debug-print the DecodeError. + format!("{decode_error:?}") + } + Err(err) => err.error_message(schema), + } +} + pub trait VecSbor: Categorize + VecEncode From 075af0be0688f501949d6687630285a5dc4dc5b5 Mon Sep 17 00:00:00 2001 From: David Edey Date: Wed, 3 Jul 2024 18:14:38 +0100 Subject: [PATCH 13/19] feat: Add file support to sbor_assert attribute --- radix-clis/Cargo.lock | 1 + .../transaction/receipt_schema_bottlenose.txt | 1 + .../src/transaction/system_structure.rs | 14 +- .../src/transaction/system_structure_v1.txt | 1 + .../src/transaction/transaction_receipt.rs | 15 +- radix-sbor-derive/src/lib.rs | 92 +++ sbor-derive-common/Cargo.toml | 1 + sbor-derive-common/src/sbor_assert.rs | 529 +++++++++++++----- sbor-derive-common/src/utils.rs | 14 +- sbor-derive/src/lib.rs | 92 +++ sbor-tests/tests/sbor_assert.rs | 53 +- sbor-tests/tests/test_enum_v1_schema.txt | 1 + sbor/src/versioned.rs | 80 ++- 13 files changed, 737 insertions(+), 157 deletions(-) create mode 100644 radix-engine/src/transaction/receipt_schema_bottlenose.txt create mode 100644 radix-engine/src/transaction/system_structure_v1.txt create mode 100644 sbor-tests/tests/test_enum_v1_schema.txt diff --git a/radix-clis/Cargo.lock b/radix-clis/Cargo.lock index 3ee2e5c994f..68d3ec65921 100644 --- a/radix-clis/Cargo.lock +++ b/radix-clis/Cargo.lock @@ -1546,6 +1546,7 @@ name = "sbor-derive-common" version = "1.3.0-dev" dependencies = [ "const-sha1", + "indexmap 2.2.6", "itertools", "proc-macro2", "quote", diff --git a/radix-engine/src/transaction/receipt_schema_bottlenose.txt b/radix-engine/src/transaction/receipt_schema_bottlenose.txt new file mode 100644 index 00000000000..7a20c2aa935 --- /dev/null +++ b/radix-engine/src/transaction/receipt_schema_bottlenose.txt @@ -0,0 +1 @@ +5c210222000121032022cb010f012307200100220101010a01000000000000000e0120220601010a020000000000000001010a030000000000000001010a040000000000000001010a050000000000000001010a080000000000000001010ac9000000000000000e01202208000107c00001070900010709000107c000010709000107c0000107c0000107c00e0120220200010708000107c00e012022070001070900010709000107c0000107c0000107c0000107c0000107c00f012307200200220001220101010a06000000000000000e0120220201010a070000000000000001010a07000000000000001002220001070c22000107090f012307200300220101010a090000000000000001220101010ac30000000000000002220101010ac8000000000000000e0120220901010a0a0000000000000001010a180000000000000001010a210000000000000001010a230000000000000001010a260000000000000001010a990000000000000001010a9d0000000000000001010aa00000000000000001010ab3000000000000000e0120220101010a0b0000000000000010022201010a0c000000000000002201010a0d000000000000000d0122000107070f012307200100220101010a0e0000000000000010022201010a0f000000000000002201010a100000000000000007000f012307200200220101010a110000000000000001220101010a160000000000000010022201010a12000000000000002201010a15000000000000000f0123072003002201000107070122010001074102220101010a13000000000000000e0120220201010a1400000000000000000107410d0122000107070f0123072002002201000107410122000f012307200100220101010a170000000000000010022201010a120000000000000022000107410e0120220501010a190000000000000001010a1a0000000000000001010a1b0000000000000001010a1c0000000000000001010a1d000000000000000d0122000107830d0122000107840d0122000107850d01220001078210022201010a0c000000000000002201010a1e000000000000000e012022020001078501010a1f000000000000000f0123072002002201000107c001220201010a200000000000000001010a20000000000000000d0122000107c20e0120220101010a220000000000000010022201010a0c0000000000000022000107c00e01202204000107c0000107c0000107c001010a240000000000000010022201010a250000000000000022000107c00f01230720020022020001078301010a0c000000000000000122020001078401010a0c000000000000000f012307200200220101010a270000000000000001220101010a29000000000000000d012201010a28000000000000000f0123072002002201000107410122000f012307200700220101010a2a0000000000000001220101010a450000000000000002220101010a5e0000000000000003220101010a6b0000000000000004220101010a6d0000000000000005220101010a710000000000000006220101010a66000000000000000f012307200400220101010a2b0000000000000001220101010a43000000000000000222010001070903220101010a44000000000000000f012307201000220101010a2c0000000000000001220101010a2d0000000000000002220101010a2f0000000000000003220101010a350000000000000004220101010a360000000000000005220101010a370000000000000006220101010a390000000000000007220101010a3a0000000000000008220101010a3b0000000000000009220101010a3c000000000000000a220101010a3d000000000000000b220101010a3e000000000000000c220101010a3f000000000000000d220101010a40000000000000000e220101010a41000000000000000f220101010a42000000000000000f012307200100220101010a2d000000000000000f012307200400220101010a2e0000000000000001220101010a0c0000000000000002220101010a0c0000000000000003220101010a0c000000000000000f012307200200220101010a0c0000000000000001220101010a0c000000000000000f012307200300220101010a300000000000000001220101010a320000000000000002220101010a34000000000000000f012307200600220101010a2e0000000000000001220101010a0c0000000000000002220101010a0c0000000000000003220101010a0c0000000000000004220101010a0c0000000000000005220101010a31000000000000000f012307200300220101010a0c0000000000000001220101010a0c0000000000000002220101010a0c000000000000000f012307200400220101010a0c0000000000000001220101010a33000000000000000222000322000f012307200f0022010001070a0122020001070a0001070a02220200010707000107070322020001070700010707042201000107070522020001070a0001070a06220200010707000107070722010001070708220100010707092201000107070a22000b22000c22010001070a0d22000e22000f01230720010022000f012307200400220101010a2e0000000000000001220101010a0c0000000000000002220101010a0c0000000000000003220101010a30000000000000000f012307200100220101010a0c000000000000000f012307200600220101010a0c0000000000000001220101010a380000000000000002220101010a0c0000000000000003220101010a310000000000000004220101010a0c000000000000000522000f012307200200220101010a0c0000000000000001220101010a0f000000000000000f012307200100220101010a0c000000000000000f012307200800220101010a0c0000000000000001220002220003220101010a320000000000000004220301010a0c0000000000000001010a0f0000000000000001010a120000000000000005220006220301010a0c0000000000000001010a0f0000000000000001010a120000000000000007220301010a0c0000000000000001010a0f0000000000000001010a12000000000000000f01230720020022010001070901220101010a0c000000000000000f0123072001002201000107090f01230720040022010001070901220101010a300000000000000002220003220101010a34000000000000000f012307200200220101010a0c0000000000000001220101010a32000000000000000f012307200300220101010a0c0000000000000001220101010a0c0000000000000002220101010a32000000000000000f012307200300220101010a0c0000000000000001220101010a0c0000000000000002220101010a32000000000000000f012307200300220101010a0c0000000000000001220301010a0c0000000000000001010a0f0000000000000001010a120000000000000002220101010a32000000000000000f012307200300220101010a0c0000000000000001220301010a0c0000000000000001010a0f0000000000000001010a120000000000000002220101010a32000000000000000f01230720010022000d012201010a0c000000000000000f012307202f0022000122000222000322000422000522000622000722000822000922000a22000b22000c22000d22000e22000f220010220101010a460000000000000011220201010a4b000000000000000001070712220201010a4b000000000000000001070713220401010a4b000000000000000001070701010a560000000000000001010a56000000000000001422001522020001070900010707162201000107f11722001822001922001a220101010a57000000000000001b2201000107f01c22001d22001e220101010a58000000000000001f220101010a59000000000000002022002122010001070c22220023220101010a5a0000000000000024220101010a5a0000000000000025220101010a5b0000000000000026220101010a5d000000000000002722002822002922002a22002b22010001070c2c22010001070c2d22002e22010001070c0f01230720090022020001070a0001070a01220101010a470000000000000002220101010a490000000000000003220201010a4a000000000000000001070704220201010a4a0000000000000001010a530000000000000005220301010a4a0000000000000001010a53000000000000000001070c06220201010a55000000000000000001070c0722000822000f012307200200220101010a48000000000000000122010001070a07000e01202203000107830001070c0001070c0e0120220501010a4b0000000000000001010a4c0000000000000001010a4d0000000000000001010a4e0000000000000001010a4f000000000000000e01202202000107830001070c0e012022030001070900010709000107090f0123072002002201000107810122000d01220001070c0d012201010a50000000000000000f012307200200220101010a510000000000000001220101010a49000000000000000e0120220201010a520000000000000001010a47000000000000000d0122000107070f01230720060022020001070c01010a54000000000000000122010001070c022201000107070322020001070701010a55000000000000000422020001070701010a55000000000000000522020001070701010a55000000000000000f01230720020022000122000f01230720020022000122000f01230720030022000122000222000f01230720030022000122000222000e0120220201010a4b0000000000000001010a4b000000000000000f01230720060022010001070c01220201010a4b000000000000000001070702220201010a4b000000000000000001070703220201010a4b000000000000000001070704220201010a4b000000000000000001070c0522010001070c0e01202203000107830001070c01010a4c000000000000000e01202203000107830001070c01010a5c000000000000000f0123072002002200012201000107830e0120220401010a0c00000000000000000107830001070c01010a5c000000000000000f012307200400220101010a5f0000000000000001220101010a660000000000000002220101010a690000000000000003220101010a6a000000000000000f012307200600220101010a600000000000000001220101010a600000000000000002220101010a0c0000000000000003220101010a61000000000000000422010001070c0522000e0120220201010a4b000000000000000001070c0e0120220201010a620000000000000001010a60000000000000000f012307200200220101010a630000000000000001220101010a65000000000000000d012201010a64000000000000000e01202202000107e701010a65000000000000000d0122000107e00f012307200100220101010a67000000000000000f0123072005002202000107c0000107c0012200022203000107090001070900010709032201000107c004220101010a68000000000000000f01230720010022000f012307200b0022010001070a0122010001070a0222010001070a0322000422020001070a0001070a0522020001070a0001070a0622020001070a0001070a0722020001070a0001070a0822020001070a0001070a0922000a22000f012307200400220201010a4b000000000000000001070c0122010001070c0222000322000f01230720080022000122010001070c0222010001070c03220101010a6c0000000000000004220101010a33000000000000000522020001070c0001070c06220101010a33000000000000000722020001070c0001070c0f01230720030022000122000222000f012307200300220101010a6e0000000000000001220101010a6f0000000000000002220101010a70000000000000000f01230720020022000122030001070c000107400001070c0f012307201b0022010001070c0122000222000322010001070c0422000522010001070906220101010a33000000000000000722000822000922000a2201000107090b2201000107090c220101010a33000000000000000d220101010a33000000000000000e220101010a33000000000000000f220101010a330000000000000010220101010a330000000000000011220012220101010a330000000000000013220101010a67000000000000001422010001070915220016220017220101010a330000000000000018220101010a330000000000000019220101010a33000000000000001a22000f01230720010022010001070a0f01230720170022010001070c01220101010a33000000000000000222010001070c03220101010a720000000000000004220101010a740000000000000005220101010a760000000000000006220101010a770000000000000007220101010a7a0000000000000008220101010a810000000000000009220101010a83000000000000000a220101010a84000000000000000b220101010a85000000000000000c220101010a88000000000000000d220101010a8a000000000000000e220101010a8b000000000000000f220101010a8c0000000000000010220101010a8d0000000000000011220101010a8e0000000000000012220101010a900000000000000013220101010a910000000000000014220101010a960000000000000015220101010a970000000000000016220101010a98000000000000000f01230720080022010001070c0122000222020001070a0001070a03220004220005220101010a73000000000000000622000722000f01230720020022000122030001070c0001070c0001070a0f01230720040022020001070a0001070a0122020001070a0001070a02220101010a330000000000000003220101010a75000000000000000f01230720020022010001070c0122010001070c0f0123072003002202000107f2000107f2012200022201000107f20f012307200d0022010001070901220100010709022201000107090322010001070904220101010a780000000000000005220101010a330000000000000006220101010a330000000000000007220101010a0c0000000000000008220101010a0c000000000000000922000a220101010a33000000000000000b220101010a79000000000000000c22000d0122000107070f01230720050022010001070a0122020001070a0001070a0222020001070700010707032202000107070001070704220200010707000107070f012307202900220101010a7b0000000000000001220101010a7f000000000000000222000322010001070c04220005220006220101010a47000000000000000722020001070c01010a80000000000000000822020001070c01010a80000000000000000922000a22000b22000c220101010a73000000000000000d22000e22010001070c0f220101010a4700000000000000102201000107071122001222020001070c0001070c13220101010a72000000000000001422001522020001070c000107e71622020001070a0001070a1722020001070a0001070a1822020001070a0001070a1922020001070a0001070a1a22020001070a0001070a1b22020001070a0001070a1c22020001070a0001070a1d2201000107e71e22030001070c0001070a0001070a1f22020001070c0001070c2022030001070c0001070a0001070a2122020001070c0001070c2222030001070c0001070a0001070a2322020001070c0001070c242202000107f2000107f22522010001070c262200272201000107f22822010001070c0f01230720180022000122010001070c02220003220004220101010a7c0000000000000005220101010a7d0000000000000006220101010a7e000000000000000722010001070c0822000922000a22000b220200010709000107090c220200010709000107090d22000e22010001070c0f22001022001122010001070c1222010001070c1322010001070c1422001522010001070c1622010001070c1722000f01230720030022010001070c0122030001070c0001070a0001070a0222010001070c0f01230720050022000122000222000322000422000f01230720020022000122000f01230720160022000122000222010001070a0322010001070a0422000522000622000722000822000922000a22000b22000c22000d22000e22000f22001022001122010001070c1222001322001422001522000f01230720020022000122010001070c0f012307200a00220201010a820000000000000001010a820000000000000001220200010705000107050222020001070a0001070a032202000107070001070a0422000522000622000722000822010001070509220200010709000107090a000f01230720090022000122000222000322000422000522000622000722020001070a0001070a0822000f0123072007002202000107c000010707012200022201000107070322000422000522000622000f012307200b002201000107c3012201000107c30222010001070c03220201010a860000000000000001010a860000000000000004220005220101010a87000000000000000622000722000822000922000a22000f01230720040022000122000222000322000f012307200500220101010a7f000000000000000122000222000322000422010001070c0f012307200500220101010a890000000000000001220101010a8a0000000000000002220101010a0c00000000000000032201000107c00422000f0123072004002202000107c0000107c0012200022201000107c20322000f01230720010022000f0123072003002201000107c20122000222000f012307200900220101010a890000000000000001220101010a8a00000000000000022201000107c0032200042200052200062200072202000107c0000107c00822000f01230720020022000122000f012307200100220101010a8f000000000000000f01230720040022000122000222000322000f01230720040022010001078501220100010785022200032201000107e40f012307200a00220001220002220101010a920000000000000003220101010a920000000000000004220101010a920000000000000005220101010a920000000000000006220007220008220201010a930000000000000001010a93000000000000000922000f01230720020022000122000e0120220201010a940000000000000001010a95000000000000000e01202203000107e0000107e0000107e00f0123072002002200012201000107090f0123072009002201000107850122000222020001078500010785032200042200052200062200072200082201000107850f012307200a002201000107850122000222020001078500010785032201000107850422000522000622000722000822000922000f012307200d0022010001078501220002220200010785000107850322010001078504220101010a1b000000000000000522000622000722000822000922000a22000b22000c22000d012201010a9a000000000000000e0120220201010a9b00000000000000000107410e0120220201010a9c000000000000000001070c0f012307200200220101010a4b0000000000000001220201010a0c00000000000000000107f00d012201010a9e000000000000000e0120220201010a9f000000000000000001070c0f01230720050022000122000222000322000422000e0120220201010aa10000000000000001010ab10000000000000010022201010a0c000000000000002201010aa20000000000000010022201010a0f000000000000002201010aa30000000000000010022201010a12000000000000002201010aa4000000000000000f012307200700220101010aa50000000000000001220002220101010aa70000000000000003220101010aa90000000000000004220101010aae0000000000000005220101010aaf0000000000000006220101010ab0000000000000000e0120220101010aa6000000000000000f01230720040022000122000222000322000e0120220201010aa80000000000000001010aa8000000000000000e0120220301010a0c0000000000000001010a520000000000000001010a47000000000000000e0120220101010aaa000000000000000f012307200200220101010aab0000000000000001220101010aad000000000000000e0120220101010aac000000000000000e012022030001078301010a520000000000000001010a47000000000000000e012022020001070701010aa8000000000000000e0120220201010aaa0000000000000001010aaa000000000000000e0120220201010aaa0000000000000001010aaa000000000000000e0120220201010aaa0000000000000001010aaa0000000000000010022201010a9b000000000000002201010ab2000000000000000e0120220101010aab000000000000000f012307200200220001220101010ab4000000000000000e0120220301010ab50000000000000001010abf0000000000000001010ac2000000000000000d012201010ab6000000000000000e0120220801010ab7000000000000000001070a01010ab9000000000000000001070a0001070a01010aba0000000000000001010aba0000000000000001010ab5000000000000000f012307200400220101010ab80000000000000001220101010ab8000000000000000222000322000e0120220201010a4b000000000000000001070c0f012307200200220101010a0c000000000000000122000e0120220201010abb0000000000000001010abd0000000000000010022201010a0c000000000000002201010abc000000000000000f012307200200220200010785000107c00122020001078501010a200000000000000010022201010a0c000000000000002201010abe000000000000000f012307200200220200010785000107c00122020001078501010a20000000000000001002220001070a2201010ac0000000000000000d012201010ac1000000000000000e0120220401010a0c0000000000000001010a0c0000000000000000010785000107c00e01202202000107c0000107c00e0120220101010ac4000000000000000f012307200700220201010ac50000000000000001010ac50000000000000001220201010ac60000000000000001010ac50000000000000002220003220004220101010ac70000000000000005220101010a29000000000000000622000a000f012307200200220001220101010ac5000000000000000f012307200400220101010a0c0000000000000001220101010a0c0000000000000002220101010a0c0000000000000003220101010a66000000000000000e0120220101010a68000000000000000f012307200200220001220101010aca000000000000000e012022030001070a0001070a0001070a2021cb01022201010c1b56657273696f6e65645472616e73616374696f6e526563656970742201012201012307210100022201010c025631220000022201010c145472616e73616374696f6e526563656970745631220101220001200c0612636f7374696e675f706172616d65746572731e7472616e73616374696f6e5f636f7374696e675f706172616d65746572730b6665655f73756d6d6172790b6665655f64657461696c7306726573756c740f7265736f75726365735f7573616765022201010c11436f7374696e67506172616d6574657273220101220001200c0819657865637574696f6e5f636f73745f756e69745f707269636519657865637574696f6e5f636f73745f756e69745f6c696d697418657865637574696f6e5f636f73745f756e69745f6c6f616e1c66696e616c697a6174696f6e5f636f73745f756e69745f70726963651c66696e616c697a6174696f6e5f636f73745f756e69745f6c696d6974097573645f70726963651373746174655f73746f726167655f707269636515617263686976655f73746f726167655f7072696365022201010c235472616e73616374696f6e436f7374696e67506172616d657465727352656365697074220101220001200c020e7469705f70657263656e7461676512667265655f6372656469745f696e5f787264022201010c155472616e73616374696f6e46656553756d6d617279220101220001200c0723746f74616c5f657865637574696f6e5f636f73745f756e6974735f636f6e73756d656426746f74616c5f66696e616c697a6174696f6e5f636f73745f756e6974735f636f6e73756d65641b746f74616c5f657865637574696f6e5f636f73745f696e5f7872641e746f74616c5f66696e616c697a6174696f6e5f636f73745f696e5f78726419746f74616c5f74697070696e675f636f73745f696e5f78726419746f74616c5f73746f726167655f636f73745f696e5f78726419746f74616c5f726f79616c74795f636f73745f696e5f787264022201010c064f7074696f6e2201012201012307210200022201010c044e6f6e6522000001022201010c04536f6d65220000022201010c155472616e73616374696f6e46656544657461696c73220101220001200c0218657865637574696f6e5f636f73745f627265616b646f776e1b66696e616c697a6174696f6e5f636f73745f627265616b646f776e02220000220000022201010c115472616e73616374696f6e526573756c742201012201012307210300022201010c06436f6d6d697422000001022201010c0652656a65637422000002022201010c0541626f7274220000022201010c0c436f6d6d6974526573756c74220101220001200c090d73746174655f757064617465731473746174655f7570646174655f73756d6d6172790a6665655f736f757263650f6665655f64657374696e6174696f6e076f7574636f6d65126170706c69636174696f6e5f6576656e7473106170706c69636174696f6e5f6c6f67731073797374656d5f7374727563747572650f657865637574696f6e5f7472616365022201010c0c537461746555706461746573220101220001200c010762795f6e6f646502220000220000022201010c064e6f64654964220000022201010c104e6f64655374617465557064617465732201012201012307210100022201010c0544656c7461220101220001200c010c62795f706172746974696f6e02220000220000022201010c0f506172746974696f6e4e756d626572220000022201010c15506172746974696f6e5374617465557064617465732201012201012307210200022201010c0544656c7461220101220001200c010b62795f737562737461746501022201010c05426174636822000002220000220000022201010c0b53756273746174654b65792201012201012307210300022201010c054669656c6422000001022201010c034d617022000002022201010c06536f727465642200000222000022000002220000220000022201010c0e44617461626173655570646174652201012201012307210200022201010c0353657422000001022201010c0644656c657465220000022201010c194261746368506172746974696f6e53746174655570646174652201012201012307210100022201010c055265736574220101220001200c01136e65775f73756273746174655f76616c75657302220000220000022201010c12537461746555706461746553756d6d617279220101220001200c050c6e65775f7061636b616765730e6e65775f636f6d706f6e656e74730d6e65775f7265736f75726365730a6e65775f7661756c7473157661756c745f62616c616e63655f6368616e676573022200002200000222000022000002220000220000022200002200000222000022000002220000220000022201010c0d42616c616e63654368616e67652201012201012307210200022201010c0846756e6769626c6522000001022201010c0b4e6f6e46756e6769626c65220101220001200c020561646465640772656d6f76656402220000220000022201010c09466565536f75726365220101220001200c010d706179696e675f7661756c747302220000220000022201010c0e46656544657374696e6174696f6e220101220001200c040b746f5f70726f706f73657210746f5f76616c696461746f725f73657407746f5f6275726e15746f5f726f79616c74795f726563697069656e747302220000220000022201010c10526f79616c7479526563697069656e742201012201012307210200022201010c075061636b61676522000001022201010c09436f6d706f6e656e74220000022201010c125472616e73616374696f6e4f7574636f6d652201012201012307210200022201010c075375636365737322000001022201010c074661696c75726522000002220000220000022201010c11496e737472756374696f6e4f75747075742201012201012307210200022201010c0a43616c6c52657475726e22000001022201010c044e6f6e65220000022201010c0c52756e74696d654572726f722201012201012307210700022201010c0b4b65726e656c4572726f7222000001022201010c0b53797374656d4572726f7222000002022201010c1153797374656d4d6f64756c654572726f7222000003022201010c1353797374656d557073747265616d4572726f7222000004022201010c07566d4572726f7222000005022201010c104170706c69636174696f6e4572726f7222000006022201010c1846696e616c697a6174696f6e436f7374696e674572726f72220000022201010c0b4b65726e656c4572726f722201012201012307210400022201010c0e43616c6c4672616d654572726f7222000001022201010c114964416c6c6f636174696f6e4572726f7222000002022201010c1a537562737461746548616e646c65446f65734e6f74457869737422000003022201010c0d4f727068616e65644e6f646573220000022201010c0e43616c6c4672616d654572726f722201012201012307211000022201010c104372656174654672616d654572726f7222000001022201010c10506173734d6573736167654572726f7222000002022201010c0f4372656174654e6f64654572726f7222000003022201010c0d44726f704e6f64654572726f7222000004022201010c0c50696e4e6f64654572726f7222000005022201010c124d6f7665506172746974696f6e4572726f7222000006022201010c1a4d61726b5472616e7369656e7453756273746174654572726f7222000007022201010c114f70656e53756273746174654572726f7222000008022201010c12436c6f736553756273746174654572726f7222000009022201010c115265616453756273746174654572726f722200000a022201010c12577269746553756273746174654572726f722200000b022201010c125363616e5375627374617465734572726f722200000c022201010c13447261696e5375627374617465734572726f722200000d022201010c185363616e536f727465645375627374617465734572726f722200000e022201010c115365745375627374617465734572726f722200000f022201010c1452656d6f76655375627374617465734572726f72220000022201010c104372656174654672616d654572726f722201012201012307210100022201010c10506173734d6573736167654572726f72220000022201010c10506173734d6573736167654572726f722201012201012307210400022201010c0d54616b654e6f64654572726f7222000001022201010c11476c6f62616c5265664e6f74466f756e6422000002022201010c114469726563745265664e6f74466f756e6422000003022201010c145472616e7369656e745265664e6f74466f756e64220000022201010c0d54616b654e6f64654572726f722201012201012307210200022201010c0b4f776e4e6f74466f756e6422000001022201010c105375627374617465426f72726f776564220000022201010c0f4372656174654e6f64654572726f722201012201012307210300022201010c1450726f6365737353756273746174654572726f7222000001022201010c1750726f6365737353756273746174654b65794572726f7222000002022201010c115375627374617465446966664572726f72220000022201010c1450726f6365737353756273746174654572726f722201012201012307210600022201010c0d54616b654e6f64654572726f7222000001022201010c1343616e7444726f704e6f6465496e53746f726522000002022201010c0b5265664e6f74466f756e6422000003022201010c1852656643616e7442654164646564546f537562737461746522000004022201010c164e6f6e476c6f62616c5265664e6f74416c6c6f77656422000005022201010c10506572736973744e6f64654572726f72220000022201010c10506572736973744e6f64654572726f722201012201012307210300022201010c14436f6e7461696e734e6f6e476c6f62616c52656622000001022201010c0c4e6f6465426f72726f77656422000002022201010c1743616e6e6f745065727369737450696e6e65644e6f6465220000022201010c1750726f6365737353756273746174654b65794572726f722201012201012307210400022201010c0e4e6f64654e6f7456697369626c6522000001022201010c0b4465636f64654572726f7222000002022201010c154f776e65644e6f64654e6f74537570706f7274656422000003022201010c184e6f6e476c6f62616c5265664e6f74537570706f72746564220000022201010c0b4465636f64654572726f722201012201012307210f00022201010c124578747261547261696c696e67427974657322000001022201010c0f427566666572556e646572666c6f77220101220001200c020872657175697265640972656d61696e696e6702022201010c17556e65787065637465645061796c6f6164507265666978220101220001200c020865787065637465640661637475616c03022201010c13556e657870656374656456616c75654b696e64220101220001200c020865787065637465640661637475616c04022201010c19556e6578706563746564437573746f6d56616c75654b696e64220101220001200c010661637475616c05022201010c0e556e657870656374656453697a65220101220001200c020865787065637465640661637475616c06022201010c17556e65787065637465644469736372696d696e61746f72220101220001200c020865787065637465640661637475616c07022201010c10556e6b6e6f776e56616c75654b696e6422000008022201010c14556e6b6e6f776e4469736372696d696e61746f7222000009022201010c0b496e76616c6964426f6f6c2200000a022201010c0b496e76616c6964557466382200000b022201010c0b496e76616c696453697a652200000c022201010c104d6178446570746845786365656465642200000d022201010c0c4475706c69636174654b65792200000e022201010c12496e76616c6964437573746f6d56616c7565220000022201010c115375627374617465446966664572726f722201012201012307210100022201010c15436f6e7461696e734475706c69636174654f776e73220000022201010c0d44726f704e6f64654572726f722201012201012307210400022201010c0d54616b654e6f64654572726f7222000001022201010c0c4e6f6465426f72726f77656422000002022201010c105375627374617465426f72726f77656422000003022201010c1450726f6365737353756273746174654572726f72220000022201010c0c50696e4e6f64654572726f722201012201012307210100022201010c0e4e6f64654e6f7456697369626c65220000022201010c124d6f7665506172746974696f6e4572726f722201012201012307210600022201010c104e6f64654e6f74417661696c61626c6522000001022201010c184865617052656d6f7665506172746974696f6e4572726f7222000002022201010c164e6f6e476c6f62616c5265664e6f74416c6c6f77656422000003022201010c10506572736973744e6f64654572726f7222000004022201010c105375627374617465426f72726f77656422000005022201010c194d6f766546726f6d53746f72654e6f745065726d6974746564220000022201010c184865617052656d6f7665506172746974696f6e4572726f722201012201012307210200022201010c0c4e6f64654e6f74466f756e6422000001022201010c0e4d6f64756c654e6f74466f756e64220000022201010c1a4d61726b5472616e7369656e7453756273746174654572726f722201012201012307210100022201010c0e4e6f64654e6f7456697369626c65220000022201010c114f70656e53756273746174654572726f722201012201012307210800022201010c0e4e6f64654e6f7456697369626c6522000001022201010c0d53756273746174654661756c7422000002022201010c13496e76616c696444656661756c7456616c756522000003022201010c1750726f6365737353756273746174654b65794572726f7222000004022201010c0e53756273746174654c6f636b656422000005022201010c1c4c6f636b556e6d6f646966696564426173654f6e486561704e6f646522000006022201010c1f4c6f636b556e6d6f646966696564426173654f6e4e6577537562737461746522000007022201010c254c6f636b556e6d6f646966696564426173654f6e4f6e557064617465645375627374617465220000022201010c12436c6f736553756273746174654572726f722201012201012307210200022201010c0e48616e646c654e6f74466f756e6422000001022201010c105375627374617465426f72726f776564220000022201010c115265616453756273746174654572726f722201012201012307210100022201010c0e48616e646c654e6f74466f756e64220000022201010c12577269746553756273746174654572726f722201012201012307210400022201010c0e48616e646c654e6f74466f756e6422000001022201010c1450726f6365737353756273746174654572726f7222000002022201010c114e6f57726974655065726d697373696f6e22000003022201010c115375627374617465446966664572726f72220000022201010c1643616c6c4672616d655363616e4b6579734572726f722201012201012307210200022201010c0e4e6f64654e6f7456697369626c6522000001022201010c1750726f6365737353756273746174654b65794572726f72220000022201010c1c43616c6c4672616d65447261696e5375627374617465734572726f722201012201012307210300022201010c0e4e6f64654e6f7456697369626c6522000001022201010c184e6f6e476c6f62616c5265664e6f74537570706f7274656422000002022201010c1750726f6365737353756273746174654b65794572726f72220000022201010c2143616c6c4672616d655363616e536f727465645375627374617465734572726f722201012201012307210300022201010c0e4e6f64654e6f7456697369626c6522000001022201010c154f776e65644e6f64654e6f74537570706f7274656422000002022201010c1750726f6365737353756273746174654b65794572726f72220000022201010c1943616c6c4672616d6553657453756273746174654572726f722201012201012307210300022201010c0e4e6f64654e6f7456697369626c6522000001022201010c0e53756273746174654c6f636b656422000002022201010c1750726f6365737353756273746174654b65794572726f72220000022201010c1c43616c6c4672616d6552656d6f766553756273746174654572726f722201012201012307210300022201010c0e4e6f64654e6f7456697369626c6522000001022201010c0e53756273746174654c6f636b656422000002022201010c1750726f6365737353756273746174654b65794572726f72220000022201010c114964416c6c6f636174696f6e4572726f722201012201012307210100022201010c074f75744f66494422000002220000220000022201010c0b53797374656d4572726f722201012201012307212f00022201010c0d4e6f426c75657072696e74496422000001022201010c104e6f5061636b6167654164647265737322000002022201010c17496e76616c69644163746f72537461746548616e646c6522000003022201010c15496e76616c69644163746f7252656648616e646c6522000004022201010c1d476c6f62616c697a696e675472616e7369656e74426c75657072696e7422000005022201010c19476c6f62616c41646472657373446f65734e6f74457869737422000006022201010c174e6f74416e416464726573735265736572766174696f6e22000007022201010c0b4e6f74416e4f626a65637422000008022201010c114e6f74414b657956616c756553746f726522000009022201010c1b4d6f64756c6573446f6e74486176654f757465724f626a656374732200000a022201010c174163746f724e6f64654964446f65734e6f7445786973742200000b022201010c174f757465724f626a656374446f65734e6f7445786973742200000c022201010c0f4e6f74414669656c6448616e646c652200000d022201010c144e6f74414669656c64577269746548616e646c652200000e022201010c0d526f6f744861734e6f547970652200000f022201010c1841646472657373426563683332456e636f64654572726f7222000010022201010c0e54797065436865636b4572726f7222000011022201010c114669656c64446f65734e6f74457869737422000012022201010c1b436f6c6c656374696f6e496e646578446f65734e6f74457869737422000013022201010c1c436f6c6c656374696f6e496e64657849734f6657726f6e675479706522000014022201010c134b657956616c7565456e7472794c6f636b656422000015022201010c0b4669656c644c6f636b656422000016022201010c184f626a6563744d6f64756c65446f65734e6f74457869737422000017022201010c174e6f74414b657956616c7565456e74727948616e646c6522000018022201010c1c4e6f74414b657956616c7565456e747279577269746548616e646c6522000019022201010c10496e76616c69644c6f636b466c6167732200001a022201010c0f43616e6e6f74476c6f62616c697a652200001b022201010c0d4d697373696e674d6f64756c652200001c022201010c1f496e76616c6964476c6f62616c416464726573735265736572766174696f6e2200001d022201010c1a496e76616c69644368696c644f626a6563744372656174696f6e2200001e022201010c11496e76616c69644d6f64756c65547970652200001f022201010c114372656174654f626a6563744572726f7222000020022201010c12496e76616c696447656e657269634172677322000021022201010c0e496e76616c69644665617475726522000022022201010c1641737365727441636365737352756c654661696c656422000023022201010c15426c75657072696e74446f65734e6f74457869737422000024022201010c184175746854656d706c617465446f65734e6f74457869737422000025022201010c16496e76616c6964476c6f62616c697a6541636365737322000026022201010c11496e76616c696444726f7041636365737322000027022201010c17436f7374696e674d6f64756c654e6f74456e61626c656422000028022201010c14417574684d6f64756c654e6f74456e61626c656422000029022201010c225472616e73616374696f6e52756e74696d654d6f64756c654e6f74456e61626c65642200002a022201010c1e466f72636557726974654576656e74466c6167734e6f74416c6c6f7765642200002b022201010c15426c75657072696e74547970654e6f74466f756e642200002c022201010c08426c734572726f722200002d022201010c0e496e70757444617461456d7074792200002e022201010c0b53797374656d50616e6963220000022201010c0e54797065436865636b4572726f722201012201012307210900022201010c1a496e76616c69644e756d6265724f6647656e6572696341726773220101220001200c020865787065637465640661637475616c01022201010c12496e76616c69644c6f63616c54797065496422000002022201010c1e496e76616c6964426c75657072696e74547970654964656e74696669657222000003022201010c16496e76616c6964436f6c6c656374696f6e496e64657822000004022201010c1c426c75657072696e745061796c6f6164446f65734e6f74457869737422000005022201010c1f426c75657072696e745061796c6f616456616c69646174696f6e4572726f7222000006022201010c234b657956616c756553746f72655061796c6f616456616c69646174696f6e4572726f7222000007022201010c16496e7374616e6365536368656d614e6f74466f756e6422000008022201010c0d4d697373696e67536368656d61220000022201010c0b4c6f63616c5479706549642201012201012307210200022201010c0957656c6c4b6e6f776e22000001022201010c10536368656d614c6f63616c496e646578220000022201010c0f57656c6c4b6e6f776e547970654964220000022201010c17426c75657072696e74547970654964656e746966696572220101220001200c030f7061636b6167655f616464726573730e626c75657072696e745f6e616d6509747970655f6e616d65022201010c0d426c75657072696e74496e666f220101220001200c050c626c75657072696e745f696411626c75657072696e745f76657273696f6e0e6f757465725f6f626a5f696e666f0866656174757265731567656e657269635f737562737469747574696f6e73022201010c0b426c75657072696e744964220101220001200c020f7061636b6167655f616464726573730e626c75657072696e745f6e616d65022201010c10426c75657072696e7456657273696f6e220101220001200c03056d616a6f72056d696e6f72057061746368022201010c0f4f757465724f626a656374496e666f2201012201012307210200022201010c04536f6d65220101220001200c010c6f757465725f6f626a65637401022201010c044e6f6e652200000222000022000002220000220000022201010c1347656e65726963537562737469747574696f6e2201012201012307210200022201010c054c6f63616c22000001022201010c0652656d6f7465220000022201010c0c53636f706564547970654964220000022201010c0a536368656d6148617368220000022201010c1a426c75657072696e745061796c6f61644964656e7469666965722201012201012307210600022201010c0846756e6374696f6e22000001022201010c054576656e7422000002022201010c054669656c6422000003022201010c0d4b657956616c7565456e74727922000004022201010c0a496e646578456e74727922000005022201010c10536f72746564496e646578456e747279220000022201010c0d496e7075744f724f75747075742201012201012307210200022201010c05496e70757422000001022201010c064f7574707574220000022201010c0a4b65794f7256616c75652201012201012307210200022201010c034b657922000001022201010c0556616c7565220000022201010c16426c75657072696e74506172746974696f6e547970652201012201012307210300022201010c124b657956616c7565436f6c6c656374696f6e22000001022201010c0f496e646578436f6c6c656374696f6e22000002022201010c15536f72746564496e646578436f6c6c656374696f6e220000022201010c1443616e6e6f74476c6f62616c697a654572726f722201012201012307210300022201010c0b4e6f74416e4f626a65637422000001022201010c11416c7265616479476c6f62616c697a656422000002022201010c12496e76616c6964426c75657072696e744964220000022201010c11496e76616c69644d6f64756c6554797065220101220001200c021265787065637465645f626c75657072696e741061637475616c5f626c75657072696e74022201010c114372656174654f626a6563744572726f722201012201012307210600022201010c11426c75657072696e744e6f74466f756e6422000001022201010c18496e76616c69644669656c64447565546f4665617475726522000002022201010c0c4d697373696e674669656c6422000003022201010c11496e76616c69644669656c64496e64657822000004022201010c15536368656d6156616c69646174696f6e4572726f7222000005022201010c14496e76616c696453756273746174655772697465220000022201010c1443616e6f6e6963616c426c75657072696e744964220101220001200c03076164647265737309626c75657072696e740776657273696f6e022201010c16496e76616c6964476c6f62616c697a65416363657373220101220001200c030f7061636b6167655f616464726573730e626c75657072696e745f6e616d650d6163746f725f7061636b616765022201010c064f7074696f6e2201012201012307210200022201010c044e6f6e6522000001022201010c04536f6d65220000022201010c11496e76616c696444726f70416363657373220101220001200c04076e6f64655f69640f7061636b6167655f616464726573730e626c75657072696e745f6e616d650d6163746f725f7061636b616765022201010c1153797374656d4d6f64756c654572726f722201012201012307210400022201010c09417574684572726f7222000001022201010c0c436f7374696e674572726f7222000002022201010c165472616e73616374696f6e4c696d6974734572726f7222000003022201010c0a4576656e744572726f72220000022201010c09417574684572726f722201012201012307210600022201010c0a4e6f46756e6374696f6e22000001022201010c0f4e6f4d6574686f644d617070696e6722000002022201010c0f5669736962696c6974794572726f7222000003022201010c0c556e617574686f72697a656422000004022201010c1a496e6e6572426c75657072696e74446f65734e6f74457869737422000005022201010c19496e76616c69644f757465724f626a6563744d617070696e67220000022201010c0c466e4964656e746966696572220101220001200c020c626c75657072696e745f6964056964656e74022201010c0c556e617574686f72697a6564220101220001200c02136661696c65645f6163636573735f72756c65730d666e5f6964656e746966696572022201010c114661696c656441636365737352756c65732201012201012307210200022201010c08526f6c654c69737422000001022201010c0a41636365737352756c65220000022200002200000222000022000002220000220000022201010c0c436f7374696e674572726f722201012201012307210100022201010c0f466565526573657276654572726f72220000022201010c0f466565526573657276654572726f722201012201012307210500022201010c13496e73756666696369656e7442616c616e6365220101220001200c020872657175697265640972656d61696e696e6701022201010c084f766572666c6f7722000002022201010c0d4c696d69744578636565646564220101220001200c03056c696d697409636f6d6d6974746564036e657703022201010c134c6f616e52657061796d656e744661696c6564220101220001200c01087872645f6f77656404022201010c0541626f7274220000022201010c0b41626f7274526561736f6e2201012201012307210100022201010c2a436f6e6669677572656441626f72745472696767657265644f6e4665654c6f616e52657061796d656e74220000022201010c165472616e73616374696f6e4c696d6974734572726f722201012201012307210b00022201010c1a4d617853756273746174654b657953697a65457863656564656422000001022201010c174d6178537562737461746553697a65457863656564656422000002022201010c1c4d6178496e766f6b655061796c6f616453697a65457863656564656422000003022201010c184d617843616c6c44657074684c696d69745265616368656422000004022201010c19547261636b537562737461746553697a654578636565646564220101220001200c020661637475616c036d617805022201010c1848656170537562737461746553697a654578636565646564220101220001200c020661637475616c036d617806022201010c0f4c6f6753697a65546f6f4c61726765220101220001200c020661637475616c036d617807022201010c114576656e7453697a65546f6f4c61726765220101220001200c020661637475616c036d617808022201010c1850616e69634d65737361676553697a65546f6f4c61726765220101220001200c020661637475616c036d617809022201010c0b546f6f4d616e794c6f67732200000a022201010c0d546f6f4d616e794576656e7473220000022201010c0a4576656e744572726f722201012201012307210400022201010c13536368656d614e6f74466f756e644572726f72220101220001200c0209626c75657072696e740a6576656e745f6e616d6501022201010c134576656e74536368656d614e6f744d6174636822000002022201010c134e6f4173736f6369617465645061636b61676522000003022201010c0c496e76616c69644163746f72220000022201010c1353797374656d557073747265616d4572726f722201012201012307210800022201010c1c53797374656d46756e6374696f6e43616c6c4e6f74416c6c6f77656422000001022201010c0a466e4e6f74466f756e6422000002022201010c1052656365697665724e6f744d6174636822000003022201010c0c486f6f6b4e6f74466f756e6422000004022201010c10496e7075744465636f64654572726f7222000005022201010c13496e707574536368656d614e6f744d6174636822000006022201010c114f75747075744465636f64654572726f7222000007022201010c144f7574707574536368656d614e6f744d61746368220000022201010c0d426c75657072696e74486f6f6b2201012201012307210300022201010c0c4f6e5669727475616c697a6522000001022201010c064f6e4d6f766522000002022201010c064f6e44726f70220000022201010c07566d4572726f722201012201012307210300022201010c064e617469766522000001022201010c045761736d22000002022201010c105363727970746f566d56657273696f6e220000022201010c124e617469766552756e74696d654572726f722201012201012307210200022201010c0d496e76616c6964436f6465496422000001022201010c0454726170220101220001200c030b6578706f72745f6e616d6505696e707574056572726f72022201010c105761736d52756e74696d654572726f722201012201012307211b00022201010c0d556e6b6e6f776e4578706f727422000001022201010c114d656d6f72794163636573734572726f7222000002022201010c12496e76616c69645761736d506f696e74657222000003022201010c0e457865637574696f6e4572726f7222000004022201010c0e4e6f74496d706c656d656e74656422000005022201010c0e4275666665724e6f74466f756e6422000006022201010c0e496e76616c69644164647265737322000007022201010c0d496e76616c6964537472696e6722000008022201010c0d496e76616c69644e6f6465496422000009022201010c1f496e76616c6964476c6f62616c416464726573735265736572766174696f6e2200000a022201010c14496e76616c69645265666572656e6365547970652200000b022201010c17496e76616c696441747461636865644d6f64756c6549642200000c022201010c13496e76616c69644f626a6563745374617465732200000d022201010c11496e76616c696441636365737352756c652200000e022201010c0e496e76616c69644d6f64756c65732200000f022201010c13496e76616c696454656d706c6174654172677322000010022201010c1a496e76616c69644b657956616c756553746f7265536368656d6122000011022201010c10496e76616c69644c6f636b466c61677322000012022201010c0f496e76616c69644c6f674c6576656c22000013022201010c0f466565526573657276654572726f7222000014022201010c11496e76616c69644576656e74466c61677322000015022201010c15496e76616c69645061636b6167654164647265737322000016022201010c0e546f6f4d616e794275666665727322000017022201010c13496e76616c6964426c735075626c69634b657922000018022201010c13496e76616c6964426c735369676e617475726522000019022201010c1c496e76616c6964426c735075626c69634b65794f724d6573736167652200001a022201010c0e496e70757444617461456d707479220000022201010c155363727970746f566d56657273696f6e4572726f722201012201012307210100022201010c0c46726f6d496e744572726f72220000022201010c104170706c69636174696f6e4572726f722201012201012307211700022201010c124578706f7274446f65734e6f74457869737422000001022201010c10496e7075744465636f64654572726f7222000002022201010c0c50616e69634d65737361676522000003022201010c13526f6c6541737369676e6d656e744572726f7222000004022201010c0d4d657461646174614572726f7222000005022201010c15436f6d706f6e656e74526f79616c74794572726f7222000006022201010c195472616e73616374696f6e50726f636573736f724572726f7222000007022201010c0c5061636b6167654572726f7222000008022201010c15436f6e73656e7375734d616e616765724572726f7222000009022201010c0e56616c696461746f724572726f722200000a022201010c1c46756e6769626c655265736f757263654d616e616765724572726f722200000b022201010c1f4e6f6e46756e6769626c655265736f757263654d616e616765724572726f722200000c022201010c0b4275636b65744572726f722200000d022201010c0a50726f6f664572726f722200000e022201010c154e6f6e46756e6769626c655661756c744572726f722200000f022201010c0a5661756c744572726f7222000010022201010c0c576f726b746f704572726f7222000011022201010c0d417574685a6f6e654572726f7222000012022201010c0c4163636f756e744572726f7222000013022201010c15416363657373436f6e74726f6c6c65724572726f7222000014022201010c144f6e655265736f75726365506f6f6c4572726f7222000015022201010c1454776f5265736f75726365506f6f6c4572726f7222000016022201010c164d756c74695265736f75726365506f6f6c4572726f72220000022201010c13526f6c6541737369676e6d656e744572726f722201012201012307210800022201010c10557365645265736572766564526f6c6522000001022201010c11557365645265736572766564537061636522000002022201010c1645786365656465644d6178526f6c654e616d654c656e220101220001200c02056c696d69740661637475616c03022201010c1a45786365656465644d617841636365737352756c65446570746822000004022201010c1a45786365656465644d617841636365737352756c654e6f64657322000005022201010c0b496e76616c69644e616d6522000006022201010c1045786365656465644d6178526f6c657322000007022201010c1a43616e6e6f74536574526f6c6549664e6f744174746163686564220000022201010c10496e76616c69644e616d654572726f722201012201012307210200022201010c0b456d707479537472696e6722000001022201010c0b496e76616c696443686172220101220001200c03046e616d650e76696f6c6174696e675f6368617205696e646578022201010c0d4d657461646174614572726f722201012201012307210400022201010c194b6579537472696e67457863656564734d61784c656e677468220101220001200c02036d61780661637475616c01022201010c1956616c756553626f72457863656564734d61784c656e677468220101220001200c02036d61780661637475616c02022201010c1056616c75654465636f64654572726f7222000003022201010c174d6574616461746156616c69646174696f6e4572726f72220000022201010c174d6574616461746156616c69646174696f6e4572726f722201012201012307210200022201010c0a496e76616c696455524c22000001022201010c0d496e76616c69644f726967696e220000022201010c15436f6d706f6e656e74526f79616c74794572726f722201012201012307210300022201010c21526f79616c7479416d6f756e744973477265617465725468616e416c6c6f776564220101220001200c02036d61780661637475616c01022201010c21556e6578706563746564446563696d616c436f6d7075746174696f6e4572726f7222000002022201010c17526f79616c7479416d6f756e7449734e65676174697665220000022201010c195472616e73616374696f6e50726f636573736f724572726f722201012201012307210d00022201010c0e4275636b65744e6f74466f756e6422000001022201010c0d50726f6f664e6f74466f756e6422000002022201010c1a416464726573735265736572766174696f6e4e6f74466f756e6422000003022201010c0f416464726573734e6f74466f756e6422000004022201010c0c426c6f624e6f74466f756e6422000005022201010c0f496e76616c696443616c6c4461746122000006022201010c14496e76616c69645061636b616765536368656d6122000007022201010c114e6f745061636b6167654164647265737322000008022201010c104e6f74476c6f62616c4164647265737322000009022201010c0f417574685a6f6e654973456d7074792200000a022201010c1b496e766f636174696f6e4f75747075744465636f64654572726f722200000b022201010c0f41726773456e636f64654572726f722200000c022201010c1a546f74616c426c6f6253697a654c696d69744578636565646564220000022201010c0448617368220000022201010c0b456e636f64654572726f722201012201012307210500022201010c104d61784465707468457863656564656422000001022201010c0c53697a65546f6f4c61726765220101220001200c020661637475616c0b6d61785f616c6c6f77656402022201010c204d69736d61746368696e674172726179456c656d656e7456616c75654b696e64220101220001200c0212656c656d656e745f76616c75655f6b696e641161637475616c5f76616c75655f6b696e6403022201010c1a4d69736d61746368696e674d61704b657956616c75654b696e64220101220001200c020e6b65795f76616c75655f6b696e641161637475616c5f76616c75655f6b696e6404022201010c1c4d69736d61746368696e674d617056616c756556616c75654b696e64220101220001200c021076616c75655f76616c75655f6b696e641161637475616c5f76616c75655f6b696e64022201010c0c5061636b6167654572726f722201012201012307212900022201010c0b496e76616c69645761736d22000001022201010c16496e76616c6964426c75657072696e74536368656d6122000002022201010c16546f6f4d616e795375627374617465536368656d617322000003022201010c1346656174757265446f65734e6f74457869737422000004022201010c15496e76616c69645472616e7369656e744669656c6422000005022201010c1e53797374656d496e737472756374696f6e734e6f74537570706f7274656422000006022201010c1a4661696c6564546f5265736f6c76654c6f63616c536368656d61220101220001200c010d6c6f63616c5f747970655f696407022201010c114576656e744e616d654d69736d61746368220101220001200c020865787065637465640661637475616c08022201010c10547970654e616d654d69736d61746368220101220001200c020865787065637465640661637475616c09022201010c12496e76616c69644576656e74536368656d612200000a022201010c15496e76616c696453797374656d46756e6374696f6e2200000b022201010c11496e76616c696454797065506172656e742200000c022201010c0b496e76616c69644e616d652200000d022201010c154d697373696e674f75746572426c75657072696e742200000e022201010c0f5761736d556e737570706f727465642200000f022201010c12496e76616c69644c6f63616c54797065496422000010022201010c10496e76616c696447656e65726963496422000011022201010c1c4576656e7447656e65726963547970654e6f74537570706f7274656422000012022201010c244f75746572426c75657072696e7443616e744265416e496e6e6572426c75657072696e74220101220001200c0205696e6e65720f76696f6c6174696e675f6f7574657213022201010c13526f6c6541737369676e6d656e744572726f7222000014022201010c10496e76616c696441757468536574757022000015022201010c17446566696e696e675265736572766564526f6c654b657922000016022201010c1045786365656465644d6178526f6c6573220101220001200c02056c696d69740661637475616c17022201010c1645786365656465644d6178526f6c654e616d654c656e220101220001200c02056c696d69740661637475616c18022201010c1b45786365656465644d6178426c75657072696e744e616d654c656e220101220001200c02056c696d69740661637475616c19022201010c1745786365656465644d61784576656e744e616d654c656e220101220001200c02056c696d69740661637475616c1a022201010c1645786365656465644d6178547970654e616d654c656e220101220001200c02056c696d69740661637475616c1b022201010c1a45786365656465644d617846756e6374696f6e4e616d654c656e220101220001200c02056c696d69740661637475616c1c022201010c1945786365656465644d6178466561747572654e616d654c656e220101220001200c02056c696d69740661637475616c1d022201010c0b4d697373696e67526f6c652200001e022201010c1c556e65787065637465644e756d6265724f664d6574686f6441757468220101220001200c0309626c75657072696e740865787065637465640661637475616c1f022201010c174d697373696e674d6574686f645065726d697373696f6e220101220001200c0209626c75657072696e74056964656e7420022201010c1e556e65787065637465644e756d6265724f6646756e6374696f6e41757468220101220001200c0309626c75657072696e740865787065637465640661637475616c21022201010c194d697373696e6746756e6374696f6e5065726d697373696f6e220101220001200c0209626c75657072696e74056964656e7422022201010c23556e65787065637465644e756d6265724f6646756e6374696f6e526f79616c74696573220101220001200c0309626c75657072696e740865787065637465640661637475616c23022201010c164d697373696e6746756e6374696f6e526f79616c7479220101220001200c0209626c75657072696e74056964656e7424022201010c21526f79616c7479416d6f756e744973477265617465725468616e416c6c6f776564220101220001200c02036d61780661637475616c25022201010c12496e76616c69644d657461646174614b657922000026022201010c13526f79616c746965734e6f74456e61626c656422000027022201010c17526f79616c7479416d6f756e7449734e6567617469766522000028022201010c1b5265736572766564526f6c654b657949734e6f74446566696e6564220000022201010c0c507265706172654572726f722201012201012307211800022201010c14446573657269616c697a6174696f6e4572726f7222000001022201010c0f56616c69646174696f6e4572726f7222000002022201010c1253657269616c697a6174696f6e4572726f7222000003022201010c17537461727446756e6374696f6e4e6f74416c6c6f77656422000004022201010c0d496e76616c6964496d706f727422000005022201010c0d496e76616c69644d656d6f727922000006022201010c0c496e76616c69645461626c6522000007022201010c11496e76616c69644578706f72744e616d6522000008022201010c17546f6f4d616e7954617267657473496e42725461626c6522000009022201010c10546f6f4d616e7946756e6374696f6e732200000a022201010c15546f6f4d616e7946756e6374696f6e506172616d732200000b022201010c15546f6f4d616e7946756e6374696f6e4c6f63616c73220101220001200c02036d61780661637475616c0c022201010c0e546f6f4d616e79476c6f62616c73220101220001200c02036d61780763757272656e740d022201010c0f4e6f4578706f727453656374696f6e2200000e022201010c0d4d697373696e674578706f7274220101220001200c010b6578706f72745f6e616d650f022201010c144e6f5363727970746f416c6c6f634578706f727422000010022201010c134e6f5363727970746f467265654578706f727422000011022201010c1d52656a65637465644279496e737472756374696f6e4d65746572696e67220101220001200c0106726561736f6e12022201010c1752656a65637465644279537461636b4d65746572696e67220101220001200c0106726561736f6e13022201010c114e6f74496e7374616e7469617461626c65220101220001200c0106726561736f6e14022201010c0d4e6f74436f6d70696c61626c6522000015022201010c0f4d6f64756c65496e666f4572726f7222000016022201010c0f5761736d5061727365724572726f7222000017022201010c084f766572666c6f77220000022201010c0d496e76616c6964496d706f72742201012201012307210300022201010c10496d706f72744e6f74416c6c6f77656422000001022201010c1750726f746f636f6c56657273696f6e4d69736d61746368220101220001200c03046e616d650f63757272656e745f76657273696f6e1065787065637465645f76657273696f6e02022201010c13496e76616c696446756e6374696f6e54797065220000022201010c0d496e76616c69644d656d6f72792201012201012307210500022201010c144d697373696e674d656d6f727953656374696f6e22000001022201010c124e6f4d656d6f7279446566696e6974696f6e22000002022201010c17546f6f4d616e794d656d6f7279446566696e6974696f6e22000003022201010c174d656d6f727953697a654c696d6974457863656564656422000004022201010c114d656d6f72794e6f744578706f72746564220000022201010c0c496e76616c69645461626c652201012201012307210200022201010c104d6f72655468616e4f6e655461626c6522000001022201010c1d496e697469616c5461626c6553697a654c696d69744578636565646564220000022201010c15536368656d6156616c69646174696f6e4572726f722201012201012307211600022201010c164d657461646174614c656e6774684d69736d6174636822000001022201010c1956616c69646174696f6e734c656e6774684d69736d6174636822000002022201010c14547970654b696e645475706c65546f6f4c6f6e67220101220001200c01086d61785f73697a6503022201010c1a547970654b696e64456e756d56617269616e74546f6f4c6f6e67220101220001200c01086d61785f73697a6504022201010c1f547970654b696e64496e76616c6964536368656d614c6f63616c496e64657822000005022201010c1d547970654b696e64496e76616c696457656c6c4b6e6f776e496e64657822000006022201010c29547970654d65746164617461436f6e7461696e6564556e65787065637465644368696c644e616d657322000007022201010c28547970654d65746164617461436f6e7461696e65644475706c69636174654669656c644e616d657322000008022201010c30547970654d657461646174614669656c644e616d65436f756e74446f65734e6f744d617463684669656c64436f756e7422000009022201010c2b547970654d65746164617461436f6e7461696e6564556e6578706563746564456e756d56617269616e74732200000a022201010c2a547970654d65746164617461436f6e7461696e6564556e65787065637465644e616d65644669656c64732200000b022201010c2a547970654d65746164617461436f6e7461696e656457726f6e674e756d6265724f6656617269616e74732200000c022201010c1e547970654d65746164617461456e756d4e616d65497352657175697265642200000d022201010c25547970654d65746164617461456e756d56617269616e744e616d65497352657175697265642200000e022201010c2d547970654d65746164617461466f72456e756d49734e6f74456e756d56617269616e744368696c644e616d65732200000f022201010c2b547970654d657461646174614861734d69736d61746368696e67456e756d4469736372696d696e61746f7222000010022201010c2e547970654d65746164617461436f6e7461696e65644475706c6963617465456e756d56617269616e744e616d657322000011022201010c10496e76616c69644964656e744e616d65220101220001200c01076d65737361676512022201010c165479706556616c69646174696f6e4d69736d6174636822000013022201010c265479706556616c69646174696f6e4e756d6572696356616c69646174696f6e496e76616c696422000014022201010c255479706556616c69646174696f6e4c656e67746856616c69646174696f6e496e76616c696422000015022201010c225479706556616c69646174696f6e4174746163686564546f437573746f6d54797065220000022201010c064f7074696f6e2201012201012307210200022201010c044e6f6e6522000001022201010c04536f6d65220000022201010c15436f6e73656e7375734d616e616765724572726f722201012201012307210a00022201010c12496e76616c6964526f756e64557064617465220101220001200c020466726f6d02746f01022201010c1e496e76616c696450726f706f73657254696d657374616d70557064617465220101220001200c020b66726f6d5f6d696c6c697309746f5f6d696c6c697302022201010c15496e636f6e73697374656e74476170526f756e6473220101220001200c020a6761705f726f756e64731170726f677265737365645f726f756e647303022201010c15496e76616c696456616c696461746f72496e646578220101220001200c0205696e64657805636f756e7404022201010c0e416c72656164795374617274656422000005022201010c064e6f7458726422000006022201010c21556e6578706563746564446563696d616c436f6d7075746174696f6e4572726f7222000007022201010c1145706f63684d6174684f766572666c6f7722000008022201010c14496e76616c6964436f6e73656e73757354696d6522000009022201010c16457863656564656456616c696461746f72436f756e74220101220001200c020763757272656e74036d6178022201010c05526f756e64220000022201010c0e56616c696461746f724572726f722201012201012307210900022201010c14496e76616c6964436c61696d5265736f7572636522000001022201010c1a496e76616c6964476574526564656d7074696f6e416d6f756e7422000002022201010c21556e6578706563746564446563696d616c436f6d7075746174696f6e4572726f7222000003022201010c1c45706f6368556e6c6f636b4861734e6f744f6363757272656459657422000004022201010c2750656e64696e674f776e65725374616b655769746864726177616c4c696d69745265616368656422000005022201010c19496e76616c696456616c696461746f72466565466163746f7222000006022201010c2556616c696461746f7249734e6f74416363657074696e6744656c6567617465645374616b6522000007022201010c20496e76616c696450726f746f636f6c56657273696f6e4e616d654c656e677468220101220001200c020865787065637465640661637475616c08022201010c1145706f63684d6174684f766572666c6f77220000022201010c1c46756e6769626c655265736f757263654d616e616765724572726f722201012201012307210700022201010c0d496e76616c6964416d6f756e7422000001022201010c154d61784d696e74416d6f756e74457863656564656422000002022201010c13496e76616c696444697669736962696c69747922000003022201010c1244726f704e6f6e456d7074794275636b657422000004022201010c0b4e6f744d696e7461626c6522000005022201010c0b4e6f744275726e61626c6522000006022201010c21556e6578706563746564446563696d616c436f6d7075746174696f6e4572726f72220000022201010c1f4e6f6e46756e6769626c655265736f757263654d616e616765724572726f722201012201012307210b00022201010c184e6f6e46756e6769626c65416c726561647945786973747322000001022201010c134e6f6e46756e6769626c654e6f74466f756e6422000002022201010c17556e6b6e6f776e4d757461626c654669656c644e616d6522000003022201010c1d4e6f6e46756e6769626c65496454797065446f65734e6f744d6174636822000004022201010c18496e76616c69644e6f6e46756e6769626c6549645479706522000005022201010c18496e76616c69644e6f6e46756e6769626c65536368656d6122000006022201010c254e6f6e46756e6769626c654c6f63616c496450726f7669646564466f72525549445479706522000007022201010c1244726f704e6f6e456d7074794275636b657422000008022201010c0b4e6f744d696e7461626c6522000009022201010c0b4e6f744275726e61626c652200000a022201010c21556e6578706563746564446563696d616c436f6d7075746174696f6e4572726f72220000022201010c114e6f6e46756e6769626c654964547970652201012201012307210400022201010c06537472696e6722000001022201010c07496e746567657222000002022201010c05427974657322000003022201010c0452554944220000022201010c18496e76616c69644e6f6e46756e6769626c65536368656d612201012201012307210500022201010c15536368656d6156616c69646174696f6e4572726f7222000001022201010c12496e76616c69644c6f63616c54797065496422000002022201010c094e6f74415475706c6522000003022201010c114d697373696e674669656c644e616d657322000004022201010c184d757461626c654669656c64446f65734e6f744578697374220000022201010c0b4275636b65744572726f722201012201012307210500022201010c0d5265736f757263654572726f7222000001022201010c0a50726f6f664572726f7222000002022201010c064c6f636b656422000003022201010c0d496e76616c6964416d6f756e7422000004022201010c0f446563696d616c4f766572666c6f77220000022201010c0d5265736f757263654572726f722201012201012307210400022201010c13496e73756666696369656e7442616c616e6365220101220001200c02097265717565737465640661637475616c01022201010c11496e76616c696454616b65416d6f756e7422000002022201010c194d697373696e674e6f6e46756e6769626c654c6f63616c496422000003022201010c0f446563696d616c4f766572666c6f77220000022201010c0a50726f6f664572726f722201012201012307210100022201010c14456d70747950726f6f664e6f74416c6c6f776564220000022201010c154e6f6e46756e6769626c655661756c744572726f722201012201012307210300022201010c094d697373696e67496422000001022201010c0f4e6f74456e6f756768416d6f756e7422000002022201010c0f446563696d616c4f766572666c6f77220000022201010c0a5661756c744572726f722201012201012307210900022201010c0d5265736f757263654572726f7222000001022201010c0a50726f6f664572726f7222000002022201010c0d496e76616c6964416d6f756e7422000003022201010c0c4e6f74467265657a61626c6522000004022201010c0d4e6f74526563616c6c61626c6522000005022201010c0d5661756c74497346726f7a656e22000006022201010c144c6f636b4665654e6f745261646978546f6b656e22000007022201010c1a4c6f636b466565496e73756666696369656e7442616c616e6365220101220001200c02097265717565737465640661637475616c08022201010c0f446563696d616c4f766572666c6f77220000022201010c0c576f726b746f704572726f722201012201012307210200022201010c0f417373657274696f6e4661696c656422000001022201010c13496e73756666696369656e7442616c616e6365220000022201010c0d417574685a6f6e654572726f722201012201012307210100022201010c11436f6d706f736550726f6f664572726f72220000022201010c11436f6d706f736550726f6f664572726f722201012201012307210400022201010c204e6f6e46756e6769626c654f7065726174696f6e4e6f74537570706f7274656422000001022201010c16496e73756666696369656e744261736550726f6f667322000002022201010c0d496e76616c6964416d6f756e7422000003022201010c21556e6578706563746564446563696d616c436f6d7075746174696f6e4572726f72220000022201010c0c4163636f756e744572726f722201012201012307210400022201010c115661756c74446f65734e6f744578697374220101220001200c01107265736f757263655f6164647265737301022201010c134465706f7369744973446973616c6c6f776564220101220001200c01107265736f757263655f6164647265737302022201010c1d4e6f74416c6c4275636b657473436f756c6442654465706f736974656422000003022201010c184e6f74416e417574686f72697a65644465706f7369746f72220101220001200c01096465706f7369746f72022201010c15416363657373436f6e74726f6c6c65724572726f722201012201012307210a00022201010c244f7065726174696f6e5265717569726573556e6c6f636b65645072696d617279526f6c6522000001022201010c0c54696d654f766572666c6f7722000002022201010c205265636f76657279416c7265616479457869737473466f7250726f706f736572220101220001200c010870726f706f73657203022201010c1b4e6f5265636f76657279457869737473466f7250726f706f736572220101220001200c010870726f706f73657204022201010c2c42616467655769746864726177417474656d7074416c7265616479457869737473466f7250726f706f736572220101220001200c010870726f706f73657205022201010c274e6f42616467655769746864726177417474656d7074457869737473466f7250726f706f736572220101220001200c010870726f706f73657206022201010c164e6f54696d65645265636f766572696573466f756e6422000007022201010c1f54696d65645265636f7665727944656c61794861734e6f74456c617073656422000008022201010c185265636f7665727950726f706f73616c4d69736d61746368220101220001200c0208657870656374656405666f756e6409022201010c0d4e6f5872644665655661756c74220000022201010c0850726f706f7365722201012201012307210200022201010c075072696d61727922000001022201010c085265636f76657279220000022201010c105265636f7665727950726f706f73616c220101220001200c020872756c655f7365741f74696d65645f7265636f766572795f64656c61795f696e5f6d696e75746573022201010c0752756c65536574220101220001200c030c7072696d6172795f726f6c650d7265636f766572795f726f6c6511636f6e6669726d6174696f6e5f726f6c65022201010c064f7074696f6e2201012201012307210200022201010c044e6f6e6522000001022201010c04536f6d65220000022201010c054572726f722201012201012307210900022201010c224e6f6e46756e6769626c655265736f75726365734172654e6f744163636570746564220101220001200c01107265736f757263655f6164647265737301022201010c244e6f6e5a65726f506f6f6c556e6974537570706c794275745a65726f526573657276657322000002022201010c17496e76616c6964506f6f6c556e69745265736f75726365220101220001200c020865787065637465640661637475616c03022201010c1e436f6e747269627574696f6e4f66456d7074794275636b65744572726f7222000004022201010c14446563696d616c4f766572666c6f774572726f7222000005022201010c1a496e76616c6964476574526564656d7074696f6e416d6f756e7422000006022201010c135a65726f506f6f6c556e6974734d696e74656422000007022201010c1252656465656d65645a65726f546f6b656e7322000008022201010c1b5265736f75726365446f65734e6f7442656c6f6e67546f506f6f6c220101220001200c01107265736f757263655f61646472657373022201010c054572726f722201012201012307210a00022201010c224e6f6e46756e6769626c655265736f75726365734172654e6f744163636570746564220101220001200c01107265736f757263655f6164647265737301022201010c244e6f6e5a65726f506f6f6c556e6974537570706c794275745a65726f526573657276657322000002022201010c17496e76616c6964506f6f6c556e69745265736f75726365220101220001200c020865787065637465640661637475616c03022201010c1b5265736f75726365446f65734e6f7442656c6f6e67546f506f6f6c220101220001200c01107265736f757263655f6164647265737304022201010c1c506f6f6c4372656174696f6e5769746853616d655265736f7572636522000005022201010c1e436f6e747269627574696f6e4f66456d7074794275636b65744572726f7222000006022201010c14446563696d616c4f766572666c6f774572726f7222000007022201010c1a496e76616c6964476574526564656d7074696f6e416d6f756e7422000008022201010c135a65726f506f6f6c556e6974734d696e74656422000009022201010c254c6172676572436f6e747269627574696f6e5265717569726564546f4d656574526174696f220000022201010c054572726f722201012201012307210d00022201010c224e6f6e46756e6769626c655265736f75726365734172654e6f744163636570746564220101220001200c01107265736f757263655f6164647265737301022201010c244e6f6e5a65726f506f6f6c556e6974537570706c794275745a65726f526573657276657322000002022201010c17496e76616c6964506f6f6c556e69745265736f75726365220101220001200c020865787065637465640661637475616c03022201010c1b5265736f75726365446f65734e6f7442656c6f6e67546f506f6f6c220101220001200c01107265736f757263655f6164647265737304022201010c154d697373696e674f72456d7074794275636b657473220101220001200c01127265736f757263655f61646472657373657305022201010c1c506f6f6c4372656174696f6e5769746853616d655265736f7572636522000006022201010c1e436f6e747269627574696f6e4f66456d7074794275636b65744572726f7222000007022201010c2543616e74437265617465506f6f6c576974684c6573735468616e4f6e655265736f7572636522000008022201010c14446563696d616c4f766572666c6f774572726f7222000009022201010c1a496e76616c6964476574526564656d7074696f6e416d6f756e742200000a022201010c0e4e6f4d696e696d756d526174696f2200000b022201010c135a65726f506f6f6c556e6974734d696e7465642200000c022201010c254c6172676572436f6e747269627574696f6e5265717569726564546f4d656574526174696f2200000222000022000002220000220000022201010c134576656e74547970654964656e746966696572220000022201010c07456d69747465722201012201012307210200022201010c0846756e6374696f6e22000001022201010c064d6574686f642200000222000022000002220000220000022201010c054c6576656c2201012201012307210500022201010c054572726f7222000001022201010c045761726e22000002022201010c04496e666f22000003022201010c05446562756722000004022201010c055472616365220000022201010c0f53797374656d537472756374757265220101220001200c021a73756273746174655f73797374656d5f73747275637475726573176576656e745f73797374656d5f73747275637475726573022200002200000222000022000002220000220000022201010c17537562737461746553797374656d5374727563747572652201012201012307210700022201010c0b53797374656d4669656c6422000001022201010c0c53797374656d536368656d6122000002022201010c124b657956616c756553746f7265456e74727922000003022201010c0b4f626a6563744669656c6422000004022201010c1c4f626a6563744b657956616c7565506172746974696f6e456e74727922000005022201010c194f626a656374496e646578506172746974696f6e456e74727922000006022201010c1f4f626a656374536f72746564496e646578506172746974696f6e456e747279220000022201010c1453797374656d4669656c64537472756374757265220101220001200c010a6669656c645f6b696e64022201010c0f53797374656d4669656c644b696e642201012201012307210400022201010c0854797065496e666f22000001022201010c06566d426f6f7422000002022201010c0a53797374656d426f6f7422000003022201010c0a4b65726e656c426f6f74220000022201010c1b4b657956616c756553746f7265456e747279537472756374757265220101220001200c02106b65795f66756c6c5f747970655f69641276616c75655f66756c6c5f747970655f6964022201010c1146756c6c7953636f706564547970654964220000022201010c0e4669656c64537472756374757265220101220001200c010c76616c75655f736368656d61022201010c1b4f626a6563745375627374617465547970655265666572656e63652201012201012307210200022201010c075061636b61676522000001022201010c0e4f626a656374496e7374616e6365220000022201010c145061636b616765547970655265666572656e6365220101220001200c010c66756c6c5f747970655f6964022201010c1146756c6c7953636f706564547970654964220000022201010c1b4f626a656374496e7374616e6365547970655265666572656e6365220101220001200c0210696e7374616e63655f747970655f6964157265736f6c7665645f66756c6c5f747970655f6964022201010c1f4b657956616c7565506172746974696f6e456e747279537472756374757265220101220001200c020a6b65795f736368656d610c76616c75655f736368656d61022201010c1c496e646578506172746974696f6e456e747279537472756374757265220101220001200c020a6b65795f736368656d610c76616c75655f736368656d61022201010c22536f72746564496e646578506172746974696f6e456e747279537472756374757265220101220001200c020a6b65795f736368656d610c76616c75655f736368656d6102220000220000022201010c144576656e7453797374656d537472756374757265220101220001200c01167061636b6167655f747970655f7265666572656e6365022201010c064f7074696f6e2201012201012307210200022201010c044e6f6e6522000001022201010c04536f6d65220000022201010c195472616e73616374696f6e457865637574696f6e5472616365220101220001200c0310657865637574696f6e5f747261636573107265736f757263655f6368616e676573096665655f6c6f636b7302220000220000022201010c0e457865637574696f6e5472616365220101220001200c08066f726967696e116b65726e656c5f63616c6c5f64657074681363757272656e745f6672616d655f6163746f721363757272656e745f6672616d655f646570746811696e737472756374696f6e5f696e64657805696e707574066f7574707574086368696c6472656e022201010c0b54726163654f726967696e2201012201012307210400022201010c0f5363727970746f46756e6374696f6e22000001022201010c0d5363727970746f4d6574686f6422000002022201010c0a4372656174654e6f646522000003022201010c0844726f704e6f6465220000022201010c174170706c69636174696f6e466e4964656e746966696572220101220001200c020c626c75657072696e745f6964056964656e74022201010c0a54726163654163746f722201012201012307210200022201010c064d6574686f6422000001022201010c094e6f6e4d6574686f64220000022201010c0f5265736f7572636553756d6d617279220101220001200c02076275636b6574730670726f6f667302220000220000022201010c0e4275636b6574536e617073686f742201012201012307210200022201010c0846756e6769626c65220101220001200c02107265736f757263655f61646472657373066c697175696401022201010c0b4e6f6e46756e6769626c65220101220001200c02107265736f757263655f61646472657373066c697175696402220000220000022201010c0d50726f6f66536e617073686f742201012201012307210200022201010c0846756e6769626c65220101220001200c02107265736f757263655f616464726573730c746f74616c5f6c6f636b656401022201010c0b4e6f6e46756e6769626c65220101220001200c02107265736f757263655f616464726573730c746f74616c5f6c6f636b65640222000022000002220000220000022201010c0e5265736f757263654368616e6765220101220001200c04076e6f64655f6964087661756c745f6964107265736f757263655f6164647265737306616d6f756e74022201010c084665654c6f636b73220101220001200c02046c6f636b0f636f6e74696e67656e745f6c6f636b022201010c0c52656a656374526573756c74220101220001200c0106726561736f6e022201010c0f52656a656374696f6e526561736f6e2201012201012307210700022201010c1b5472616e73616374696f6e45706f63684e6f7459657456616c6964220101220001200c020a76616c69645f66726f6d0d63757272656e745f65706f636801022201010c1d5472616e73616374696f6e45706f63684e6f4c6f6e67657256616c6964220101220001200c020b76616c69645f756e74696c0d63757272656e745f65706f636802022201010c1d496e74656e744861736850726576696f75736c79436f6d6d697474656422000003022201010c1d496e74656e744861736850726576696f75736c7943616e63656c6c656422000004022201010c10426f6f746c6f6164696e674572726f7222000005022201010c254572726f724265666f72654c6f616e416e644465666572726564436f73747352657061696422000006022201010c1a537563636573734275744665654c6f616e4e6f74526570616964220000022201010c0545706f6368220000022201010c064f7074696f6e2201012201012307210200022201010c044e6f6e6522000001022201010c04536f6d65220000022201010c10426f6f746c6f6164696e674572726f722201012201012307210400022201010c1a5265666572656e6365644e6f6465446f65734e6f74457869737422000001022201010c1b5265666572656e6365644e6f646549734e6f74416e4f626a65637422000002022201010c265265666572656e6365644e6f6465446f65734e6f74416c6c6f7744697265637441636365737322000003022201010c1a4661696c6564546f4170706c794465666572726564436f737473220000022201010c0b41626f7274526573756c74220101220001200c0106726561736f6e022201010c064f7074696f6e2201012201012307210200022201010c044e6f6e6522000001022201010c04536f6d65220000022201010c0e5265736f75726365735573616765220101220001200c0314686561705f616c6c6f636174696f6e735f73756d10686561705f7065616b5f6d656d6f72790a6370755f6379636c65732022cb010000000000000000000000000000000000000000000000000c012102220101091e000000220101091e00000000000000000000000000000000000c0121022201010902000000220101090200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0121022201010920000000220101092000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0121022201010920000000220101092000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002201010a0000000000000000 \ No newline at end of file diff --git a/radix-engine/src/transaction/system_structure.rs b/radix-engine/src/transaction/system_structure.rs index 58038c72209..e4f882a7394 100644 --- a/radix-engine/src/transaction/system_structure.rs +++ b/radix-engine/src/transaction/system_structure.rs @@ -29,13 +29,6 @@ pub struct SystemFieldStructure { pub field_kind: SystemFieldKind, } -// NOTE: Adding BootLoader and BootLoader(BootLoaderFieldKind) are both changes to the -// TransactionReceiptV1 encoding, BUT we only require the encoding to be consistent -// for the toolkit which parses it opaquely at the moment when doing transaction preview -// in the wallet. This change doesn't apply because BootLoader substates are only edited -// during transaction preview. -// -// Node depends on this structure, so append only. #[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)] pub enum SystemFieldKind { TypeInfo, @@ -98,7 +91,12 @@ pub struct EventSystemStructure { pub type SubstateSystemStructures = IndexMap>>; -#[derive(Default, Debug, Clone, ScryptoSbor, PartialEq, Eq)] +#[derive(Default, Debug, Clone, ScryptoSbor, PartialEq, Eq, ScryptoSborAssertion)] +// This is currently persisted in the node's `LocalTransactionExecution` store, +// so needs to be backwards compatible to avoid breaking the Core API transaction stream. +// This assertion can be removed when `radixdlt-scrypto` is merged into the node, +// because there will be an equivalent assertion against the `LocalTransactionExecution` itself. +#[sbor_assert(backwards_compatible(bottlenose = "FILE:system_structure_v1.txt"))] pub struct SystemStructure { pub substate_system_structures: SubstateSystemStructures, pub event_system_structures: IndexMap, diff --git a/radix-engine/src/transaction/system_structure_v1.txt b/radix-engine/src/transaction/system_structure_v1.txt new file mode 100644 index 00000000000..b6b7a3f16be --- /dev/null +++ b/radix-engine/src/transaction/system_structure_v1.txt @@ -0,0 +1 @@ +5c2102220001210320221e0e0120220201010a010000000000000001010a190000000000000010022201010a02000000000000002201010a03000000000000000d01220001070710022201010a04000000000000002201010a0500000000000000070010022201010a06000000000000002201010a09000000000000000f0123072003002201000107070122010001074102220101010a07000000000000000e0120220201010a0800000000000000000107410d0122000107070f012307200700220101010a0a0000000000000001220002220101010a0c0000000000000003220101010a110000000000000004220101010a160000000000000005220101010a170000000000000006220101010a18000000000000000e0120220101010a0b000000000000000f01230720040022000122000222000322000e0120220201010a0d0000000000000001010a0d000000000000000e0120220301010a020000000000000001010a0e0000000000000001010a0f000000000000000d0122000107070f012307200200220101010a10000000000000000122010001070a07000e0120220101010a12000000000000000f012307200200220101010a130000000000000001220101010a15000000000000000e0120220101010a14000000000000000e012022030001078301010a0e0000000000000001010a0f000000000000000e012022020001070701010a0d000000000000000e0120220201010a120000000000000001010a12000000000000000e0120220201010a120000000000000001010a12000000000000000e0120220201010a120000000000000001010a120000000000000010022201010a1a000000000000002201010a1d000000000000000e0120220201010a1b000000000000000001070c0f012307200200220101010a1c0000000000000001220201010a0200000000000000000107f00e01202202000107830001070c0e0120220101010a130000000000000020211e022201010c0f53797374656d537472756374757265220101220001200c021a73756273746174655f73797374656d5f73747275637475726573176576656e745f73797374656d5f7374727563747572657302220000220000022201010c064e6f6465496422000002220000220000022201010c0f506172746974696f6e4e756d62657222000002220000220000022201010c0b53756273746174654b65792201012201012307210300022201010c054669656c6422000001022201010c034d617022000002022201010c06536f727465642200000222000022000002220000220000022201010c17537562737461746553797374656d5374727563747572652201012201012307210700022201010c0b53797374656d4669656c6422000001022201010c0c53797374656d536368656d6122000002022201010c124b657956616c756553746f7265456e74727922000003022201010c0b4f626a6563744669656c6422000004022201010c1c4f626a6563744b657956616c7565506172746974696f6e456e74727922000005022201010c194f626a656374496e646578506172746974696f6e456e74727922000006022201010c1f4f626a656374536f72746564496e646578506172746974696f6e456e747279220000022201010c1453797374656d4669656c64537472756374757265220101220001200c010a6669656c645f6b696e64022201010c0f53797374656d4669656c644b696e642201012201012307210400022201010c0854797065496e666f22000001022201010c06566d426f6f7422000002022201010c0a53797374656d426f6f7422000003022201010c0a4b65726e656c426f6f74220000022201010c1b4b657956616c756553746f7265456e747279537472756374757265220101220001200c02106b65795f66756c6c5f747970655f69641276616c75655f66756c6c5f747970655f6964022201010c1146756c6c7953636f706564547970654964220000022201010c0a536368656d6148617368220000022201010c0b4c6f63616c5479706549642201012201012307210200022201010c0957656c6c4b6e6f776e22000001022201010c10536368656d614c6f63616c496e646578220000022201010c0f57656c6c4b6e6f776e547970654964220000022201010c0e4669656c64537472756374757265220101220001200c010c76616c75655f736368656d61022201010c1b4f626a6563745375627374617465547970655265666572656e63652201012201012307210200022201010c075061636b61676522000001022201010c0e4f626a656374496e7374616e6365220000022201010c145061636b616765547970655265666572656e6365220101220001200c010c66756c6c5f747970655f6964022201010c1146756c6c7953636f706564547970654964220000022201010c1b4f626a656374496e7374616e6365547970655265666572656e6365220101220001200c0210696e7374616e63655f747970655f6964157265736f6c7665645f66756c6c5f747970655f6964022201010c1f4b657956616c7565506172746974696f6e456e747279537472756374757265220101220001200c020a6b65795f736368656d610c76616c75655f736368656d61022201010c1c496e646578506172746974696f6e456e747279537472756374757265220101220001200c020a6b65795f736368656d610c76616c75655f736368656d61022201010c22536f72746564496e646578506172746974696f6e456e747279537472756374757265220101220001200c020a6b65795f736368656d610c76616c75655f736368656d6102220000220000022201010c134576656e74547970654964656e746966696572220000022201010c07456d69747465722201012201012307210200022201010c0846756e6374696f6e22000001022201010c064d6574686f64220000022201010c0b426c75657072696e744964220101220001200c020f7061636b6167655f616464726573730e626c75657072696e745f6e616d65022201010c144576656e7453797374656d537472756374757265220101220001200c01167061636b6167655f747970655f7265666572656e636520221e000000000c012102220101091e000000220101091e000000000000000000000000000c01210222010109020000002201010902000000000000000000000000000c012102220101092000000022010109200000000000000000000000000000000000000000000000000000000000000000002201010a0000000000000000 \ No newline at end of file diff --git a/radix-engine/src/transaction/transaction_receipt.rs b/radix-engine/src/transaction/transaction_receipt.rs index 31937233925..94861e5934f 100644 --- a/radix-engine/src/transaction/transaction_receipt.rs +++ b/radix-engine/src/transaction/transaction_receipt.rs @@ -25,7 +25,20 @@ define_single_versioned! { /// receipt versions, allowing us to release a wallet ahead-of-time which is forward /// compatible with a new version of the engine (and so a new transaction receipt). #[derive(Clone, ScryptoSbor)] - pub VersionedTransactionReceipt(TransactionReceiptVersions) => TransactionReceipt = TransactionReceiptV1 + pub VersionedTransactionReceipt(TransactionReceiptVersions) => TransactionReceipt = TransactionReceiptV1, + outer_attributes: [ + // VersionedTransactionReceipt is currently encoded in the node's preview API. + // It is then decoded in lots of different mobile wallets as part of Transaction Review. + // We therefore can't make any changes/additions, without breaking this. + // + // In the interim, we are planning to: + // * Temporarily, serialize a PreviewTransactionReceipt which is fixed as just a single v1 versioned + // VersionedTransactionReceipt, and have `impl From for PreviewTransactionReceipt`. + // This will allow us to add new receipt versions, but ensuring they can still map to the preview model. + // * Change the API to return some kind of explicit extensible preview DTO. + #[derive(ScryptoSborAssertion)] + #[sbor_assert(fixed("FILE:receipt_schema_bottlenose.txt"))] + ], } #[derive(Clone, ScryptoSbor, PartialEq, Eq)] diff --git a/radix-sbor-derive/src/lib.rs b/radix-sbor-derive/src/lib.rs index dc5742cd456..5521fd94f69 100644 --- a/radix-sbor-derive/src/lib.rs +++ b/radix-sbor-derive/src/lib.rs @@ -170,6 +170,98 @@ pub fn scrypto_sbor(input: TokenStream) -> TokenStream { } /// A macro for outputting tests and marker traits to assert that a type has maintained its shape over time. +/// +/// There are two types of assertion modes: +/// * "fixed" mode is used to ensure that a type is unchanged. +/// * "backwards_compatible" mode is used when multiple versions of the type are permissible, but +/// newer versions of the type must be compatible with the older version where they align. +/// This mode (A) ensures that the type's current schema is equivalent to the latest version, and +/// (B) ensures that each of the schemas is a strict extension of the previous mode. +/// +/// There is also a "generate" mode which can be used to export the current schema. Upon running the generated test, +/// the schema is either written to a file, or output in a panic message. +/// +/// ## Initial schema generation +/// +/// To output the generated schema to a file path relative to the source file, add an attribute like this - +/// and then run the test which gets generated. If using Rust Analyzer this can be run from the IDE, +/// or it can be run via `cargo test`. +/// +/// ```no_run +/// #[derive(ScryptoSbor, ScryptoSborAssertion)] +/// #[sbor_assert(generate("FILE:MyType-schema-v1.txt"))] +/// struct MyType { +/// // ... +/// } +/// ``` +/// +/// The test should generate the given file and then panic. If you wish to only generate the schema +/// in the panic message, you can with `#[sbor_assert(generate("INLINE"))]`. +/// +/// ## Fixed schema verification +/// +/// To verify the type's schema is unchanged, do: +/// ```no_run +/// #[derive(ScryptoSbor, ScryptoSborAssertion)] +/// #[sbor_assert(fixed("FILE:MyType-schema-v1.txt"))] +/// struct MyType { +/// // ... +/// } +/// ``` +/// +/// Other supported options are `fixed("INLINE:")` and `fixed("CONST:")`. +/// +/// ## Backwards compatibility verification +/// +/// To allow multiple backwards-compatible versions, you can do this: +/// ```no_run +/// #[derive(ScryptoSbor, ScryptoSborAssertion)] +/// #[sbor_assert(backwards_compatible( +/// version1 = "FILE:MyType-schema-v1.txt", +/// version2 = "FILE:MyType-schema-v2.txt", +/// ))] +/// struct MyType { +/// // ... +/// } +/// ``` +/// +/// Instead of `"FILE:X"`, you can also use `"INLINE:"` and `"CONST:"`. +/// +/// ## Custom settings +/// By default, the `fixed` mode will use `SchemaComparisonSettings::require_equality()` and +/// the `backwards_compatible` mode will use `SchemaComparisonSettings::allow_extension()`. +/// +/// You may wish to change these: +/// * If you just wish to ignore the equality of metadata such as names, you can use the +/// `allow_name_changes` flag. +/// * If you wish to override any settings, you can provide a constant containing your +/// own SchemaComparisonSettings. +/// +/// For example: +/// ```no_run +/// #[derive(ScryptoSbor, ScryptoSborAssertion)] +/// #[sbor_assert( +/// fixed("FILE:MyType-schema-v1.txt"), +/// settings(allow_name_changes), +/// )] +/// struct MyType { +/// // ... +/// } +/// +/// const CUSTOM_COMPARISON_SETTINGS: sbor::schema::SchemaComparisonSettings = sbor::schema::SchemaComparisonSettings::require_equality(); +/// +/// #[derive(ScryptoSbor, ScryptoSborAssertion)] +/// #[sbor_assert( +/// backwards_compatible( +/// version1 = "FILE:MyType-schema-v1.txt", +/// version2 = "FILE:MyType-schema-v2.txt", +/// ), +/// settings(CUSTOM_COMPARISON_SETTINGS), +/// )] +/// struct MyOtherType { +/// // ... +/// } +/// ``` #[proc_macro_derive(ScryptoSborAssertion, attributes(sbor_assert))] pub fn scrypto_sbor_assertion(input: TokenStream) -> TokenStream { sbor_derive_common::sbor_assert::handle_sbor_assert_derive( diff --git a/sbor-derive-common/Cargo.toml b/sbor-derive-common/Cargo.toml index a65843e53fd..17205987d8c 100644 --- a/sbor-derive-common/Cargo.toml +++ b/sbor-derive-common/Cargo.toml @@ -13,6 +13,7 @@ syn = { workspace = true, features = ["full", "extra-traits"] } quote = { workspace = true } const-sha1 = { workspace = true } # Chosen because of its small size and 0 transitive dependencies itertools = { workspace = true } +indexmap = { workspace = true } [features] trace = [] diff --git a/sbor-derive-common/src/sbor_assert.rs b/sbor-derive-common/src/sbor_assert.rs index c37e7807168..c2245df1857 100644 --- a/sbor-derive-common/src/sbor_assert.rs +++ b/sbor-derive-common/src/sbor_assert.rs @@ -1,7 +1,6 @@ use proc_macro2::{Span, TokenStream}; use quote::format_ident; use quote::quote; -use quote::ToTokens; use spanned::Spanned as _; use syn::*; @@ -31,13 +30,15 @@ pub fn handle_sbor_assert_derive( )); }; - let assertion_variant = extract_assertion_variant(&parsed.attrs)?; + let (assertion_variant, advanced_settings) = extract_settings(&parsed.attrs)?; let output = match assertion_variant { - AssertionVariant::Generate => handle_generate(context_custom_schema, parsed), - AssertionVariant::Fixed(params) => handle_fixed(context_custom_schema, parsed, params), - AssertionVariant::BackwardsCompatible(params) => { - handle_backwards_compatible(context_custom_schema, parsed, params) + AssertionMode::Generate(params) => handle_generate(context_custom_schema, parsed, params), + AssertionMode::Fixed(params) => { + handle_fixed(context_custom_schema, parsed, params, advanced_settings) + } + AssertionMode::BackwardsCompatible(params) => { + handle_backwards_compatible(context_custom_schema, parsed, params, advanced_settings) } }?; @@ -45,124 +46,242 @@ pub fn handle_sbor_assert_derive( Ok(output) } -const GENERAL_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(generate)]` OR `#[sbor_assert(fixed(...))]` or `#[sbor_assert(backwards_compatible(...))]`"; -const FIXED_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(fixed(\"\"))]` OR `#[sbor_assert(fixed(CONSTANT))]` where CONSTANT is a string or implements `IntoSchema`"; -const BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(backwards_compatible(version1 = \"...\", version2 = \"...\"))]` where the placeholders are hex-encoded schemas, OR `#[sbor_assert(backwards_compatible(CONSTANT))]` where CONSTANT implements `IntoIterator, K: AsRef, V: IntoSchema`. For example: `const TYPE_X_NAMED_VERSIONS: [(&'static str, &'static str); X] = [(\"version1\", \"...\")]`"; +const GENERAL_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(generate(..))]` OR `#[sbor_assert(fixed(..))]` OR `#[sbor_assert(backwards_compatible(..))], with an optional second `settings(..)` parameter to `sbor_assert`."; -fn extract_assertion_variant(attributes: &[Attribute]) -> Result { +fn extract_settings(attributes: &[Attribute]) -> Result<(AssertionMode, AdvancedSettings)> { // When we come to extract fixed named types, let inner_attributes = extract_wrapped_root_attributes(attributes, "sbor_assert")?; let keyed_inner_attributes = extract_wrapped_inner_attributes(&inner_attributes, GENERAL_PARSE_ERROR_MESSAGE)?; - if keyed_inner_attributes.len() != 1 { + if keyed_inner_attributes.len() == 0 || keyed_inner_attributes.len() > 2 { return Err(Error::new(Span::call_site(), GENERAL_PARSE_ERROR_MESSAGE)); } - let (attribute_name, (attribute_name_ident, attribute_value)) = - keyed_inner_attributes.into_iter().next().unwrap(); - match attribute_name.as_str() { - "generate" => { - if attribute_value.is_some() { - Err(Error::new( + let assertion_mode = { + let (attribute_name, (attribute_name_ident, attribute_value)) = + keyed_inner_attributes.get_index(0).unwrap(); + let error_span = attribute_name_ident.span(); + match attribute_name.as_str() { + "generate" => AssertionMode::Generate(extract_generation_options( + attribute_value.as_ref(), + error_span, + )?), + "backwards_compatible" => { + AssertionMode::BackwardsCompatible(extract_backwards_compatible_schema_parameters( + attribute_value.as_ref(), attribute_name_ident.span(), - GENERAL_PARSE_ERROR_MESSAGE, - )) - } else { - Ok(AssertionVariant::Generate) + )?) } + "fixed" => AssertionMode::Fixed(extract_fixed_schema_options( + attribute_value.as_ref(), + error_span, + )?), + _ => return Err(Error::new(error_span, GENERAL_PARSE_ERROR_MESSAGE)), } - "backwards_compatible" => { - let schema_parameters = match attribute_value { - Some(meta_list) => { - if let [NestedMeta::Meta(Meta::Path(path))] = meta_list.as_slice() { - // Constant - BackwardsCompatibleSchemaParameters::FromConstant { - constant: path.clone(), - } - } else { - // Key-value based - let named_schemas = meta_list - .iter() - .map(|meta| -> Result<_> { - match meta { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - path, - lit, - .. - })) => { - let Some(ident) = path.get_ident() else { - return Err(Error::new( - path.span(), - BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE, - )); - }; - Ok(NamedSchema { - name: ident.clone(), - schema: lit.into_token_stream(), - }) - } - _ => { - return Err(Error::new( - meta.span(), - BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE, - )) - } - } - }) - .collect::>()?; - BackwardsCompatibleSchemaParameters::NamedSchemas { named_schemas } - } - } - _ => { - return Err(Error::new( - attribute_name_ident.span(), - BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE, - )); - } - }; - Ok(AssertionVariant::BackwardsCompatible(schema_parameters)) + }; + + let advanced_settings = if let Some(second_attribute) = keyed_inner_attributes.get_index(1) { + let (attribute_name, (attribute_name_ident, attribute_value)) = second_attribute; + let error_span = attribute_name_ident.span(); + + match attribute_name.as_str() { + "settings" => extract_advanced_settings(attribute_value.as_ref(), error_span)?, + _ => return Err(Error::new(error_span, SETTINGS_PARSE_ERROR_MESSAGE)), } - "fixed" => { - let fixed_schema_parameters = match attribute_value { - Some(inner) if inner.len() == 1 => match inner[0] { - NestedMeta::Meta(Meta::Path(path)) => FixedSchemaParameters::FromConstant { - constant_path: path.clone(), - }, - NestedMeta::Lit(Lit::Str(lit_str)) => FixedSchemaParameters::FixedSchema { - fixed_schema: lit_str.into_token_stream(), - }, - _ => { - return Err(Error::new( - attribute_name_ident.span(), - FIXED_PARSE_ERROR_MESSAGE, - )); - } - }, - _ => { - return Err(Error::new( - attribute_name_ident.span(), - FIXED_PARSE_ERROR_MESSAGE, - )); + } else { + AdvancedSettings { + settings_resolution: ComparisonSettingsResolution::Default, + } + }; + + Ok((assertion_mode, advanced_settings)) +} + +const GENERATE_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(generate(\"INLINE\"))]` OR `#[sbor_assert(generate(\"FILE:\"))]`"; + +fn extract_generation_options( + attribute_value: Option<&Vec<&NestedMeta>>, + error_span: Span, +) -> Result { + match attribute_value { + Some(meta_list) if meta_list.len() == 1 => match meta_list[0] { + NestedMeta::Lit(Lit::Str(lit_str)) => { + let content = lit_str.value(); + if content == "INLINE" { + return Ok(GenerationOptions::Inline); + } else if let Some(file_path) = extract_prefixed(lit_str, "FILE:") { + return Ok(GenerationOptions::File { file_path }); } - }; - Ok(AssertionVariant::Fixed(fixed_schema_parameters)) + } + _ => {} + }, + _ => {} + }; + return Err(Error::new(error_span, GENERATE_PARSE_ERROR_MESSAGE)); +} + +const FIXED_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(fixed(X))]` where `X` is one of:\n* `\"INLINE:\"`\n* `\"FILE:\"`\n* Either `NAMED_CONSTANT` or `\"CONST:\"` where `` is the name of a defined constant string literal or some other type implementing `IntoSchema`"; + +fn extract_fixed_schema_options( + attribute_value: Option<&Vec<&NestedMeta>>, + error_span: Span, +) -> Result { + match attribute_value { + Some(meta_list) if meta_list.len() == 1 => match meta_list[0] { + NestedMeta::Meta(Meta::Path(path)) => { + return Ok(SchemaLocation::FromConstant { + constant_path: path.clone(), + }); + } + NestedMeta::Lit(Lit::Str(lit_str)) => { + return extract_schema_location_from_string(lit_str); + } + _ => {} + }, + _ => {} + } + return Err(Error::new(error_span, FIXED_PARSE_ERROR_MESSAGE)); +} + +const BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE: &'static str = "Expected EITHER `#[sbor_assert(backwards_compatible(version1 = X, version2 = X))]` where `X` is one of:\n* `\"INLINE:\"`\n* `\"FILE:\"`\n* `\"CONST:\"` where `` is the name of a defined constant string literal or some other type implementing `IntoSchema`\n\nOR `#[sbor_assert(backwards_compatible())]` where `` is the name of a defined constant whose type implements `IntoIterator, K: AsRef, V: IntoSchema`. For example: `const TYPE_X_NAMED_VERSIONS: [(&'static str, &'static str); 1] = [(\"version1\", \"...\")]`"; + +fn extract_backwards_compatible_schema_parameters( + attribute_value: Option<&Vec<&NestedMeta>>, + error_span: Span, +) -> Result { + match attribute_value { + Some(meta_list) => { + if let [NestedMeta::Meta(Meta::Path(path))] = meta_list.as_slice() { + return Ok(BackwardsCompatibleSchemaParameters::FromConstant { + constant: path.clone(), + }); + } else { + // Assume key-value based + let named_schemas = meta_list + .iter() + .map(|meta| -> Result<_> { + match meta { + NestedMeta::Meta(Meta::NameValue(MetaNameValue { + path, lit, .. + })) => { + let Some(ident) = path.get_ident() else { + return Err(Error::new( + path.span(), + BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE, + )); + }; + let Lit::Str(lit_str) = lit else { + return Err(Error::new( + path.span(), + "Only string literals are supported here", + )); + }; + Ok(NamedSchema { + name: ident.clone(), + schema: extract_schema_location_from_string(lit_str)?, + }) + } + _ => { + return Err(Error::new( + meta.span(), + BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE, + )) + } + } + }) + .collect::>()?; + return Ok(BackwardsCompatibleSchemaParameters::NamedSchemas { named_schemas }); + } + } + _ => {} + } + + return Err(Error::new( + error_span, + BACKWARDS_COMPATIBLE_PARSE_ERROR_MESSAGE, + )); +} + +fn extract_schema_location_from_string(lit_str: &LitStr) -> Result { + let schema_definition = if let Some(file_path) = extract_prefixed(lit_str, "FILE:") { + SchemaLocation::StringFromFile { file_path } + } else if let Some(constant_path) = extract_prefixed(lit_str, "CONST:") { + SchemaLocation::FromConstant { + constant_path: constant_path.parse()?, + } + } else if let Some(inline_schema) = extract_prefixed(lit_str, "INLINE:") { + SchemaLocation::InlineString { + inline: inline_schema, } - _ => Err(Error::new( - attribute_name_ident.span(), - GENERAL_PARSE_ERROR_MESSAGE, - )), + } else { + return Err(Error::new( + lit_str.span(), + "Expected string to be prefixed with FILE:, CONST: or INLINE:", + )); + }; + + Ok(schema_definition) +} + +const SETTINGS_PARSE_ERROR_MESSAGE: &'static str = "Expected `#[sbor_assert(__, settings(allow_name_changes))]` OR `#[sbor_assert(__, settings())]` `` is the name of a defined constant with type `SchemaComparisonSettings`"; + +fn extract_advanced_settings( + attribute_value: Option<&Vec<&NestedMeta>>, + error_span: Span, +) -> Result { + match attribute_value { + Some(meta_list) if meta_list.len() == 1 => match meta_list[0] { + NestedMeta::Meta(Meta::Path(path)) => { + let allow_name_changes = if let Some(ident) = path.get_ident() { + ident.to_string() == "allow_name_changes" + } else { + false + }; + if allow_name_changes { + return Ok(AdvancedSettings { + settings_resolution: + ComparisonSettingsResolution::DefaultAllowingNameChanges, + }); + } else { + return Ok(AdvancedSettings { + settings_resolution: ComparisonSettingsResolution::FromConstant { + constant_path: path.clone(), + }, + }); + } + } + _ => {} + }, + _ => {} + }; + return Err(Error::new(error_span, SETTINGS_PARSE_ERROR_MESSAGE)); +} + +fn extract_prefixed(lit_str: &LitStr, prefix: &str) -> Option { + let contents = lit_str.value(); + if contents.starts_with(prefix) { + let (_prefix, inner_contents) = contents.split_at(prefix.len()); + Some(LitStr::new(inner_contents, lit_str.span())) + } else { + None } } -enum AssertionVariant { - Generate, - Fixed(FixedSchemaParameters), +enum AssertionMode { + Generate(GenerationOptions), + Fixed(SchemaLocation), BackwardsCompatible(BackwardsCompatibleSchemaParameters), } -enum FixedSchemaParameters { +enum GenerationOptions { + Inline, + File { file_path: LitStr }, +} + +enum SchemaLocation { + InlineString { inline: LitStr }, FromConstant { constant_path: Path }, - FixedSchema { fixed_schema: TokenStream }, + StringFromFile { file_path: LitStr }, } enum BackwardsCompatibleSchemaParameters { @@ -172,14 +291,108 @@ enum BackwardsCompatibleSchemaParameters { struct NamedSchema { name: Ident, - schema: TokenStream, + schema: SchemaLocation, +} + +struct AdvancedSettings { + settings_resolution: ComparisonSettingsResolution, +} + +enum ComparisonSettingsResolution { + Default, + DefaultAllowingNameChanges, + FromConstant { constant_path: Path }, } -fn handle_generate(context_custom_schema: &str, parsed: DeriveInput) -> Result { +/// Only supposed to be used as a temporary mode, to assist with generating the schema. The generated test always panics. +fn handle_generate( + context_custom_schema: &str, + parsed: DeriveInput, + options: GenerationOptions, +) -> Result { let DeriveInput { ident, .. } = &parsed; let custom_schema: Path = parse_str(context_custom_schema)?; - let test_ident = format_ident!("test_{}_type_is_generated_in_panic_message", ident); + let test_ident = format_ident!("test_{}_type_schema_is_generated", ident); + + let output_content = match options { + GenerationOptions::Inline => quote! { + panic!( + "Copy the below encoded type schema and replace `generate` with `fixed` or `backwards_compatible` in the attribute to receive further instructions.\n The current type schema is:\n{hex}" + ); + }, + GenerationOptions::File { file_path } => quote! { + use std::path::{Path, PathBuf}; + use std::fs::File; + use std::io::Write; + use std::convert::AsRef; + + // So `file!()` is only intended for debugging, and is currently a relative path against `CARGO_RUSTC_CURRENT_DIR`. + // However `CARGO_RUSTC_CURRENT_DIR` is a nightly-only env variable. + // + // For single crates, `CARGO_RUSTC_CURRENT_DIR` = `CARGO_MANIFEST_DIR` + // For workspaces, `CARGO_RUSTC_CURRENT_DIR` is the workspace root, typically an ancestor of `CARGO_MANIFEST_DIR` + // + // So we add some resolution logic to resolve things... + // + // RELEVANT LINKS: + // * https://github.com/rust-lang/cargo/issues/3946#issuecomment-412363291 - Absolute use of `file!()` + // * https://github.com/rust-lang/cargo/issues/3946#issuecomment-1832514876 + // * https://github.com/rust-lang/cargo/pull/13644 - blocked stabilization of `CARGO_RUSTC_CURRENT_DIR` + + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let relative_source_file_path = file!(); + + let mut path_root = PathBuf::from(&manifest_dir); + let source_file_path = loop { + let candidate_source_file_path = path_root.as_path().join(relative_source_file_path); + if candidate_source_file_path.is_file() { + break candidate_source_file_path; + } + if !path_root.pop() { + panic!( + "Could not resolve the source file path from CARGO_MANIFEST_DIR ({}) and file!() path ({})", + manifest_dir, + relative_source_file_path, + ); + } + }; + + let source_file_folder = source_file_path + .parent() + .unwrap_or_else(|| panic!( + "Could not resolve the parent folder of the current source file: {}", + source_file_path.display(), + )); + + if !source_file_folder.is_dir() { + panic!( + "The resolved parent folder of the current source file doesn't appear to exist: {}", + source_file_folder.display(), + ); + } + + // Resolve the provided file path relative to the source file's folder + let full_file_path = source_file_folder.join(#file_path); + + let mut file = File::create_new(full_file_path.as_path()) + .unwrap_or_else(|err| panic!( + "Could not open new file for writing - perhaps it already exists? If you wish to replace it, delete it first: {} - Error: {}", + full_file_path.display(), + err, + )); + + file.write_all(hex.as_ref()) + .unwrap_or_else(|err| panic!( + "Schema could not be written to {} - Error: {}", + full_file_path.display(), + err, + )); + + // We panic because the generate test is always expected to fail - so that someone doesn't leave it in generate mode accidentally. + panic!("Schema written successfully to {}", full_file_path.display()); + }, + }; // NOTE: Generics are explicitly _NOT_ supported for now, because we need a concrete type // to generate the schema from. @@ -189,9 +402,8 @@ fn handle_generate(context_custom_schema: &str, parsed: DeriveInput) -> Result::for_type::<#ident>(); - panic!( - "Copy the below encoded type schema and replace `generate` with `fixed` or `backwards_compatible` in the attribute to receive further instructions.\n The current type schema is:\n{}", type_schema.encode_to_hex() - ); + let hex = type_schema.encode_to_hex(); + #output_content } }; @@ -201,22 +413,29 @@ fn handle_generate(context_custom_schema: &str, parsed: DeriveInput) -> Result Result { let DeriveInput { ident, .. } = &parsed; - let fixed_schema = match params { - FixedSchemaParameters::FromConstant { constant_path } => { - quote! { sbor::schema::SingleTypeSchema::from(#constant_path) } - } - FixedSchemaParameters::FixedSchema { fixed_schema } => { - quote! { sbor::schema::SingleTypeSchema::from(#fixed_schema) } - } - }; + let fixed_schema = schema_location_to_single_type_schema_code(&schema_location); let custom_schema: Path = parse_str(context_custom_schema)?; let test_ident = format_ident!("test_{}_type_is_fixed", ident); + let comparison_settings = match advanced_settings.settings_resolution { + ComparisonSettingsResolution::Default => quote! { + sbor::schema::SchemaComparisonSettings::require_equality() + }, + ComparisonSettingsResolution::DefaultAllowingNameChanges => quote! { + sbor::schema::SchemaComparisonSettings::require_equality() + .metadata_settings(sbor::schema::SchemaComparisonMetadataSettings::allow_all_changes()) + }, + ComparisonSettingsResolution::FromConstant { constant_path } => quote! { + #constant_path.clone() + }, + }; + // NOTE: Generics are explicitly _NOT_ supported for now, because we need a concrete type // to generate the schema from. let output = quote! { @@ -227,15 +446,10 @@ fn handle_fixed( #[test] #[allow(non_snake_case)] pub fn #test_ident() { + let comparison_settings: sbor::schema::SchemaComparisonSettings = #comparison_settings; let current = sbor::schema::SingleTypeSchema::for_type::<#ident>(); let fixed = #fixed_schema; - let result = sbor::schema::compare_single_type_schemas::< - #custom_schema, - >( - &sbor::schema::SchemaComparisonSettings::require_equality(), - &fixed, - ¤t, - ); + let result = sbor::schema::compare_single_type_schemas::<#custom_schema>(&comparison_settings, &fixed, ¤t); if let Some(error_message) = result.error_message("fixed", "current") { use sbor::rust::fmt::Write; use sbor::rust::prelude::String; @@ -251,42 +465,79 @@ fn handle_fixed( Ok(output) } +fn schema_location_to_single_type_schema_code(params: &SchemaLocation) -> TokenStream { + match params { + SchemaLocation::FromConstant { constant_path } => { + quote! { sbor::schema::SingleTypeSchema::from(#constant_path) } + } + SchemaLocation::InlineString { + inline: fixed_schema, + } => { + quote! { sbor::schema::SingleTypeSchema::from(#fixed_schema) } + } + SchemaLocation::StringFromFile { file_path } => { + quote! { sbor::schema::SingleTypeSchema::from(include_str!(#file_path)) } + } + } +} + fn handle_backwards_compatible( context_custom_schema: &str, parsed: DeriveInput, params: BackwardsCompatibleSchemaParameters, + advanced_settings: AdvancedSettings, ) -> Result { let DeriveInput { ident, .. } = &parsed; let custom_schema: Path = parse_str(context_custom_schema)?; let test_ident = format_ident!("test_{}_type_is_backwards_compatible", ident); + let comparison_settings = match advanced_settings.settings_resolution { + ComparisonSettingsResolution::Default => quote! { + sbor::schema::SchemaComparisonSettings::allow_extension() + }, + ComparisonSettingsResolution::DefaultAllowingNameChanges => quote! { + sbor::schema::SchemaComparisonSettings::allow_extension() + .metadata_settings(sbor::schema::SchemaComparisonMetadataSettings::allow_all_changes()) + }, + ComparisonSettingsResolution::FromConstant { constant_path } => quote! { + #constant_path.clone() + }, + }; + let test_content = match params { BackwardsCompatibleSchemaParameters::FromConstant { constant } => { quote! { - sbor::schema::assert_type_backwards_compatibility::< - #custom_schema, - #ident, - >(|v| sbor::schema::NamedSchemaVersions::from(#constant)); + let comparison_settings: sbor::schema::SchemaComparisonSettings = #comparison_settings; + sbor::schema::assert_type_compatibility::<#custom_schema, #ident>( + &comparison_settings, + |v| sbor::schema::NamedSchemaVersions::from(#constant), + ); } } BackwardsCompatibleSchemaParameters::NamedSchemas { named_schemas } => { // NOTE: It's okay for these to be empty - the test output will output a correct default schema. let (version_names, schemas): (Vec<_>, Vec<_>) = named_schemas .into_iter() - .map(|named_schema| (named_schema.name.to_string(), named_schema.schema)) + .map(|named_schema| { + ( + named_schema.name.to_string(), + schema_location_to_single_type_schema_code(&named_schema.schema), + ) + }) .unzip(); quote! { - sbor::schema::assert_type_backwards_compatibility::< - #custom_schema, - #ident, - >(|v| { - v - #( - .register_version(#version_names, #schemas) - )* - }); + let comparison_settings: sbor::schema::SchemaComparisonSettings = #comparison_settings; + sbor::schema::assert_type_compatibility::<#custom_schema, #ident>( + &comparison_settings, + |v| { + v + #( + .register_version(#version_names, #schemas) + )* + } + ); } } }; diff --git a/sbor-derive-common/src/utils.rs b/sbor-derive-common/src/utils.rs index 8849ac4f4d8..c59bd6187ae 100644 --- a/sbor-derive-common/src/utils.rs +++ b/sbor-derive-common/src/utils.rs @@ -13,6 +13,11 @@ use quote::format_ident; use quote::quote; use syn::*; +// See https://github.com/bluss/indexmap/pull/207 +// By defining an alias with a default `DefaultHashBuilder`, we ensure that this type works as `IndexMap` and that the `FromIter` impl works in no-std. +type DefaultHashBuilder = std::collections::hash_map::RandomState; +type IndexMap = indexmap::IndexMap; + #[allow(dead_code)] pub fn print_generated_code(kind: &str, code: S) { if let Ok(mut proc) = Command::new("rustfmt") @@ -154,8 +159,8 @@ pub fn extract_wrapped_root_attributes( pub fn extract_wrapped_inner_attributes<'m>( inner_attributes: &'m [Meta], error_message: &str, -) -> Result>)>> { - let mut fields = BTreeMap::new(); +) -> Result>)>> { + let mut fields = IndexMap::default(); for meta in inner_attributes { let (name, inner) = extract_wrapping_inner_attribute(meta, error_message)?; fields.insert(name.to_string(), (name.clone(), inner)); @@ -595,6 +600,11 @@ pub fn get_generic_types(generics: &Generics) -> Vec { .collect() } +pub fn parse_str_with_span(source_string: &str, span: Span) -> Result { + // https://github.com/dtolnay/syn/issues/559 + LitStr::new(source_string, span).parse() +} + pub fn parse_single_type(source_string: &LitStr) -> syn::Result { source_string.parse() } diff --git a/sbor-derive/src/lib.rs b/sbor-derive/src/lib.rs index 660f537d685..68cfc9fe277 100644 --- a/sbor-derive/src/lib.rs +++ b/sbor-derive/src/lib.rs @@ -215,6 +215,98 @@ pub fn basic_sbor(input: TokenStream) -> TokenStream { } /// A macro for outputting tests and marker traits to assert that a type has maintained its shape over time. +/// +/// There are two types of assertion modes: +/// * "fixed" mode is used to ensure that a type is unchanged. +/// * "backwards_compatible" mode is used when multiple versions of the type are permissible, but +/// newer versions of the type must be compatible with the older version where they align. +/// This mode (A) ensures that the type's current schema is equivalent to the latest version, and +/// (B) ensures that each of the schemas is a strict extension of the previous mode. +/// +/// There is also a "generate" mode which can be used to export the current schema. Upon running the generated test, +/// the schema is either written to a file, or output in a panic message. +/// +/// ## Initial schema generation +/// +/// To output the generated schema to a file path relative to the source file, add an attribute like this - +/// and then run the test which gets generated. If using Rust Analyzer this can be run from the IDE, +/// or it can be run via `cargo test`. +/// +/// ```no_run +/// #[derive(BasicSbor, BasicSborAssertion)] +/// #[sbor_assert(generate("FILE:MyType-schema-v1.txt"))] +/// struct MyType { +/// // ... +/// } +/// ``` +/// +/// The test should generate the given file and then panic. If you wish to only generate the schema +/// in the panic message, you can with `#[sbor_assert(generate("INLINE"))]`. +/// +/// ## Fixed schema verification +/// +/// To verify the type's schema is unchanged, do: +/// ```no_run +/// #[derive(BasicSbor, BasicSborAssertion)] +/// #[sbor_assert(fixed("FILE:MyType-schema-v1.txt"))] +/// struct MyType { +/// // ... +/// } +/// ``` +/// +/// Other supported options are `fixed("INLINE:")` and `fixed("CONST:")`. +/// +/// ## Backwards compatibility verification +/// +/// To allow multiple backwards-compatible versions, you can do this: +/// ```no_run +/// #[derive(BasicSbor, BasicSborAssertion)] +/// #[sbor_assert(backwards_compatible( +/// version1 = "FILE:MyType-schema-v1.txt", +/// version2 = "FILE:MyType-schema-v2.txt", +/// ))] +/// struct MyType { +/// // ... +/// } +/// ``` +/// +/// Instead of `"FILE:X"`, you can also use `"INLINE:"` and `"CONST:"`. +/// +/// ## Custom settings +/// By default, the `fixed` mode will use `SchemaComparisonSettings::require_equality()` and +/// the `backwards_compatible` mode will use `SchemaComparisonSettings::allow_extension()`. +/// +/// You may wish to change these: +/// * If you just wish to ignore the equality of metadata such as names, you can use the +/// `allow_name_changes` flag. +/// * If you wish to override any settings, you can provide a constant containing your +/// own SchemaComparisonSettings. +/// +/// For example: +/// ```no_run +/// #[derive(BasicSbor, BasicSborAssertion)] +/// #[sbor_assert( +/// fixed("FILE:MyType-schema-v1.txt"), +/// settings(allow_name_changes), +/// )] +/// struct MyType { +/// // ... +/// } +/// +/// const CUSTOM_COMPARISON_SETTINGS: sbor::schema::SchemaComparisonSettings = sbor::schema::SchemaComparisonSettings::require_equality(); +/// +/// #[derive(BasicSbor, BasicSborAssertion)] +/// #[sbor_assert( +/// backwards_compatible( +/// version1 = "FILE:MyType-schema-v1.txt", +/// version2 = "FILE:MyType-schema-v2.txt", +/// ), +/// settings(CUSTOM_COMPARISON_SETTINGS), +/// )] +/// struct MyOtherType { +/// // ... +/// } +/// ``` #[proc_macro_derive(BasicSborAssertion, attributes(sbor_assert))] pub fn basic_sbor_assertion(input: TokenStream) -> TokenStream { sbor_derive_common::sbor_assert::handle_sbor_assert_derive( diff --git a/sbor-tests/tests/sbor_assert.rs b/sbor-tests/tests/sbor_assert.rs index 97bee4e6994..208e165e73e 100644 --- a/sbor-tests/tests/sbor_assert.rs +++ b/sbor-tests/tests/sbor_assert.rs @@ -4,7 +4,7 @@ use sbor::*; #[derive(BasicSborAssertion, BasicSbor)] -#[sbor_assert(fixed("5b210222000121032022010f012307200100220100010709202101022201010c0a4d7954657374456e756d2201012201012307210100022201010c0548656c6c6f220101220001200c0105737461746520220100002201010a0000000000000000"))] +#[sbor_assert(fixed("INLINE:5b210222000121032022010f012307200100220100010709202101022201010c0a4d7954657374456e756d2201012201012307210100022201010c0548656c6c6f220101220001200c0105737461746520220100002201010a0000000000000000"))] #[sbor(type_name = "MyTestEnum")] pub enum MyTestEnum_FixedSchema_Test1 { Hello { state: u32 }, @@ -19,12 +19,26 @@ pub enum MyTestEnum_FixedSchema_Test2 { Hello { state: u32 }, } +#[derive(BasicSborAssertion, BasicSbor)] +#[sbor_assert(fixed("CONST:TEST_ENUM_SCHEMA"))] +#[sbor(type_name = "MyTestEnum")] +pub enum MyTestEnum_FixedSchema_Test3 { + Hello { state: u32 }, +} + +#[derive(BasicSborAssertion, BasicSbor)] +#[sbor_assert(fixed("FILE:test_enum_v1_schema.txt"))] +#[sbor(type_name = "MyTestEnum")] +pub enum MyTestEnum_FixedSchema_Test4 { + Hello { state: u32 }, +} + const TEST_ENUM_V2_SCHEMA: &'static str = "5b210222000121032022010f012307200200220100010709012200202101022201010c0a4d7954657374456e756d2201012201012307210200022201010c0548656c6c6f220101220001200c0105737461746501022201010c05576f726c6422000020220100002201010a0000000000000000"; #[derive(BasicSborAssertion, BasicSbor)] #[sbor_assert(backwards_compatible( - v1 = "5b210222000121032022010f012307200100220100010709202101022201010c0a4d7954657374456e756d2201012201012307210100022201010c0548656c6c6f220101220001200c0105737461746520220100002201010a0000000000000000", - v2 = "5b210222000121032022010f012307200200220100010709012200202101022201010c0a4d7954657374456e756d2201012201012307210200022201010c0548656c6c6f220101220001200c0105737461746501022201010c05576f726c6422000020220100002201010a0000000000000000", + v1 = "FILE:test_enum_v1_schema.txt", + v2 = "CONST:TEST_ENUM_V2_SCHEMA", ))] #[sbor(type_name = "MyTestEnum")] pub enum MyTestEnum_WhichHasBeenExtended_Test1 { @@ -45,3 +59,36 @@ pub enum MyTestEnum_WhichHasBeenExtended_Test2 { Hello { state: u32 }, World, // Extension } + +const TEST_ENUM_V2_RENAMED_SCHEMA: &'static str = "5b210222000121032022010f012307200200220100010709012200202101022201010c0a4d7954657374456e756d2201012201012307210200022201010c0548656c6c6f220101220001200c010d73746174655f72656e616d656401022201010c05576f726c6422000020220100002201010a0000000000000000"; + +#[derive(BasicSborAssertion, BasicSbor)] +#[sbor_assert( + backwards_compatible( + v1 = "FILE:test_enum_v1_schema.txt", + v2 = "CONST:TEST_ENUM_V2_RENAMED_SCHEMA", + ), + settings(allow_name_changes) +)] +#[sbor(type_name = "MyTestEnum")] +pub enum MyTestEnum_WhichHasBeenExtendedAndFieldNameChanged_WorksWithAllowNameChanges { + Hello { state_renamed: u32 }, + World, // Extension +} + +const ALLOW_RENAME_SETTINGS: SchemaComparisonSettings = SchemaComparisonSettings::allow_extension() + .metadata_settings(SchemaComparisonMetadataSettings::allow_all_changes()); + +#[derive(BasicSborAssertion, BasicSbor)] +#[sbor_assert( + backwards_compatible( + v1 = "FILE:test_enum_v1_schema.txt", + v2 = "CONST:TEST_ENUM_V2_RENAMED_SCHEMA", + ), + settings(ALLOW_RENAME_SETTINGS) +)] +#[sbor(type_name = "MyTestEnum")] +pub enum MyTestEnum_WhichHasBeenExtendedAndFieldNameChanged_WorksWithOverridenSettings { + Hello { state_renamed: u32 }, + World, // Extension +} diff --git a/sbor-tests/tests/test_enum_v1_schema.txt b/sbor-tests/tests/test_enum_v1_schema.txt new file mode 100644 index 00000000000..0d62275dd34 --- /dev/null +++ b/sbor-tests/tests/test_enum_v1_schema.txt @@ -0,0 +1 @@ +5b210222000121032022010f012307200100220100010709202101022201010c0a4d7954657374456e756d2201012201012307210100022201010c0548656c6c6f220101220001200c0105737461746520220100002201010a0000000000000000 \ No newline at end of file diff --git a/sbor/src/versioned.rs b/sbor/src/versioned.rs index ac02489ca6b..7da03999252 100644 --- a/sbor/src/versioned.rs +++ b/sbor/src/versioned.rs @@ -112,7 +112,8 @@ pub trait UniqueVersioned: Versioned { /// This macro is just a simpler wrapper around the [`define_versioned`] macro, /// for use when there's just a single version. /// -/// Example usage: +/// ## Example usage +/// /// ```rust /// use sbor::prelude::*; /// @@ -123,7 +124,11 @@ pub trait UniqueVersioned: Versioned { /// /// define_single_versioned! { /// #[derive(Clone, PartialEq, Eq, Hash, Debug, Sbor)] -/// pub VersionedFoo(FooVersions) => Foo = FooV1 +/// pub VersionedFoo(FooVersions) => Foo = FooV1, +/// outer_attributes: [ +/// ], +/// inner_attributes: [ +/// ], /// } /// /// // `Foo` is created as an alias for `FooV1` @@ -135,14 +140,41 @@ pub trait UniqueVersioned: Versioned { /// assert_eq!(a2, a3); /// assert_eq!(42, a.as_unique_version().bar); /// ``` +/// +/// ## Advanced attribute handling +/// +/// Note that the provided attributes get applied to _both_ the outer "Versioned" type, +/// and the inner "Versions" type. To only apply to one type, you can include the +/// `outer_attributes` optional argument and/or the `inner_attributes` optional argument: +/// ``` +/// define_single_versioned! { +/// #[derive(Clone, PartialEq, Eq, Hash, Debug, Sbor)] +/// pub VersionedFoo(FooVersions) => Foo = FooV1, +/// outer_attributes: [ +/// #[sbor(type_name = "MyVersionedFoo")] +/// ], +/// inner_attributes: [ +/// #[sbor(type_name = "MyFooVersions")] +/// ], +/// } +/// ``` #[macro_export] macro_rules! define_single_versioned { ( $(#[$attributes:meta])* - $vis:vis $versioned_name:ident($versions_name:ident) + $vis:vis $versioned_name:ident( + $versions_name:ident + ) $(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? $( = $deflt:tt)? ),+ >)? => $latest_version_alias:ty = $latest_version_type:ty + $(, outer_attributes: [ + $(#[$outer_attributes:meta])* + ])? + $(, inner_attributes: [ + $(#[$inner_attributes:meta])* + ])? + $(,)? ) => { $crate::define_versioned!( $(#[$attributes])* @@ -154,6 +186,12 @@ macro_rules! define_single_versioned { 1 => $latest_version_alias = $latest_version_type }, } + $(, outer_attributes: [ + $(#[$outer_attributes])* + ])? + $(, inner_attributes: [ + $(#[$inner_attributes])* + ])? ); $crate::paste::paste! { @@ -197,7 +235,8 @@ macro_rules! define_single_versioned { /// In the future, this may become a programmatic macro to support better error handling / /// edge case detection, and opting into more explicit SBOR handling. /// -/// Example usage: +/// ## Example usage +/// /// ```rust /// use sbor::prelude::*; /// @@ -247,6 +286,30 @@ macro_rules! define_single_versioned { /// // After a call to `a.in_place_fully_update_and_as_latest_version_mut()`, `a` has now been updated: /// assert_eq!(a, b); /// ``` +/// +/// ## Advanced attribute handling +/// +/// The provided attributes get applied to _both_ the outer "Versioned" type, +/// and the inner "Versions" type. To only apply to one type, you can include the +/// `outer_attributes` optional argument and/or the `inner_attributes` optional argument: +/// ``` +/// define_versioned! { +/// #[derive(Debug, Clone, PartialEq, Eq, Sbor)] +/// VersionedFoo(FooVersions) { +/// previous_versions: [ +/// 1 => FooV1: { updates_to: 2 }, +/// ], +/// latest_version: { +/// 2 => Foo = FooV2, +/// }, +/// } +/// outer_attributes: [ +/// #[sbor(type_name = "MyVersionedFoo")] +/// ], +/// inner_attributes: [ +/// #[sbor(type_name = "MyFooVersions")] +/// ], +/// } #[macro_export] macro_rules! define_versioned { ( @@ -268,6 +331,13 @@ macro_rules! define_versioned { } $(,)? // Optional trailing comma } + $(, outer_attributes: [ + $(#[$outer_attributes:meta])* + ])? + $(, inner_attributes: [ + $(#[$inner_attributes:meta])* + ])? + $(,)? ) => { $crate::eager_replace! { [!SET! #FullGenerics = $(< $( $lt $( : $clt $(+ $dlt )* )? $( = $deflt)? ),+ >)?] @@ -286,6 +356,7 @@ macro_rules! define_versioned { #[derive(#PermitSborAttributesAlias)] $(#[$attributes])* + $($(#[$outer_attributes])*)? // Needs to go below $attributes so that a #[derive(Sbor)] in the attributes can see it. #[sbor(as_type = [!stringify! #VersionsType])] /// If you wish to get access to match on the versions, use `.as_ref()` or `.as_mut()`. @@ -396,6 +467,7 @@ macro_rules! define_versioned { #[derive(#PermitSborAttributesAlias)] $(#[$attributes])* + $($(#[$inner_attributes])*)? $vis enum $versions_name #FullGenerics { $($( From d5c5b326ca5487656aac9ef6930af05df7755893 Mon Sep 17 00:00:00 2001 From: David Edey Date: Wed, 3 Jul 2024 18:32:16 +0100 Subject: [PATCH 14/19] doc: Slight fix to rust doc --- sbor/src/versioned.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sbor/src/versioned.rs b/sbor/src/versioned.rs index 7da03999252..3a80ac7067b 100644 --- a/sbor/src/versioned.rs +++ b/sbor/src/versioned.rs @@ -124,11 +124,7 @@ pub trait UniqueVersioned: Versioned { /// /// define_single_versioned! { /// #[derive(Clone, PartialEq, Eq, Hash, Debug, Sbor)] -/// pub VersionedFoo(FooVersions) => Foo = FooV1, -/// outer_attributes: [ -/// ], -/// inner_attributes: [ -/// ], +/// pub VersionedFoo(FooVersions) => Foo = FooV1 /// } /// /// // `Foo` is created as an alias for `FooV1` From 02dd7be33f17293a9a5701eb2e670558ae19362a Mon Sep 17 00:00:00 2001 From: David Edey Date: Thu, 11 Jul 2024 09:54:42 +0100 Subject: [PATCH 15/19] fix: Make SystemError::SystemPanic not conditionally compiled --- radix-engine/src/errors.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/radix-engine/src/errors.rs b/radix-engine/src/errors.rs index a7f460b385a..cedb8c52b7f 100644 --- a/radix-engine/src/errors.rs +++ b/radix-engine/src/errors.rs @@ -279,7 +279,8 @@ pub enum SystemError { /// A panic that's occurred in the system-layer or below. We're calling it system panic since /// we're treating the system as a black-box here. - #[cfg(feature = "std")] + /// + /// Note that this is only used when feature std is used. SystemPanic(String), } From 67674c7247f4f3f7bb1f2865bb926fc03818a140 Mon Sep 17 00:00:00 2001 From: David Edey Date: Thu, 11 Jul 2024 12:40:30 +0100 Subject: [PATCH 16/19] tweak: Include leaf context (e.g. variant names) on schema comparison errors --- sbor/src/schema/schema_comparison.rs | 324 +++++++++++++++++----- sbor/src/traversal/path_formatting.rs | 285 +++++++++++-------- sbor/src/traversal/typed/full_location.rs | 4 +- 3 files changed, 421 insertions(+), 192 deletions(-) diff --git a/sbor/src/schema/schema_comparison.rs b/sbor/src/schema/schema_comparison.rs index a8c813992b4..52f035dcc84 100644 --- a/sbor/src/schema/schema_comparison.rs +++ b/sbor/src/schema/schema_comparison.rs @@ -5,6 +5,7 @@ use crate::schema::*; use crate::traversal::AnnotatedSborAncestor; use crate::traversal::AnnotatedSborAncestorContainer; use crate::traversal::AnnotatedSborPartialLeaf; +use crate::traversal::AnnotatedSborPartialLeafLocator; use crate::traversal::MapEntryPart; use crate::traversal::PathAnnotate; use crate::BASIC_SBOR_V1_MAX_DEPTH; @@ -367,12 +368,22 @@ impl SchemaComparisonError { compared_schema: &Schema, ) -> core::fmt::Result { if let Some(location) = &self.example_location { - write!( + let (base_type_kind, base_metadata, _) = base_schema + .resolve_type_data(location.leaf_base_type_id) + .expect("Invalid base schema - Could not find data for base type"); + let (compared_type_kind, compared_metadata, _) = compared_schema + .resolve_type_data(location.leaf_compared_type_id) + .expect("Invalid compared schema - Could not find data for compared type"); + + self.error_detail.write_with_context( f, - "{:?} under {} at type path ", - &self.error_detail, location.root_type_identifier, + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, )?; - (location, base_schema, compared_schema).write_path(f)?; + write!(f, " under {} at path ", location.root_type_identifier)?; + (location, base_schema, compared_schema, &self.error_detail).write_path(f)?; } else { write!(f, "{:?}", &self.error_detail)?; } @@ -380,7 +391,7 @@ impl SchemaComparisonError { } } -fn combine_names(base_name: Option<&str>, compared_name: Option<&str>) -> Option { +fn combine_optional_names(base_name: Option<&str>, compared_name: Option<&str>) -> Option { match (base_name, compared_name) { (Some(base_name), Some(compared_name)) if base_name == compared_name => { Some(base_name.to_string()) @@ -392,42 +403,58 @@ fn combine_names(base_name: Option<&str>, compared_name: Option<&str>) -> Option } } -impl<'s, 'a, S: CustomSchema> PathAnnotate for (&'a TypeFullPath, &'a Schema, &'a Schema) { +fn combine_type_names( + base_metadata: &TypeMetadata, + base_type_kind: &LocalTypeKind, + compared_metadata: &TypeMetadata, + compared_type_kind: &LocalTypeKind, +) -> String { + if let Some(combined_name) = + combine_optional_names(base_metadata.get_name(), compared_metadata.get_name()) + { + return combined_name; + } + let base_category_name = base_type_kind.category_name(); + let compared_category_name = compared_type_kind.category_name(); + if base_category_name == compared_category_name { + base_category_name.to_string() + } else { + format!("{base_category_name}|{compared_category_name}") + } +} + +impl<'s, 'a, S: CustomSchema> PathAnnotate + for ( + &'a TypeFullPath, + &'a Schema, + &'a Schema, + &'a SchemaComparisonErrorDetail, + ) +{ fn iter_ancestor_path(&self) -> Box> + '_> { - let (full_path, base_schema, compared_schema) = *self; + let (full_path, base_schema, compared_schema, _error_detail) = *self; let iterator = full_path.ancestor_path.iter().map(|path_segment| { - let base_metadata = base_schema - .resolve_type_metadata(path_segment.parent_base_type_id) - .expect("Invalid base schema - Could not find metadata for base type"); - let compared_metadata = compared_schema - .resolve_type_metadata(path_segment.parent_compared_type_id) - .expect("Invalid compared schema - Could not find metadata for compared type"); - - let name = Cow::Owned( - combine_names(base_metadata.get_name(), compared_metadata.get_name()) - .unwrap_or_else(|| { - combine_names( - Some( - base_schema - .resolve_type_kind(path_segment.parent_base_type_id) - .unwrap() - .category_name(), - ), - Some( - compared_schema - .resolve_type_kind(path_segment.parent_compared_type_id) - .unwrap() - .category_name(), - ), - ) - .unwrap() - }), - ); + let base_type_id = path_segment.parent_base_type_id; + let compared_type_id = path_segment.parent_compared_type_id; + + let (base_type_kind, base_metadata, _) = base_schema + .resolve_type_data(base_type_id) + .expect("Invalid base schema - Could not find data for base type"); + let (compared_type_kind, compared_metadata, _) = compared_schema + .resolve_type_data(compared_type_id) + .expect("Invalid compared schema - Could not find data for compared type"); + + let name = Cow::Owned(combine_type_names::( + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, + )); let container = match path_segment.child_locator { ChildTypeLocator::Tuple { field_index } => { - let field_name = combine_names( + let field_name = combine_optional_names( base_metadata.get_field_name(field_index), compared_metadata.get_field_name(field_index), ) @@ -447,12 +474,12 @@ impl<'s, 'a, S: CustomSchema> PathAnnotate for (&'a TypeFullPath, &'a Schema, let compared_variant_metadata = compared_metadata .get_enum_variant_data(discriminator) .expect("Compared schema has variant names"); - let variant_name = combine_names( + let variant_name = combine_optional_names( base_variant_metadata.get_name(), compared_variant_metadata.get_name(), ) .map(Cow::Owned); - let field_name = combine_names( + let field_name = combine_optional_names( base_variant_metadata.get_field_name(field_index), compared_variant_metadata.get_field_name(field_index), ) @@ -478,43 +505,23 @@ impl<'s, 'a, S: CustomSchema> PathAnnotate for (&'a TypeFullPath, &'a Schema, } fn annotated_leaf(&self) -> Option> { - let (full_path, base_schema, compared_schema) = *self; + let (full_path, base_schema, compared_schema, error_detail) = *self; let base_type_id = full_path.leaf_base_type_id; let compared_type_id = full_path.leaf_compared_type_id; - let base_metadata = base_schema - .resolve_type_metadata(base_type_id) - .expect("Invalid base schema - Could not find metadata for base type"); - let compared_metadata = compared_schema - .resolve_type_metadata(compared_type_id) - .expect("Invalid compared schema - Could not find metadata for compared type"); - - let name = Cow::Owned( - combine_names(base_metadata.get_name(), compared_metadata.get_name()).unwrap_or_else( - || { - combine_names( - Some( - base_schema - .resolve_type_kind(base_type_id) - .unwrap() - .category_name(), - ), - Some( - compared_schema - .resolve_type_kind(compared_type_id) - .unwrap() - .category_name(), - ), - ) - .unwrap() - }, - ), - ); + let (base_type_kind, base_metadata, _) = base_schema + .resolve_type_data(base_type_id) + .expect("Invalid base schema - Could not find data for base type"); + let (compared_type_kind, compared_metadata, _) = compared_schema + .resolve_type_data(compared_type_id) + .expect("Invalid compared schema - Could not find data for compared type"); - Some(AnnotatedSborPartialLeaf { - name, - partial_leaf_locator: None, - }) + Some(error_detail.resolve_annotated_leaf( + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, + )) } } @@ -597,6 +604,183 @@ pub enum SchemaComparisonErrorDetail { }, } +impl SchemaComparisonErrorDetail { + fn resolve_annotated_leaf( + &self, + base_metadata: &TypeMetadata, + base_type_kind: &LocalTypeKind, + compared_metadata: &TypeMetadata, + compared_type_kind: &LocalTypeKind, + ) -> AnnotatedSborPartialLeaf<'_> { + AnnotatedSborPartialLeaf { + name: Cow::Owned(combine_type_names::( + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, + )), + partial_leaf_locator: self + .resolve_partial_leaf_locator(base_metadata, compared_metadata), + } + } + + fn resolve_partial_leaf_locator( + &self, + base_metadata: &TypeMetadata, + compared_metadata: &TypeMetadata, + ) -> Option> { + match *self { + SchemaComparisonErrorDetail::TypeKindMismatch { .. } => None, + SchemaComparisonErrorDetail::TupleFieldCountMismatch { .. } => None, + SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { .. } => { + // This error handles multiple variants, so we can't list them here - instead we handle it in the custom debug print + None + } + SchemaComparisonErrorDetail::EnumVariantFieldCountMismatch { + variant_discriminator, + .. + } => { + let base_variant = base_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid base schema - Could not find metadata for enum variant"); + let compared_variant = compared_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid compared schema - Could not find metadata for enum variant"); + Some(AnnotatedSborPartialLeafLocator::EnumVariant { + variant_discriminator: Some(variant_discriminator), + variant_name: combine_optional_names( + base_variant.get_name(), + compared_variant.get_name(), + ) + .map(Cow::Owned), + field_index: None, + field_name: None, + }) + } + SchemaComparisonErrorDetail::TypeNameChangeError(_) => None, + SchemaComparisonErrorDetail::FieldNameChangeError { field_index, .. } => { + let base_field_name = base_metadata.get_field_name(field_index); + let compared_field_name = compared_metadata.get_field_name(field_index); + Some(AnnotatedSborPartialLeafLocator::Tuple { + field_index: Some(field_index), + field_name: combine_optional_names(base_field_name, compared_field_name) + .map(Cow::Owned), + }) + } + SchemaComparisonErrorDetail::EnumVariantNameChangeError { + variant_discriminator, + .. + } => { + let base_variant = base_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid base schema - Could not find metadata for enum variant"); + let compared_variant = compared_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid compared schema - Could not find metadata for enum variant"); + Some(AnnotatedSborPartialLeafLocator::EnumVariant { + variant_discriminator: Some(variant_discriminator), + variant_name: combine_optional_names( + base_variant.get_name(), + compared_variant.get_name(), + ) + .map(Cow::Owned), + field_index: None, + field_name: None, + }) + } + SchemaComparisonErrorDetail::EnumVariantFieldNameChangeError { + variant_discriminator, + field_index, + .. + } => { + let base_variant = base_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid base schema - Could not find metadata for enum variant"); + let compared_variant = compared_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid compared schema - Could not find metadata for enum variant"); + let base_field_name = base_variant.get_field_name(field_index); + let compared_field_name = compared_variant.get_field_name(field_index); + Some(AnnotatedSborPartialLeafLocator::EnumVariant { + variant_discriminator: Some(variant_discriminator), + variant_name: combine_optional_names( + base_variant.get_name(), + compared_metadata.get_name(), + ) + .map(Cow::Owned), + field_index: Some(field_index), + field_name: combine_optional_names(base_field_name, compared_field_name) + .map(Cow::Owned), + }) + } + SchemaComparisonErrorDetail::TypeValidationChangeError { .. } => None, + SchemaComparisonErrorDetail::NamedRootTypeMissingInComparedSchema { .. } => None, + SchemaComparisonErrorDetail::DisallowedNewRootTypeInComparedSchema { .. } => None, + SchemaComparisonErrorDetail::TypeUnreachableFromRootInBaseSchema { .. } => None, + SchemaComparisonErrorDetail::TypeUnreachableFromRootInComparedSchema { .. } => None, + } + } + + fn write_with_context( + &self, + f: &mut F, + base_metadata: &TypeMetadata, + base_type_kind: &LocalTypeKind, + compared_metadata: &TypeMetadata, + compared_type_kind: &LocalTypeKind, + ) -> core::fmt::Result { + self.resolve_annotated_leaf( + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, + ) + .write(f, true)?; + write!(f, " - ")?; + + match self { + // Handle any errors where we can add extra detail + SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { + base_variants_missing_in_compared, + compared_variants_missing_in_base, + } => { + write!( + f, + "EnumSupportedVariantsMismatch {{ base_variants_missing_in_compared: {{" + )?; + for variant_discriminator in base_variants_missing_in_compared { + let variant_data = base_metadata + .get_enum_variant_data(*variant_discriminator) + .unwrap(); + write!( + f, + "{variant_discriminator}|{}", + variant_data.get_name().unwrap_or("anon") + )?; + } + write!(f, "}}, compared_variants_missing_in_base: {{")?; + for variant_discriminator in compared_variants_missing_in_base { + let variant_data = compared_metadata + .get_enum_variant_data(*variant_discriminator) + .unwrap(); + write!( + f, + "{variant_discriminator}|{}", + variant_data.get_name().unwrap_or("anon") + )?; + } + write!(f, "}}, }}")?; + } + // All other errors already have their context added in printing the annotated leaf + _ => { + write!(f, "{self:?}")?; + } + } + + Ok(()) + } +} + struct TypeKindComparisonResult { children_needing_checking: Vec<(ChildTypeLocator, LocalTypeId, LocalTypeId)>, errors: Vec>, diff --git a/sbor/src/traversal/path_formatting.rs b/sbor/src/traversal/path_formatting.rs index e75c3fb8e37..644560a5683 100644 --- a/sbor/src/traversal/path_formatting.rs +++ b/sbor/src/traversal/path_formatting.rs @@ -8,6 +8,25 @@ pub struct AnnotatedSborAncestor<'a> { pub container: AnnotatedSborAncestorContainer<'a>, } +impl<'a> AnnotatedSborAncestor<'a> { + pub fn write( + &self, + f: &mut impl core::fmt::Write, + is_start_of_path: bool, + ) -> core::fmt::Result { + let AnnotatedSborAncestor { name, container } = self; + + if !is_start_of_path { + write!(f, "->")?; + } + + write!(f, "{name}")?; + container.write(f)?; + + Ok(()) + } +} + pub enum AnnotatedSborAncestorContainer<'a> { Tuple { field_index: usize, @@ -30,22 +49,96 @@ pub enum AnnotatedSborAncestorContainer<'a> { }, } +impl<'a> AnnotatedSborAncestorContainer<'a> { + pub fn write(&self, f: &mut impl core::fmt::Write) -> core::fmt::Result { + // This should align with AnnotatedSborPartialLeafLocator + match self { + Self::Tuple { + field_index, + field_name, + } => { + if let Some(field_name) = field_name { + write!(f, ".[{field_index}|{field_name}]")?; + } else { + write!(f, ".[{field_index}]")?; + } + } + Self::EnumVariant { + discriminator: variant_discriminator, + variant_name, + field_index, + field_name, + } => { + if let Some(variant_name) = variant_name { + write!(f, "::{{{variant_discriminator}|{variant_name}}}")?; + } else { + write!(f, "::{{{variant_discriminator}}}")?; + } + if let Some(field_name) = field_name { + write!(f, ".[{field_index}|{field_name}]")?; + } else { + write!(f, ".[{field_index}]")?; + } + } + Self::Array { index } => { + if let Some(index) = index { + write!(f, ".[{index}]")?; + } + } + Self::Map { index, entry_part } => { + if let Some(index) = index { + write!(f, ".[{index}]")?; + } + match entry_part { + MapEntryPart::Key => write!(f, ".Key")?, + MapEntryPart::Value => write!(f, ".Value")?, + } + } + } + Ok(()) + } +} + pub struct AnnotatedSborPartialLeaf<'a> { /// Ideally the type's type name, else the value kind name or type name, depending on what's available pub name: Cow<'a, str>, pub partial_leaf_locator: Option>, } +impl<'a> AnnotatedSborPartialLeaf<'a> { + pub fn write( + &self, + f: &mut impl core::fmt::Write, + is_start_of_path: bool, + ) -> core::fmt::Result { + let AnnotatedSborPartialLeaf { + name, + partial_leaf_locator: partial_kinded_data, + } = self; + + if !is_start_of_path { + write!(f, "->")?; + } + + write!(f, "{}", name.deref())?; + if let Some(partial_kinded_data) = partial_kinded_data { + partial_kinded_data.write(f)?; + } + + Ok(()) + } +} + pub enum AnnotatedSborPartialLeafLocator<'a> { Tuple { - field_offset: Option, - field_name: Option<&'a str>, + field_index: Option, + field_name: Option>, }, EnumVariant { variant_discriminator: Option, - variant_name: Option<&'a str>, - field_offset: Option, - field_name: Option<&'a str>, + variant_name: Option>, + field_index: Option, + field_name: Option>, }, Array { index: Option, @@ -56,6 +149,64 @@ pub enum AnnotatedSborPartialLeafLocator<'a> { }, } +impl<'a> AnnotatedSborPartialLeafLocator<'a> { + pub fn write(&self, f: &mut impl core::fmt::Write) -> core::fmt::Result { + // This should align with AnnotatedSborAncestorContainer + match self { + Self::Tuple { + field_index, + field_name, + } => { + if let Some(field_index) = field_index { + if let Some(field_name) = field_name { + write!(f, ".[{field_index}|{field_name}]")?; + } else { + write!(f, ".[{field_index}]")?; + } + } + } + Self::EnumVariant { + variant_discriminator, + variant_name, + field_index, + field_name, + } => { + if let Some(variant_discriminator) = variant_discriminator { + if let Some(variant_name) = variant_name { + write!(f, "::{{{variant_discriminator}|{variant_name}}}")?; + } else { + write!(f, "::{{{variant_discriminator}}}")?; + } + if let Some(field_index) = field_index { + if let Some(field_name) = field_name { + write!(f, ".[{field_index}|{field_name}]")?; + } else { + write!(f, ".[{field_index}]")?; + } + } + } + } + Self::Array { index } => { + if let Some(index) = index { + write!(f, ".[{index}]")?; + } + } + Self::Map { index, entry_part } => { + if let Some(index) = index { + write!(f, ".[{index}]")?; + } + if let Some(entry_part) = entry_part { + match entry_part { + MapEntryPart::Key => write!(f, ".Key")?, + MapEntryPart::Value => write!(f, ".Value")?, + } + } + } + } + Ok(()) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum MapEntryPart { Key, @@ -73,123 +224,17 @@ pub trait PathAnnotate { buf } - fn write_path(&self, buf: &mut impl core::fmt::Write) -> core::fmt::Result { - let mut is_first = true; - for AnnotatedSborAncestor { name, container } in self.iter_ancestor_path() { - if is_first { - is_first = false; - } else { - write!(buf, "->")?; - } - write!(buf, "{name}")?; - match container { - AnnotatedSborAncestorContainer::Tuple { - field_index: field_offset, - field_name, - } => { - if let Some(field_name) = field_name { - write!(buf, ".[{field_offset}|{field_name}]")?; - } else { - write!(buf, ".[{field_offset}]")?; - } - } - AnnotatedSborAncestorContainer::EnumVariant { - discriminator: variant_discriminator, - variant_name, - field_index: field_offset, - field_name, - } => { - if let Some(variant_name) = variant_name { - write!(buf, "::{{{variant_discriminator}|{variant_name}}}")?; - } else { - write!(buf, "::{{{variant_discriminator}}}")?; - } - if let Some(field_name) = field_name { - write!(buf, ".[{field_offset}|{field_name}]")?; - } else { - write!(buf, ".[{field_offset}]")?; - } - } - AnnotatedSborAncestorContainer::Array { index } => { - if let Some(index) = index { - write!(buf, ".[{index}]")?; - } - } - AnnotatedSborAncestorContainer::Map { index, entry_part } => { - if let Some(index) = index { - write!(buf, ".[{index}]")?; - } - match entry_part { - MapEntryPart::Key => write!(buf, ".Key")?, - MapEntryPart::Value => write!(buf, ".Value")?, - } - } - } - } - if let Some(AnnotatedSborPartialLeaf { - name, - partial_leaf_locator: partial_kinded_data, - }) = self.annotated_leaf() - { - if !is_first { - write!(buf, "->")?; - } - write!(buf, "{}", name.deref())?; - if let Some(partial_kinded_data) = partial_kinded_data { - match partial_kinded_data { - AnnotatedSborPartialLeafLocator::Tuple { - field_offset, - field_name, - } => { - if let Some(field_offset) = field_offset { - if let Some(field_name) = field_name { - write!(buf, ".[{field_offset}|{field_name}]")?; - } else { - write!(buf, ".[{field_offset}]")?; - } - } - } - AnnotatedSborPartialLeafLocator::EnumVariant { - variant_discriminator, - variant_name, - field_offset, - field_name, - } => { - if let Some(variant_discriminator) = variant_discriminator { - if let Some(variant_name) = variant_name { - write!(buf, "::{{{variant_discriminator}|{variant_name}}}")?; - } else { - write!(buf, "::{{{variant_discriminator}}}")?; - } - if let Some(field_offset) = field_offset { - if let Some(field_name) = field_name { - write!(buf, ".[{field_offset}|{field_name}]")?; - } else { - write!(buf, ".[{field_offset}]")?; - } - } - } - } - AnnotatedSborPartialLeafLocator::Array { index } => { - if let Some(index) = index { - write!(buf, ".[{index}]")?; - } - } - AnnotatedSborPartialLeafLocator::Map { index, entry_part } => { - if let Some(index) = index { - write!(buf, ".[{index}]")?; - } - if let Some(entry_part) = entry_part { - match entry_part { - MapEntryPart::Key => write!(buf, ".Key")?, - MapEntryPart::Value => write!(buf, ".Value")?, - } - } - } - } - } + fn write_path(&self, f: &mut impl core::fmt::Write) -> core::fmt::Result { + let mut is_start_of_path = true; + for ancestor in self.iter_ancestor_path() { + ancestor.write(f, is_start_of_path)?; + is_start_of_path = false; } + if let Some(leaf) = self.annotated_leaf() { + leaf.write(f, is_start_of_path)?; + }; + Ok(()) } } diff --git a/sbor/src/traversal/typed/full_location.rs b/sbor/src/traversal/typed/full_location.rs index e19ec2719d0..1f5f9fe74c4 100644 --- a/sbor/src/traversal/typed/full_location.rs +++ b/sbor/src/traversal/typed/full_location.rs @@ -98,12 +98,12 @@ impl<'s, 'a, E: CustomExtension> PathAnnotate if let Some(variant_discriminator) = current_value_info.variant { let variant_data = metadata.and_then(|v| v.get_enum_variant_data(variant_discriminator)); - let variant_name = variant_data.and_then(|d| d.get_name()); + let variant_name = variant_data.and_then(|d| d.get_name()).map(Cow::Borrowed); Some(AnnotatedSborPartialLeafLocator::EnumVariant { variant_discriminator: Some(variant_discriminator), variant_name, - field_offset: None, + field_index: None, field_name: None, }) } else { From 0a9d4987fc8a707a36890a3f01a403f1afd55c76 Mon Sep 17 00:00:00 2001 From: David Edey Date: Wed, 7 Aug 2024 14:53:15 +0100 Subject: [PATCH 17/19] refactor: Split schema comparison into separate files --- sbor-tests/tests/schema_comparison.rs | 6 +- sbor/src/schema/schema.rs | 49 + sbor/src/schema/schema_comparison.rs | 2231 ----------------- .../schema_comparison/comparable_schema.rs | 165 ++ .../comparisons_and_assertions.rs | 213 ++ sbor/src/schema/schema_comparison/mod.rs | 25 + .../schema_comparison_kernel.rs | 1043 ++++++++ .../schema_comparison_result.rs | 557 ++++ .../schema_comparison_settings.rs | 194 ++ sbor/src/schema/type_aggregator.rs | 2 +- 10 files changed, 2248 insertions(+), 2237 deletions(-) delete mode 100644 sbor/src/schema/schema_comparison.rs create mode 100644 sbor/src/schema/schema_comparison/comparable_schema.rs create mode 100644 sbor/src/schema/schema_comparison/comparisons_and_assertions.rs create mode 100644 sbor/src/schema/schema_comparison/mod.rs create mode 100644 sbor/src/schema/schema_comparison/schema_comparison_kernel.rs create mode 100644 sbor/src/schema/schema_comparison/schema_comparison_result.rs create mode 100644 sbor/src/schema/schema_comparison/schema_comparison_settings.rs diff --git a/sbor-tests/tests/schema_comparison.rs b/sbor-tests/tests/schema_comparison.rs index 9e7ad474169..981c3517872 100644 --- a/sbor-tests/tests/schema_comparison.rs +++ b/sbor-tests/tests/schema_comparison.rs @@ -1,6 +1,6 @@ use sbor::prelude::*; use sbor::schema::*; -use sbor::{BasicTypeAggregator, BasicValue, ComparableSchema, NoCustomSchema, NoCustomTypeKind}; +use sbor::{BasicTypeAggregator, BasicValue, NoCustomSchema, NoCustomTypeKind}; //===================== // HELPER CODE / TRAITS @@ -9,10 +9,6 @@ trait DerivableTypeSchema: Describe { fn single_type_schema_version() -> SingleTypeSchema { SingleTypeSchema::for_type::() } - - fn single_type_schema_version_hex() -> String { - Self::single_type_schema_version().encode_to_hex() - } } impl> DerivableTypeSchema for T {} diff --git a/sbor/src/schema/schema.rs b/sbor/src/schema/schema.rs index 59250f5b7f8..61210199325 100644 --- a/sbor/src/schema/schema.rs +++ b/sbor/src/schema/schema.rs @@ -29,6 +29,55 @@ impl Default for VersionedSchema { } } +/// A serializable record of the schema of a single type. +/// Intended for historical backwards compatibility checking of a single type. +#[derive(Debug, Clone, Sbor)] +#[sbor(child_types = "S::CustomLocalTypeKind, S::CustomTypeValidation")] +pub struct SingleTypeSchema { + pub schema: VersionedSchema, + pub type_id: LocalTypeId, +} + +impl SingleTypeSchema { + pub fn new(schema: VersionedSchema, type_id: LocalTypeId) -> Self { + Self { schema, type_id } + } + + pub fn from>(from: T) -> Self { + from.into_schema() + } + + pub fn for_type + ?Sized>() -> Self { + generate_single_type_schema::() + } +} + +/// A serializable record of the schema of a set of named types. +/// Intended for historical backwards compatibility of a collection +/// of types in a single schema. +/// +/// For example, traits, or blueprint interfaces. +#[derive(Debug, Clone, Sbor)] +#[sbor(child_types = "S::CustomLocalTypeKind, S::CustomTypeValidation")] +pub struct TypeCollectionSchema { + pub schema: VersionedSchema, + pub type_ids: IndexMap, +} + +impl TypeCollectionSchema { + pub fn new(schema: VersionedSchema, type_ids: IndexMap) -> Self { + Self { schema, type_ids } + } + + pub fn from>(from: &T) -> Self { + from.into_schema() + } + + pub fn from_aggregator(aggregator: TypeAggregator) -> Self { + aggregator.generate_type_collection_schema::() + } +} + /// An array of custom type kinds, and associated extra information which can attach to the type kinds #[derive(Debug, Clone, PartialEq, Eq, Sbor)] // NB - the generic parameter E isn't embedded in the value model itself - instead: diff --git a/sbor/src/schema/schema_comparison.rs b/sbor/src/schema/schema_comparison.rs deleted file mode 100644 index 52f035dcc84..00000000000 --- a/sbor/src/schema/schema_comparison.rs +++ /dev/null @@ -1,2231 +0,0 @@ -use basic_well_known_types::ANY_TYPE; - -use crate::internal_prelude::*; -use crate::schema::*; -use crate::traversal::AnnotatedSborAncestor; -use crate::traversal::AnnotatedSborAncestorContainer; -use crate::traversal::AnnotatedSborPartialLeaf; -use crate::traversal::AnnotatedSborPartialLeafLocator; -use crate::traversal::MapEntryPart; -use crate::traversal::PathAnnotate; -use crate::BASIC_SBOR_V1_MAX_DEPTH; -use radix_rust::rust::fmt::Write; - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct SchemaComparisonSettings { - completeness: SchemaComparisonCompletenessSettings, - structure: SchemaComparisonStructureSettings, - metadata: SchemaComparisonMetadataSettings, - validation: SchemaComparisonValidationSettings, -} - -impl SchemaComparisonSettings { - /// A set of defaults intended to enforce effective equality of the schemas, - /// but with clear error messages if they diverge - pub const fn require_equality() -> Self { - Self { - completeness: SchemaComparisonCompletenessSettings::enforce_type_roots_cover_schema_disallow_new_root_types(), - structure: SchemaComparisonStructureSettings::equality(), - metadata: SchemaComparisonMetadataSettings::equality(), - validation: SchemaComparisonValidationSettings::equality(), - } - } - - /// A set of defaults intended to capture a pretty tight definition of structural extension. - /// - /// This captures that: - /// * Payloads which are valid/decodable against the old schema are valid against the new schema - /// * Programmatic SBOR JSON is unchanged (that is, type/field/variant names are also unchanged) - /// - /// Notably: - /// * Type roots can be added in the compared schema, but we check that the type roots - /// provided completely cover both schemas - /// * Types must be structurally identical on their intersection, except new enum variants can be added - /// * Type metadata (e.g. names) must be identical on their intersection - /// * Type validation must be equal or strictly weaker in the new schema - pub const fn allow_extension() -> Self { - Self { - completeness: SchemaComparisonCompletenessSettings::enforce_type_roots_cover_schema_allow_new_root_types(), - structure: SchemaComparisonStructureSettings::allow_extension(), - metadata: SchemaComparisonMetadataSettings::equality(), - validation: SchemaComparisonValidationSettings::allow_weakening(), - } - } - - pub const fn completeness_settings( - mut self, - checks: SchemaComparisonCompletenessSettings, - ) -> Self { - self.completeness = checks; - self - } - - pub const fn structure_settings(mut self, checks: SchemaComparisonStructureSettings) -> Self { - self.structure = checks; - self - } - - pub const fn metadata_settings(mut self, checks: SchemaComparisonMetadataSettings) -> Self { - self.metadata = checks; - self - } - - pub const fn validation_settings(mut self, checks: SchemaComparisonValidationSettings) -> Self { - self.validation = checks; - self - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] -pub struct SchemaComparisonCompletenessSettings { - allow_root_unreachable_types_in_base_schema: bool, - allow_root_unreachable_types_in_compared_schema: bool, - /// This is only relevant in the "multiple named roots" mode - allow_compared_to_have_more_root_types: bool, -} - -impl SchemaComparisonCompletenessSettings { - pub const fn allow_type_roots_not_to_cover_schema() -> Self { - Self { - allow_root_unreachable_types_in_base_schema: true, - allow_root_unreachable_types_in_compared_schema: true, - allow_compared_to_have_more_root_types: true, - } - } - - pub const fn enforce_type_roots_cover_schema_allow_new_root_types() -> Self { - Self { - allow_root_unreachable_types_in_base_schema: false, - allow_root_unreachable_types_in_compared_schema: false, - allow_compared_to_have_more_root_types: true, - } - } - - pub const fn enforce_type_roots_cover_schema_disallow_new_root_types() -> Self { - Self { - allow_root_unreachable_types_in_base_schema: false, - allow_root_unreachable_types_in_compared_schema: false, - allow_compared_to_have_more_root_types: false, - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] -pub struct SchemaComparisonStructureSettings { - allow_new_enum_variants: bool, - allow_replacing_with_any: bool, -} - -impl SchemaComparisonStructureSettings { - pub const fn equality() -> Self { - Self { - allow_new_enum_variants: false, - allow_replacing_with_any: false, - } - } - - pub const fn allow_extension() -> Self { - Self { - allow_new_enum_variants: true, - allow_replacing_with_any: true, - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct SchemaComparisonMetadataSettings { - type_name_changes: NameChangeRule, - field_name_changes: NameChangeRule, - variant_name_changes: NameChangeRule, -} - -impl SchemaComparisonMetadataSettings { - pub const fn equality() -> Self { - Self { - type_name_changes: NameChangeRule::equality(), - field_name_changes: NameChangeRule::equality(), - variant_name_changes: NameChangeRule::equality(), - } - } - - pub const fn allow_adding_names() -> Self { - Self { - type_name_changes: NameChangeRule::AllowAddingNames, - field_name_changes: NameChangeRule::AllowAddingNames, - variant_name_changes: NameChangeRule::AllowAddingNames, - } - } - - pub const fn allow_all_changes() -> Self { - Self { - type_name_changes: NameChangeRule::AllowAllChanges, - field_name_changes: NameChangeRule::AllowAllChanges, - variant_name_changes: NameChangeRule::AllowAllChanges, - } - } - - fn checks_required(&self) -> bool { - let everything_allowed = self.type_name_changes == NameChangeRule::AllowAllChanges - && self.field_name_changes == NameChangeRule::AllowAllChanges - && self.variant_name_changes == NameChangeRule::AllowAllChanges; - !everything_allowed - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum NameChangeRule { - DisallowAllChanges, - AllowAddingNames, - AllowAllChanges, -} - -impl NameChangeRule { - pub const fn equality() -> Self { - Self::DisallowAllChanges - } -} - -pub enum NameChange<'a> { - Unchanged, - NameAdded { - new_name: &'a str, - }, - NameRemoved { - old_name: &'a str, - }, - NameChanged { - old_name: &'a str, - new_name: &'a str, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NameChangeError { - change: OwnedNameChange, - rule_broken: NameChangeRule, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum OwnedNameChange { - Unchanged, - NameAdded { new_name: String }, - NameRemoved { old_name: String }, - NameChanged { old_name: String, new_name: String }, -} - -impl<'a> NameChange<'a> { - pub fn of_changed_option(from: Option<&'a str>, to: Option<&'a str>) -> Self { - match (from, to) { - (Some(old_name), Some(new_name)) if old_name == new_name => NameChange::Unchanged, - (Some(old_name), Some(new_name)) => NameChange::NameChanged { old_name, new_name }, - (Some(old_name), None) => NameChange::NameRemoved { old_name }, - (None, Some(new_name)) => NameChange::NameAdded { new_name }, - (None, None) => NameChange::Unchanged, - } - } - - pub fn validate(&self, rule: NameChangeRule) -> Result<(), NameChangeError> { - let passes = match (self, rule) { - (NameChange::Unchanged, _) => true, - (_, NameChangeRule::AllowAllChanges) => true, - (_, NameChangeRule::DisallowAllChanges) => false, - (NameChange::NameAdded { .. }, NameChangeRule::AllowAddingNames) => true, - (NameChange::NameRemoved { .. }, NameChangeRule::AllowAddingNames) => false, - (NameChange::NameChanged { .. }, NameChangeRule::AllowAddingNames) => false, - }; - if passes { - Ok(()) - } else { - Err(NameChangeError { - rule_broken: rule, - change: self.into_owned(), - }) - } - } - - fn into_owned(&self) -> OwnedNameChange { - match *self { - NameChange::Unchanged => OwnedNameChange::Unchanged, - NameChange::NameAdded { new_name } => OwnedNameChange::NameAdded { - new_name: new_name.to_string(), - }, - NameChange::NameRemoved { old_name } => OwnedNameChange::NameRemoved { - old_name: old_name.to_string(), - }, - NameChange::NameChanged { old_name, new_name } => OwnedNameChange::NameChanged { - old_name: old_name.to_string(), - new_name: new_name.to_string(), - }, - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct SchemaComparisonValidationSettings { - allow_validation_weakening: bool, -} - -impl SchemaComparisonValidationSettings { - pub const fn equality() -> Self { - Self { - allow_validation_weakening: false, - } - } - - pub const fn allow_weakening() -> Self { - Self { - allow_validation_weakening: true, - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum ValidationChange { - Unchanged, - Strengthened, - Weakened, - Incomparable, -} - -impl ValidationChange { - pub fn combine(self, other: ValidationChange) -> Self { - match (self, other) { - (ValidationChange::Incomparable, _) => ValidationChange::Incomparable, - (_, ValidationChange::Incomparable) => ValidationChange::Incomparable, - (ValidationChange::Unchanged, other) => other, - (other, ValidationChange::Unchanged) => other, - (ValidationChange::Strengthened, ValidationChange::Strengthened) => { - ValidationChange::Strengthened - } - (ValidationChange::Strengthened, ValidationChange::Weakened) => { - ValidationChange::Incomparable - } - (ValidationChange::Weakened, ValidationChange::Strengthened) => { - ValidationChange::Incomparable - } - (ValidationChange::Weakened, ValidationChange::Weakened) => ValidationChange::Weakened, - } - } -} - -#[must_use = "You must read / handle the comparison result"] -pub struct SchemaComparisonResult<'s, S: CustomSchema> { - base_schema: &'s Schema, - compared_schema: &'s Schema, - errors: Vec>, -} - -impl<'s, S: CustomSchema> SchemaComparisonResult<'s, S> { - pub fn is_valid(&self) -> bool { - self.errors.len() == 0 - } - - pub fn error_message( - &self, - base_schema_name: &str, - compared_schema_name: &str, - ) -> Option { - if self.errors.len() == 0 { - return None; - } - let mut output = String::new(); - writeln!( - &mut output, - "Schema comparison FAILED between base schema ({}) and compared schema ({}) with {} {}:", - base_schema_name, - compared_schema_name, - self.errors.len(), - if self.errors.len() == 1 { "error" } else { "errors" }, - ).unwrap(); - for error in &self.errors { - write!(&mut output, "- ").unwrap(); - error - .write_against_schemas(&mut output, &self.base_schema, &self.compared_schema) - .unwrap(); - writeln!(&mut output).unwrap(); - } - Some(output) - } - - pub fn assert_valid(&self, base_schema_name: &str, compared_schema_name: &str) { - if let Some(error_message) = self.error_message(base_schema_name, compared_schema_name) { - panic!("{}", error_message); - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SchemaComparisonError { - error_detail: SchemaComparisonErrorDetail, - example_location: Option, -} - -impl SchemaComparisonError { - fn write_against_schemas( - &self, - f: &mut F, - base_schema: &Schema, - compared_schema: &Schema, - ) -> core::fmt::Result { - if let Some(location) = &self.example_location { - let (base_type_kind, base_metadata, _) = base_schema - .resolve_type_data(location.leaf_base_type_id) - .expect("Invalid base schema - Could not find data for base type"); - let (compared_type_kind, compared_metadata, _) = compared_schema - .resolve_type_data(location.leaf_compared_type_id) - .expect("Invalid compared schema - Could not find data for compared type"); - - self.error_detail.write_with_context( - f, - base_metadata, - base_type_kind, - compared_metadata, - compared_type_kind, - )?; - write!(f, " under {} at path ", location.root_type_identifier)?; - (location, base_schema, compared_schema, &self.error_detail).write_path(f)?; - } else { - write!(f, "{:?}", &self.error_detail)?; - } - Ok(()) - } -} - -fn combine_optional_names(base_name: Option<&str>, compared_name: Option<&str>) -> Option { - match (base_name, compared_name) { - (Some(base_name), Some(compared_name)) if base_name == compared_name => { - Some(base_name.to_string()) - } - (Some(base_name), Some(compared_name)) => Some(format!("{base_name}|{compared_name}")), - (Some(base_name), None) => Some(format!("{base_name}|anon")), - (None, Some(compared_name)) => Some(format!("anon|{compared_name}")), - (None, None) => None, - } -} - -fn combine_type_names( - base_metadata: &TypeMetadata, - base_type_kind: &LocalTypeKind, - compared_metadata: &TypeMetadata, - compared_type_kind: &LocalTypeKind, -) -> String { - if let Some(combined_name) = - combine_optional_names(base_metadata.get_name(), compared_metadata.get_name()) - { - return combined_name; - } - let base_category_name = base_type_kind.category_name(); - let compared_category_name = compared_type_kind.category_name(); - if base_category_name == compared_category_name { - base_category_name.to_string() - } else { - format!("{base_category_name}|{compared_category_name}") - } -} - -impl<'s, 'a, S: CustomSchema> PathAnnotate - for ( - &'a TypeFullPath, - &'a Schema, - &'a Schema, - &'a SchemaComparisonErrorDetail, - ) -{ - fn iter_ancestor_path(&self) -> Box> + '_> { - let (full_path, base_schema, compared_schema, _error_detail) = *self; - - let iterator = full_path.ancestor_path.iter().map(|path_segment| { - let base_type_id = path_segment.parent_base_type_id; - let compared_type_id = path_segment.parent_compared_type_id; - - let (base_type_kind, base_metadata, _) = base_schema - .resolve_type_data(base_type_id) - .expect("Invalid base schema - Could not find data for base type"); - let (compared_type_kind, compared_metadata, _) = compared_schema - .resolve_type_data(compared_type_id) - .expect("Invalid compared schema - Could not find data for compared type"); - - let name = Cow::Owned(combine_type_names::( - base_metadata, - base_type_kind, - compared_metadata, - compared_type_kind, - )); - - let container = match path_segment.child_locator { - ChildTypeLocator::Tuple { field_index } => { - let field_name = combine_optional_names( - base_metadata.get_field_name(field_index), - compared_metadata.get_field_name(field_index), - ) - .map(Cow::Owned); - AnnotatedSborAncestorContainer::Tuple { - field_index, - field_name, - } - } - ChildTypeLocator::EnumVariant { - discriminator, - field_index, - } => { - let base_variant_metadata = base_metadata - .get_enum_variant_data(discriminator) - .expect("Base schema has variant names"); - let compared_variant_metadata = compared_metadata - .get_enum_variant_data(discriminator) - .expect("Compared schema has variant names"); - let variant_name = combine_optional_names( - base_variant_metadata.get_name(), - compared_variant_metadata.get_name(), - ) - .map(Cow::Owned); - let field_name = combine_optional_names( - base_variant_metadata.get_field_name(field_index), - compared_variant_metadata.get_field_name(field_index), - ) - .map(Cow::Owned); - AnnotatedSborAncestorContainer::EnumVariant { - discriminator, - variant_name, - field_index, - field_name, - } - } - ChildTypeLocator::Array {} => AnnotatedSborAncestorContainer::Array { index: None }, - ChildTypeLocator::Map { entry_part } => AnnotatedSborAncestorContainer::Map { - index: None, - entry_part, - }, - }; - - AnnotatedSborAncestor { name, container } - }); - - Box::new(iterator) - } - - fn annotated_leaf(&self) -> Option> { - let (full_path, base_schema, compared_schema, error_detail) = *self; - let base_type_id = full_path.leaf_base_type_id; - let compared_type_id = full_path.leaf_compared_type_id; - - let (base_type_kind, base_metadata, _) = base_schema - .resolve_type_data(base_type_id) - .expect("Invalid base schema - Could not find data for base type"); - let (compared_type_kind, compared_metadata, _) = compared_schema - .resolve_type_data(compared_type_id) - .expect("Invalid compared schema - Could not find data for compared type"); - - Some(error_detail.resolve_annotated_leaf( - base_metadata, - base_type_kind, - compared_metadata, - compared_type_kind, - )) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct SchemaComparisonPathSegment { - parent_base_type_id: LocalTypeId, - parent_compared_type_id: LocalTypeId, - child_locator: ChildTypeLocator, -} - -impl SchemaComparisonPathSegment { - pub fn of( - parent_base_type_id: &LocalTypeId, - parent_compared_type_id: &LocalTypeId, - child_locator: ChildTypeLocator, - ) -> Self { - Self { - parent_base_type_id: *parent_base_type_id, - parent_compared_type_id: *parent_compared_type_id, - child_locator, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SchemaComparisonErrorDetail { - // Type kind errors - TypeKindMismatch { - base: TypeKindLabel, - compared: TypeKindLabel, - }, - TupleFieldCountMismatch { - base_field_count: usize, - compared_field_count: usize, - }, - EnumSupportedVariantsMismatch { - base_variants_missing_in_compared: IndexSet, - compared_variants_missing_in_base: IndexSet, - }, - EnumVariantFieldCountMismatch { - base_field_count: usize, - compared_field_count: usize, - variant_discriminator: u8, - }, - // Type metadata errors - TypeNameChangeError(NameChangeError), - FieldNameChangeError { - error: NameChangeError, - field_index: usize, - }, - EnumVariantNameChangeError { - error: NameChangeError, - variant_discriminator: u8, - }, - EnumVariantFieldNameChangeError { - error: NameChangeError, - variant_discriminator: u8, - field_index: usize, - }, - // Type validation error - TypeValidationChangeError { - change: ValidationChange, - old: TypeValidation, - new: TypeValidation, - }, - // Completeness errors - NamedRootTypeMissingInComparedSchema { - root_type_name: String, - }, - DisallowedNewRootTypeInComparedSchema { - root_type_name: String, - }, - TypeUnreachableFromRootInBaseSchema { - local_type_index: usize, - type_name: Option, - }, - TypeUnreachableFromRootInComparedSchema { - local_type_index: usize, - type_name: Option, - }, -} - -impl SchemaComparisonErrorDetail { - fn resolve_annotated_leaf( - &self, - base_metadata: &TypeMetadata, - base_type_kind: &LocalTypeKind, - compared_metadata: &TypeMetadata, - compared_type_kind: &LocalTypeKind, - ) -> AnnotatedSborPartialLeaf<'_> { - AnnotatedSborPartialLeaf { - name: Cow::Owned(combine_type_names::( - base_metadata, - base_type_kind, - compared_metadata, - compared_type_kind, - )), - partial_leaf_locator: self - .resolve_partial_leaf_locator(base_metadata, compared_metadata), - } - } - - fn resolve_partial_leaf_locator( - &self, - base_metadata: &TypeMetadata, - compared_metadata: &TypeMetadata, - ) -> Option> { - match *self { - SchemaComparisonErrorDetail::TypeKindMismatch { .. } => None, - SchemaComparisonErrorDetail::TupleFieldCountMismatch { .. } => None, - SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { .. } => { - // This error handles multiple variants, so we can't list them here - instead we handle it in the custom debug print - None - } - SchemaComparisonErrorDetail::EnumVariantFieldCountMismatch { - variant_discriminator, - .. - } => { - let base_variant = base_metadata - .get_enum_variant_data(variant_discriminator) - .expect("Invalid base schema - Could not find metadata for enum variant"); - let compared_variant = compared_metadata - .get_enum_variant_data(variant_discriminator) - .expect("Invalid compared schema - Could not find metadata for enum variant"); - Some(AnnotatedSborPartialLeafLocator::EnumVariant { - variant_discriminator: Some(variant_discriminator), - variant_name: combine_optional_names( - base_variant.get_name(), - compared_variant.get_name(), - ) - .map(Cow::Owned), - field_index: None, - field_name: None, - }) - } - SchemaComparisonErrorDetail::TypeNameChangeError(_) => None, - SchemaComparisonErrorDetail::FieldNameChangeError { field_index, .. } => { - let base_field_name = base_metadata.get_field_name(field_index); - let compared_field_name = compared_metadata.get_field_name(field_index); - Some(AnnotatedSborPartialLeafLocator::Tuple { - field_index: Some(field_index), - field_name: combine_optional_names(base_field_name, compared_field_name) - .map(Cow::Owned), - }) - } - SchemaComparisonErrorDetail::EnumVariantNameChangeError { - variant_discriminator, - .. - } => { - let base_variant = base_metadata - .get_enum_variant_data(variant_discriminator) - .expect("Invalid base schema - Could not find metadata for enum variant"); - let compared_variant = compared_metadata - .get_enum_variant_data(variant_discriminator) - .expect("Invalid compared schema - Could not find metadata for enum variant"); - Some(AnnotatedSborPartialLeafLocator::EnumVariant { - variant_discriminator: Some(variant_discriminator), - variant_name: combine_optional_names( - base_variant.get_name(), - compared_variant.get_name(), - ) - .map(Cow::Owned), - field_index: None, - field_name: None, - }) - } - SchemaComparisonErrorDetail::EnumVariantFieldNameChangeError { - variant_discriminator, - field_index, - .. - } => { - let base_variant = base_metadata - .get_enum_variant_data(variant_discriminator) - .expect("Invalid base schema - Could not find metadata for enum variant"); - let compared_variant = compared_metadata - .get_enum_variant_data(variant_discriminator) - .expect("Invalid compared schema - Could not find metadata for enum variant"); - let base_field_name = base_variant.get_field_name(field_index); - let compared_field_name = compared_variant.get_field_name(field_index); - Some(AnnotatedSborPartialLeafLocator::EnumVariant { - variant_discriminator: Some(variant_discriminator), - variant_name: combine_optional_names( - base_variant.get_name(), - compared_metadata.get_name(), - ) - .map(Cow::Owned), - field_index: Some(field_index), - field_name: combine_optional_names(base_field_name, compared_field_name) - .map(Cow::Owned), - }) - } - SchemaComparisonErrorDetail::TypeValidationChangeError { .. } => None, - SchemaComparisonErrorDetail::NamedRootTypeMissingInComparedSchema { .. } => None, - SchemaComparisonErrorDetail::DisallowedNewRootTypeInComparedSchema { .. } => None, - SchemaComparisonErrorDetail::TypeUnreachableFromRootInBaseSchema { .. } => None, - SchemaComparisonErrorDetail::TypeUnreachableFromRootInComparedSchema { .. } => None, - } - } - - fn write_with_context( - &self, - f: &mut F, - base_metadata: &TypeMetadata, - base_type_kind: &LocalTypeKind, - compared_metadata: &TypeMetadata, - compared_type_kind: &LocalTypeKind, - ) -> core::fmt::Result { - self.resolve_annotated_leaf( - base_metadata, - base_type_kind, - compared_metadata, - compared_type_kind, - ) - .write(f, true)?; - write!(f, " - ")?; - - match self { - // Handle any errors where we can add extra detail - SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { - base_variants_missing_in_compared, - compared_variants_missing_in_base, - } => { - write!( - f, - "EnumSupportedVariantsMismatch {{ base_variants_missing_in_compared: {{" - )?; - for variant_discriminator in base_variants_missing_in_compared { - let variant_data = base_metadata - .get_enum_variant_data(*variant_discriminator) - .unwrap(); - write!( - f, - "{variant_discriminator}|{}", - variant_data.get_name().unwrap_or("anon") - )?; - } - write!(f, "}}, compared_variants_missing_in_base: {{")?; - for variant_discriminator in compared_variants_missing_in_base { - let variant_data = compared_metadata - .get_enum_variant_data(*variant_discriminator) - .unwrap(); - write!( - f, - "{variant_discriminator}|{}", - variant_data.get_name().unwrap_or("anon") - )?; - } - write!(f, "}}, }}")?; - } - // All other errors already have their context added in printing the annotated leaf - _ => { - write!(f, "{self:?}")?; - } - } - - Ok(()) - } -} - -struct TypeKindComparisonResult { - children_needing_checking: Vec<(ChildTypeLocator, LocalTypeId, LocalTypeId)>, - errors: Vec>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum ChildTypeLocator { - Tuple { - field_index: usize, - }, - EnumVariant { - discriminator: u8, - field_index: usize, - }, - Array {}, // Unlike values, we don't have an index - Map { - entry_part: MapEntryPart, - }, // Unlike values, we don't have an index -} - -impl TypeKindComparisonResult { - fn new() -> Self { - Self { - children_needing_checking: vec![], - errors: vec![], - } - } - - fn add_error(&mut self, error: SchemaComparisonErrorDetail) { - self.errors.push(error) - } - - fn with_mismatch_error( - mut self, - base_type_kind: &LocalTypeKind, - compared_type_kind: &LocalTypeKind, - ) -> Self { - self.add_error(SchemaComparisonErrorDetail::TypeKindMismatch { - base: base_type_kind.label(), - compared: compared_type_kind.label(), - }); - self - } - - fn with_error(mut self, error: SchemaComparisonErrorDetail) -> Self { - self.add_error(error); - self - } - - fn add_child_to_check( - &mut self, - child_locator: ChildTypeLocator, - base_type_id: LocalTypeId, - compared_type_id: LocalTypeId, - ) { - self.children_needing_checking - .push((child_locator, base_type_id, compared_type_id)); - } -} - -struct TypeMetadataComparisonResult { - errors: Vec>, -} - -impl TypeMetadataComparisonResult { - fn new() -> Self { - Self { errors: vec![] } - } - - fn add_error(&mut self, error: SchemaComparisonErrorDetail) { - self.errors.push(error) - } -} - -struct TypeValidationComparisonResult { - errors: Vec>, -} - -impl TypeValidationComparisonResult { - fn new() -> Self { - Self { errors: vec![] } - } - - fn add_error(&mut self, error: SchemaComparisonErrorDetail) { - self.errors.push(error) - } -} - -struct ErrorsAggregator { - errors: Vec>, -} - -impl ErrorsAggregator { - fn new() -> Self { - Self { errors: vec![] } - } - - fn record_error( - &mut self, - error_detail: SchemaComparisonErrorDetail, - example_location: &TypeAncestorPath, - base_type_id: LocalTypeId, - compared_type_id: LocalTypeId, - ) { - self.errors.push(SchemaComparisonError { - error_detail, - example_location: Some(TypeFullPath { - root_type_identifier: example_location.root_type_identifier.clone(), - ancestor_path: example_location.ancestor_path.clone(), - leaf_base_type_id: base_type_id, - leaf_compared_type_id: compared_type_id, - }), - }) - } - - fn record_error_with_unvisited_location( - &mut self, - error_detail: SchemaComparisonErrorDetail, - ) { - self.errors.push(SchemaComparisonError { - error_detail, - example_location: None, - }) - } -} - -struct SchemaComparisonKernel<'s, 'o, S: CustomSchema> { - base_schema: &'s Schema, - compared_schema: &'s Schema, - settings: &'o SchemaComparisonSettings, - /// A matrix tracking if two types have been compared shallowly - cached_located_type_comparisons: - NonIterMap<(LocalTypeId, LocalTypeId), LocatedTypeComparisonResult>, - /// A list of pending comparisons - pending_comparison_work_list: Vec, - /// Used to cheaply capture whether we've seen a local type, for completeness checking - base_local_types_reachable_from_a_root: NonIterMap, - /// Used to cheaply capture whether we've seen a local type, for completeness checking - compared_local_types_reachable_from_a_root: NonIterMap, - - /// Tracking all the errors discovered - errors: ErrorsAggregator, -} - -impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { - /// This assumes the schemas provided are valid, and can panic if they're not. - /// - /// NOTE: This is NOT designed to be used: - /// * In situations where the schemas are untrusted. - /// The worst case runtime performance here for malicious schemas is O((N + W)^2) - /// where N is the number of schema types and W is the number of well known types. - /// * In situations where performance matters. - /// Whilst the expected performance for normal schemas is O(N), this - /// isn't designed in a very optimal way (e.g. there are lots of allocations, some - /// cloning etc). - fn new( - base_schema: &'s Schema, - compared_schema: &'s Schema, - settings: &'o SchemaComparisonSettings, - ) -> Self { - Self { - base_schema, - compared_schema, - settings, - cached_located_type_comparisons: Default::default(), - pending_comparison_work_list: Default::default(), - base_local_types_reachable_from_a_root: Default::default(), - compared_local_types_reachable_from_a_root: Default::default(), - errors: ErrorsAggregator::new(), - } - } - - pub fn compare_using_fixed_type_roots( - mut self, - type_roots: &[ComparisonTypeRoot], - ) -> SchemaComparisonResult<'s, S> { - // NOTE: While providing 0 type_roots is typically an accident, it isn't technically incorrect. - // There are some auto-generated cases (e.g. an empty interface) where it may make sense / be easiest - // to check an empty list of type roots. - for ComparisonTypeRoot { - name, - base_type_id, - compared_type_id, - } in type_roots.iter() - { - self.deep_compare_root_types(name, base_type_id, compared_type_id); - self.mark_root_reachable_base_types(base_type_id); - self.mark_root_reachable_compared_types(compared_type_id); - } - - self.check_for_completeness(); - self.into_result() - } - - pub fn compare_using_named_type_roots( - mut self, - base_type_roots: &IndexMap, - compared_type_roots: &IndexMap, - ) -> SchemaComparisonResult<'s, S> { - // First, let's loop through the base types, and compare them against the corresponding compared types. - // It is an error for a base named type not to exist in the corresponding compared list. - for (base_root_type_name, base_type_id) in base_type_roots.iter() { - if let Some(compared_type_id) = compared_type_roots.get(base_root_type_name) { - self.deep_compare_root_types(base_root_type_name, base_type_id, compared_type_id); - self.mark_root_reachable_base_types(base_type_id); - self.mark_root_reachable_compared_types(compared_type_id); - } else { - self.errors.record_error_with_unvisited_location( - SchemaComparisonErrorDetail::NamedRootTypeMissingInComparedSchema { - root_type_name: base_root_type_name.clone(), - }, - ); - self.mark_root_reachable_base_types(base_type_id); - } - } - - // We now loop through the compared types not covered in the above loop over base types - for (compared_root_type_name, compared_type_id) in compared_type_roots.iter() { - if !base_type_roots.contains_key(compared_root_type_name) { - if !self - .settings - .completeness - .allow_compared_to_have_more_root_types - { - self.errors.record_error_with_unvisited_location( - SchemaComparisonErrorDetail::DisallowedNewRootTypeInComparedSchema { - root_type_name: compared_root_type_name.clone(), - }, - ); - } - self.mark_root_reachable_compared_types(compared_type_id); - } - } - - self.check_for_completeness(); - self.into_result() - } - - fn deep_compare_root_types( - &mut self, - root_type_identifier: &str, - base_type_id: &LocalTypeId, - compared_type_id: &LocalTypeId, - ) { - self.pending_comparison_work_list - .push(PendingComparisonRequest { - base_type_id: *base_type_id, - compared_type_id: *compared_type_id, - ancestor_path: TypeAncestorPath { - root_type_identifier: root_type_identifier.to_string(), - ancestor_path: vec![], - }, - }); - // Run all comparison analysis we can perform. - // Due to the cache of shallow results over (TypesInBase * TypesInCompared), this must end. - while let Some(request) = self.pending_comparison_work_list.pop() { - self.run_single_type_comparison(request); - } - } - - fn mark_root_reachable_base_types(&mut self, root_base_type_id: &LocalTypeId) { - // Due to the cache, we do max O(TypesInBase) work. - // Note that reachability analysis needs to be performed separately to comparison analysis, because - // sometimes with comparisons of MyTuple(A) and MyTuple(B1, B2), we still want to perform reachability - // analysis on A, B1 and B2; but we can't make any sensible comparisons between them. - let LocalTypeId::SchemaLocalIndex(root_base_local_index) = root_base_type_id else { - return; - }; - let mut base_reachability_work_list = vec![*root_base_local_index]; - while let Some(base_type_index) = base_reachability_work_list.pop() { - match self - .base_local_types_reachable_from_a_root - .entry(base_type_index) - { - hash_map::Entry::Occupied(_) => continue, - hash_map::Entry::Vacant(vacant_entry) => vacant_entry.insert(()), - }; - let type_id = LocalTypeId::SchemaLocalIndex(base_type_index); - let type_kind = self - .base_schema - .resolve_type_kind(type_id) - .unwrap_or_else(|| { - panic!("Invalid base schema - type kind for {type_id:?} not found") - }); - visit_type_kind_children(type_kind, |_child_locator, child_type_kind| { - if let LocalTypeId::SchemaLocalIndex(local_index) = child_type_kind { - base_reachability_work_list.push(local_index); - }; - }) - } - } - - fn mark_root_reachable_compared_types(&mut self, root_compared_type_id: &LocalTypeId) { - let LocalTypeId::SchemaLocalIndex(root_compared_local_index) = root_compared_type_id else { - return; - }; - let mut compared_reachability_work_list = vec![*root_compared_local_index]; - while let Some(compared_local_index) = compared_reachability_work_list.pop() { - match self - .compared_local_types_reachable_from_a_root - .entry(compared_local_index) - { - hash_map::Entry::Occupied(_) => continue, - hash_map::Entry::Vacant(vacant_entry) => vacant_entry.insert(()), - }; - let type_id = LocalTypeId::SchemaLocalIndex(compared_local_index); - let type_kind = self - .compared_schema - .resolve_type_kind(type_id) - .unwrap_or_else(|| { - panic!("Invalid compared schema - type kind for {type_id:?} not found") - }); - visit_type_kind_children(type_kind, |_child_locator, child_type_kind| { - if let LocalTypeId::SchemaLocalIndex(local_index) = child_type_kind { - compared_reachability_work_list.push(local_index); - }; - }) - } - } - - fn run_single_type_comparison(&mut self, request: PendingComparisonRequest) { - let PendingComparisonRequest { - base_type_id, - compared_type_id, - ancestor_path: example_location, - } = request; - let status_key = (base_type_id, compared_type_id); - - if self - .cached_located_type_comparisons - .contains_key(&status_key) - { - return; - } - - let result = self.compare_types_internal(&example_location, base_type_id, compared_type_id); - for (child_locator, child_base_type_id, child_compared_type_id) in - result.child_checks_required - { - if self - .cached_located_type_comparisons - .contains_key(&(child_base_type_id, child_compared_type_id)) - { - continue; - } - let child_example_location = TypeAncestorPath { - root_type_identifier: example_location.root_type_identifier.clone(), - ancestor_path: { - let mut path = example_location.ancestor_path.clone(); - path.push(SchemaComparisonPathSegment::of( - &base_type_id, - &compared_type_id, - child_locator, - )); - path - }, - }; - self.pending_comparison_work_list - .push(PendingComparisonRequest { - base_type_id: child_base_type_id, - compared_type_id: child_compared_type_id, - ancestor_path: child_example_location, - }) - } - let located_result = LocatedTypeComparisonResult { - shallow_status: result.shallow_status, - example_location, - }; - self.cached_located_type_comparisons - .insert(status_key, located_result); - } - - fn compare_types_internal( - &mut self, - example_location: &TypeAncestorPath, - base_type_id: LocalTypeId, - compared_type_id: LocalTypeId, - ) -> ShallowTypeComparisonResult { - // Quick short-circuit when comparing equal well-known types - match (base_type_id, compared_type_id) { - ( - LocalTypeId::WellKnown(base_well_known), - LocalTypeId::WellKnown(compared_well_known), - ) => { - if base_well_known == compared_well_known { - return ShallowTypeComparisonResult::no_child_checks_required( - TypeComparisonStatus::Pass, - ); - } - } - _ => {} - } - - // Load type data from each schema - let (base_type_kind, base_type_metadata, base_type_validation) = self - .base_schema - .resolve_type_data(base_type_id) - .unwrap_or_else(|| { - panic!("Base schema was not valid - no type data for {base_type_id:?}") - }); - let (compared_type_kind, compared_type_metadata, compared_type_validation) = self - .compared_schema - .resolve_type_data(compared_type_id) - .unwrap_or_else(|| { - panic!("Compared schema was not valid - no type data for {compared_type_id:?}") - }); - - // Type Kind Comparison - let further_checks_required = { - let TypeKindComparisonResult { - errors, - children_needing_checking, - } = self.compare_type_kind_internal(base_type_kind, compared_type_kind); - - if errors.len() > 0 { - for error in errors { - self.errors.record_error( - error, - example_location, - base_type_id, - compared_type_id, - ); - } - // If the type kind comparison fails, then the metadata and validation comparisons aren't helpful information, - // so we can abort here without further tests. - return ShallowTypeComparisonResult { - shallow_status: TypeComparisonStatus::Failure, - child_checks_required: children_needing_checking, - }; - } - - children_needing_checking - }; - - let mut error_recorded = false; - - // Type Metadata Comparison - { - let TypeMetadataComparisonResult { errors } = self.compare_type_metadata_internal( - base_type_kind, - base_type_metadata, - compared_type_metadata, - ); - - for error in errors { - error_recorded = true; - self.errors - .record_error(error, example_location, base_type_id, compared_type_id); - } - } - - // Type Validation Comparison - { - let TypeValidationComparisonResult { errors } = self - .compare_type_validation_internal(base_type_validation, compared_type_validation); - - for error in errors { - error_recorded = true; - self.errors - .record_error(error, example_location, base_type_id, compared_type_id); - } - } - - return ShallowTypeComparisonResult { - shallow_status: if error_recorded { - TypeComparisonStatus::Failure - } else { - TypeComparisonStatus::Pass - }, - child_checks_required: further_checks_required, - }; - } - - fn compare_type_kind_internal( - &self, - base_type_kind: &LocalTypeKind, - compared_type_kind: &LocalTypeKind, - ) -> TypeKindComparisonResult { - // The returned children to check should be driven from the base type kind, - // because these are the children where we have to maintain backwards-compatibility - - let mut result = TypeKindComparisonResult::new(); - let settings = self.settings.structure; - if *compared_type_kind == TypeKind::Any - && *base_type_kind != TypeKind::Any - && settings.allow_replacing_with_any - { - // If we allow replacing any type with TypeKind::Any, and the new schema is Any, then the check is valid. - // - // That said, we should still check any children against Any: - // * In case they fail other checks (e.g. ancestor types on the base side required particular type names, - // which have now disappeared because the Compared side is Any) - // * To ensure we pass completeness checks on the base side - visit_type_kind_children(&base_type_kind, |child_type_locator, child_type_kind| { - result.add_child_to_check( - child_type_locator, - child_type_kind, - LocalTypeId::WellKnown(ANY_TYPE), - ); - }); - return result; - } - - match base_type_kind { - TypeKind::Any - | TypeKind::Bool - | TypeKind::I8 - | TypeKind::I16 - | TypeKind::I32 - | TypeKind::I64 - | TypeKind::I128 - | TypeKind::U8 - | TypeKind::U16 - | TypeKind::U32 - | TypeKind::U64 - | TypeKind::U128 - | TypeKind::String => { - if compared_type_kind != base_type_kind { - return result.with_mismatch_error(base_type_kind, compared_type_kind); - } - } - TypeKind::Array { - element_type: base_element_type, - } => { - let TypeKind::Array { - element_type: compared_element_type, - } = compared_type_kind - else { - return result.with_mismatch_error(base_type_kind, compared_type_kind); - }; - result.add_child_to_check( - ChildTypeLocator::Array {}, - *base_element_type, - *compared_element_type, - ); - } - TypeKind::Tuple { - field_types: base_field_types, - } => { - let TypeKind::Tuple { - field_types: compared_field_types, - } = compared_type_kind - else { - return result.with_mismatch_error(base_type_kind, compared_type_kind); - }; - if base_field_types.len() != compared_field_types.len() { - return result.with_error( - SchemaComparisonErrorDetail::TupleFieldCountMismatch { - base_field_count: base_field_types.len(), - compared_field_count: compared_field_types.len(), - }, - ); - } - let matched_field_types = base_field_types - .iter() - .cloned() - .zip(compared_field_types.iter().cloned()) - .enumerate(); - for (field_index, (base, compared)) in matched_field_types { - result.add_child_to_check( - ChildTypeLocator::Tuple { field_index }, - base, - compared, - ); - } - } - TypeKind::Enum { - variants: base_variants, - } => { - let TypeKind::Enum { - variants: compared_variants, - } = compared_type_kind - else { - return result.with_mismatch_error(base_type_kind, compared_type_kind); - }; - - let base_variants_missing_in_compared: IndexSet<_> = base_variants - .keys() - .filter(|base_variant_id| !compared_variants.contains_key(*base_variant_id)) - .cloned() - .collect(); - let compared_variants_missing_in_base: IndexSet<_> = compared_variants - .keys() - .filter(|compared_variant_id| !base_variants.contains_key(*compared_variant_id)) - .cloned() - .collect(); - - if base_variants_missing_in_compared.len() > 0 - || (compared_variants_missing_in_base.len() > 0 - && !settings.allow_new_enum_variants) - { - result.add_error(SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { - base_variants_missing_in_compared, - compared_variants_missing_in_base, - }); - } - - for (discriminator, base_field_type_ids) in base_variants.iter() { - let Some(compared_field_type_ids) = compared_variants.get(discriminator) else { - // We have already output a EnumSupportedVariantsMismatch error above for this. - // But let's continue to see if we can match / compare further variants structurally, - // to get as many errors as we can. - continue; - }; - let discriminator = *discriminator; - - if base_field_type_ids.len() != compared_field_type_ids.len() { - result.add_error( - SchemaComparisonErrorDetail::EnumVariantFieldCountMismatch { - variant_discriminator: discriminator, - base_field_count: base_field_type_ids.len(), - compared_field_count: compared_field_type_ids.len(), - }, - ); - } else { - let paired_child_ids = base_field_type_ids - .iter() - .zip(compared_field_type_ids.iter()) - .enumerate(); - for (field_index, (base_child_type_id, compared_child_type_id)) in - paired_child_ids - { - result.add_child_to_check( - ChildTypeLocator::EnumVariant { - discriminator, - field_index, - }, - *base_child_type_id, - *compared_child_type_id, - ); - } - } - } - } - TypeKind::Map { - key_type: base_key_type, - value_type: base_value_type, - } => { - let TypeKind::Map { - key_type: compared_key_type, - value_type: compared_value_type, - } = compared_type_kind - else { - return result.with_mismatch_error(base_type_kind, compared_type_kind); - }; - - result.add_child_to_check( - ChildTypeLocator::Map { - entry_part: MapEntryPart::Key, - }, - *base_key_type, - *compared_key_type, - ); - result.add_child_to_check( - ChildTypeLocator::Map { - entry_part: MapEntryPart::Value, - }, - *base_value_type, - *compared_value_type, - ); - } - // Assume for now that custom types are leaf types. - // Therefore we can directly run equality on the types, like the simple types. - TypeKind::Custom(_) => { - if compared_type_kind != base_type_kind { - return result.with_mismatch_error(base_type_kind, compared_type_kind); - } - } - } - - result - } - - fn compare_type_metadata_internal( - &self, - base_type_kind: &LocalTypeKind, - base_type_metadata: &TypeMetadata, - compared_type_metadata: &TypeMetadata, - ) -> TypeMetadataComparisonResult { - let settings = self.settings.metadata; - let mut result = TypeMetadataComparisonResult::new(); - if !settings.checks_required() { - return result; - } - if let Err(error) = NameChange::of_changed_option( - base_type_metadata.type_name.as_deref(), - compared_type_metadata.type_name.as_deref(), - ) - .validate(settings.type_name_changes) - { - result.add_error(SchemaComparisonErrorDetail::TypeNameChangeError(error)); - } - - // NOTE: For these tests, we assume that the schema is valid - that is, that the type metadata - // aligns with the underlying type kinds. - // Also, we have already tested for consistency of the compared type kind against the base type kind. - // So we can drive field/variant metadata iteration off the base type kind. - match base_type_kind { - TypeKind::Tuple { field_types } => { - for field_index in 0..field_types.len() { - if let Err(error) = NameChange::of_changed_option( - base_type_metadata.get_field_name(field_index), - compared_type_metadata.get_field_name(field_index), - ) - .validate(settings.field_name_changes) - { - result.add_error(SchemaComparisonErrorDetail::FieldNameChangeError { - field_index, - error, - }); - } - } - } - TypeKind::Enum { variants } => { - for (variant_discriminator, base_variant_types) in variants.iter() { - let variant_discriminator = *variant_discriminator; - let base_variant_metadata = base_type_metadata - .get_enum_variant_data(variant_discriminator) - .expect("Base schema was not valid - base did not have enum child names for an enum variant"); - let compared_variant_metadata = compared_type_metadata - .get_enum_variant_data(variant_discriminator) - .expect("Compared schema was not valid - base and compared agreed on structural equality of an enum, but compared did not have variant metadata for a base variant"); - - if let Err(error) = NameChange::of_changed_option( - base_variant_metadata.type_name.as_deref(), - compared_variant_metadata.type_name.as_deref(), - ) - .validate(settings.field_name_changes) - { - result.add_error(SchemaComparisonErrorDetail::EnumVariantNameChangeError { - variant_discriminator, - error, - }); - } - - for field_index in 0..base_variant_types.len() { - if let Err(error) = NameChange::of_changed_option( - base_variant_metadata.get_field_name(field_index), - compared_variant_metadata.get_field_name(field_index), - ) - .validate(settings.field_name_changes) - { - result.add_error( - SchemaComparisonErrorDetail::EnumVariantFieldNameChangeError { - variant_discriminator, - field_index, - error, - }, - ); - } - } - } - } - _ => { - // We can assume the schema is valid, therefore the only valid value is ChildNames::None - // So validation passes trivially - } - } - - result - } - - fn compare_type_validation_internal( - &self, - base_type_validation: &TypeValidation, - compared_type_validation: &TypeValidation, - ) -> TypeValidationComparisonResult { - let settings = self.settings.validation; - let mut result = TypeValidationComparisonResult::new(); - - let validation_change = match (base_type_validation, compared_type_validation) { - (TypeValidation::None, TypeValidation::None) => ValidationChange::Unchanged, - // Strictly a provided validation might be equivalent to None, for example: - // (for example NumericValidation { min: None, max: None } or NumericValidation:: { min: 0, max: 255 }) - // but for now assume that it's different - (_, TypeValidation::None) => ValidationChange::Weakened, - (TypeValidation::None, _) => ValidationChange::Strengthened, - // Now test equal validations - (TypeValidation::I8(base), TypeValidation::I8(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::I16(base), TypeValidation::I16(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::I32(base), TypeValidation::I32(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::I64(base), TypeValidation::I64(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::I128(base), TypeValidation::I128(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::U8(base), TypeValidation::U8(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::U16(base), TypeValidation::U16(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::U32(base), TypeValidation::U32(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::U64(base), TypeValidation::U64(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::U128(base), TypeValidation::U128(compared)) => { - NumericValidation::compare(base, compared) - } - (TypeValidation::String(base), TypeValidation::String(compared)) => { - LengthValidation::compare(base, compared) - } - (TypeValidation::Array(base), TypeValidation::Array(compared)) => { - LengthValidation::compare(base, compared) - } - (TypeValidation::Map(base), TypeValidation::Map(compared)) => { - LengthValidation::compare(base, compared) - } - (TypeValidation::Custom(base), TypeValidation::Custom(compared)) => { - <::CustomTypeValidation as CustomTypeValidation>::compare( - base, compared, - ) - } - // Otherwise assume they are incomparable - _ => ValidationChange::Incomparable, - }; - let is_valid = match validation_change { - ValidationChange::Unchanged => true, - ValidationChange::Strengthened => false, - ValidationChange::Weakened => settings.allow_validation_weakening, - ValidationChange::Incomparable => false, - }; - if !is_valid { - result.add_error(SchemaComparisonErrorDetail::TypeValidationChangeError { - change: validation_change, - old: base_type_validation.clone(), - new: compared_type_validation.clone(), - }) - } - result - } - - fn check_for_completeness(&mut self) { - if !self - .settings - .completeness - .allow_root_unreachable_types_in_base_schema - { - if self.base_local_types_reachable_from_a_root.len() - < self.base_schema.type_metadata.len() - { - for (local_type_index, metadata) in - self.base_schema.type_metadata.iter().enumerate() - { - if !self - .base_local_types_reachable_from_a_root - .contains_key(&local_type_index) - { - let type_name = metadata.type_name.as_ref().map(|n| n.clone().into_owned()); - self.errors.record_error_with_unvisited_location( - SchemaComparisonErrorDetail::TypeUnreachableFromRootInBaseSchema { - local_type_index, - type_name, - }, - ) - } - } - } - } - if !self - .settings - .completeness - .allow_root_unreachable_types_in_compared_schema - { - if self.compared_local_types_reachable_from_a_root.len() - < self.compared_schema.type_metadata.len() - { - for (local_type_index, metadata) in - self.compared_schema.type_metadata.iter().enumerate() - { - if !self - .compared_local_types_reachable_from_a_root - .contains_key(&local_type_index) - { - let type_name = metadata.type_name.as_ref().map(|n| n.clone().into_owned()); - self.errors.record_error_with_unvisited_location( - SchemaComparisonErrorDetail::TypeUnreachableFromRootInComparedSchema { - local_type_index, - type_name, - }, - ) - } - } - } - } - } - - fn into_result(self) -> SchemaComparisonResult<'s, S> { - SchemaComparisonResult { - base_schema: self.base_schema, - compared_schema: self.compared_schema, - errors: self.errors.errors, - } - } -} - -fn visit_type_kind_children>( - type_kind: &TypeKind, - mut visitor: impl FnMut(ChildTypeLocator, LocalTypeId), -) { - return match type_kind { - TypeKind::Any - | TypeKind::Bool - | TypeKind::I8 - | TypeKind::I16 - | TypeKind::I32 - | TypeKind::I64 - | TypeKind::I128 - | TypeKind::U8 - | TypeKind::U16 - | TypeKind::U32 - | TypeKind::U64 - | TypeKind::U128 - | TypeKind::String => {} - TypeKind::Array { element_type } => { - visitor(ChildTypeLocator::Array {}, *element_type); - } - TypeKind::Tuple { field_types } => { - for (field_index, field_type) in field_types.iter().enumerate() { - visitor(ChildTypeLocator::Tuple { field_index }, *field_type) - } - } - TypeKind::Enum { variants } => { - for (discriminator, field_types) in variants { - for (field_index, field_type) in field_types.iter().enumerate() { - visitor( - ChildTypeLocator::EnumVariant { - discriminator: *discriminator, - field_index, - }, - *field_type, - ) - } - } - } - TypeKind::Map { - key_type, - value_type, - } => { - visitor( - ChildTypeLocator::Map { - entry_part: MapEntryPart::Key, - }, - *key_type, - ); - visitor( - ChildTypeLocator::Map { - entry_part: MapEntryPart::Value, - }, - *value_type, - ); - } - // At present, assume that custom types are leaf types. - TypeKind::Custom(_) => {} - }; -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct PendingComparisonRequest { - base_type_id: LocalTypeId, - compared_type_id: LocalTypeId, - ancestor_path: TypeAncestorPath, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct TypeAncestorPath { - root_type_identifier: String, - ancestor_path: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct TypeFullPath { - root_type_identifier: String, - ancestor_path: Vec, - leaf_base_type_id: LocalTypeId, - leaf_compared_type_id: LocalTypeId, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct LocatedTypeComparisonResult { - shallow_status: TypeComparisonStatus, - example_location: TypeAncestorPath, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ShallowTypeComparisonResult { - shallow_status: TypeComparisonStatus, - child_checks_required: Vec<(ChildTypeLocator, LocalTypeId, LocalTypeId)>, -} - -impl ShallowTypeComparisonResult { - pub fn no_child_checks_required(status: TypeComparisonStatus) -> Self { - Self { - shallow_status: status, - child_checks_required: vec![], - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum TypeComparisonStatus { - Pass, - Failure, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ComparisonTypeRoot { - name: String, - base_type_id: LocalTypeId, - compared_type_id: LocalTypeId, -} - -pub struct NamedSchemaVersions> { - ordered_versions: IndexMap, - custom_schema: PhantomData, -} - -impl> NamedSchemaVersions { - pub fn new() -> Self { - Self { - ordered_versions: Default::default(), - custom_schema: Default::default(), - } - } - - pub fn from, K: AsRef, V: IntoSchema>( - from: F, - ) -> Self { - Self { - ordered_versions: from - .into_iter() - .map(|(name, version)| (name.as_ref().to_string(), version.into_schema())) - .collect(), - custom_schema: Default::default(), - } - } - - pub fn register_version( - mut self, - name: impl AsRef, - version: impl IntoSchema, - ) -> Self { - self.ordered_versions - .insert(name.as_ref().to_string(), version.into_schema()); - self - } - - pub fn get_versions(&self) -> &IndexMap { - &self.ordered_versions - } -} - -/// Designed for basic comparisons between two types, e.g. equality checking. -/// -/// ## Example usage -/// ```no_run -/// # use radix_rust::prelude::*; -/// # use sbor::NoCustomSchema; -/// # use sbor::SchemaComparison::*; -/// # type ScryptoCustomSchema = NoCustomSchema; -/// # struct MyType; -/// let base = SingleTypeSchema::from("5b...."); -/// let current = SingleTypeSchema::for_type::(); -/// compare_single_type_schema::( -/// &SchemaComparisonSettings::require_equality(), -/// &base, -/// ¤t, -/// ).assert_valid("base", "compared"); -/// ``` -pub fn compare_single_type_schemas<'s, S: CustomSchema>( - comparison_settings: &SchemaComparisonSettings, - base: &'s SingleTypeSchema, - compared: &'s SingleTypeSchema, -) -> SchemaComparisonResult<'s, S> { - base.compare_with(compared, comparison_settings) -} - -/// Designed for ensuring a type is only altered in ways which ensure -/// backwards compatibility in SBOR serialization (i.e. that old payloads -/// can be deserialized correctly by the latest type). -/// -/// This function: -/// * Checks that the type's current schema is equal to the latest version -/// * Checks that each schema is consistent with the previous schema - but -/// can be an extension (e.g. enums can have new variants) -/// -/// The version registry is be a map from a version name to some encoding -/// of a `SingleTypeSchema` - including as-is, or hex-encoded sbor-encoded. -/// The version name is only used for a more useful message on error. -/// -/// ## Example usage -/// ```no_run -/// # use radix_rust::prelude::*; -/// # use sbor::NoCustomSchema; -/// # use sbor::SchemaComparison::*; -/// # type ScryptoCustomSchema = NoCustomSchema; -/// # struct MyType; -/// assert_type_backwards_compatibility::( -/// |v| { -/// v.register_version("babylon_launch", "5b...") -/// .register_version("bottlenose", "5b...") -/// }, -/// ); -/// ``` -/// ## Setup -/// To generate the encoded schema, just run the method with an empty `indexmap!` -/// and the assertion will include the encoded schemas, for copying into the assertion. -/// -/// ``` -pub fn assert_type_backwards_compatibility< - S: CustomSchema, - T: Describe, ->( - versions_builder: impl FnOnce( - NamedSchemaVersions>, - ) -> NamedSchemaVersions>, -) { - assert_type_compatibility::( - &SchemaComparisonSettings::allow_extension(), - versions_builder, - ) -} - -/// Designed for ensuring a type is only altered in ways which ensure -/// backwards compatibility in SBOR serialization (i.e. that old payloads -/// can be deserialized correctly by the latest type). -/// -/// This function: -/// * Checks that the type's current schema is equal to the latest version -/// * Checks that each schema is consistent with the previous schema - but -/// can be an extension (e.g. enums can have new variants) -/// -/// The version registry is be a map from a version name to some encoding -/// of a `SingleTypeSchema` - including as-is, or hex-encoded sbor-encoded. -/// The version name is only used for a more useful message on error. -/// -/// ## Example usage -/// ```no_run -/// # use radix_rust::prelude::*; -/// # use sbor::NoCustomSchema; -/// # use sbor::SchemaComparison::*; -/// # type ScryptoCustomSchema = NoCustomSchema; -/// # struct MyType; -/// assert_type_compatibility::( -/// SchemaComparisonSettings::allow_extension(), -/// |v| { -/// v.register_version("babylon_launch", "5b...") -/// .register_version("bottlenose", "5b...") -/// }, -/// ); -/// ``` -/// ## Setup -/// To generate the encoded schema, just run the method with an empty `indexmap!` -/// and the assertion will include the encoded schemas, for copying into the assertion. -/// -/// ``` -pub fn assert_type_compatibility>( - comparison_settings: &SchemaComparisonSettings, - versions_builder: impl FnOnce( - NamedSchemaVersions>, - ) -> NamedSchemaVersions>, -) { - let current = generate_single_type_schema::(); - assert_schema_compatibility( - comparison_settings, - ¤t, - &versions_builder(NamedSchemaVersions::new()), - ) -} - -pub fn compare_type_collection_schemas<'s, S: CustomSchema>( - comparison_settings: &SchemaComparisonSettings, - base: &'s TypeCollectionSchema, - compared: &'s TypeCollectionSchema, -) -> SchemaComparisonResult<'s, S> { - base.compare_with(compared, comparison_settings) -} - -pub fn assert_type_collection_backwards_compatibility( - current: TypeCollectionSchema, - versions_builder: impl FnOnce( - NamedSchemaVersions>, - ) -> NamedSchemaVersions>, -) { - assert_type_collection_compatibility( - &SchemaComparisonSettings::allow_extension(), - current, - versions_builder, - ) -} - -pub fn assert_type_collection_compatibility( - comparison_settings: &SchemaComparisonSettings, - current: TypeCollectionSchema, - versions_builder: impl FnOnce( - NamedSchemaVersions>, - ) -> NamedSchemaVersions>, -) { - assert_schema_compatibility( - comparison_settings, - ¤t, - &versions_builder(NamedSchemaVersions::new()), - ) -} - -fn assert_schema_compatibility>( - schema_comparison_settings: &SchemaComparisonSettings, - current: &C, - named_versions: &NamedSchemaVersions, -) { - let named_versions = named_versions.get_versions(); - - // Part 0 - Check that there is at least one named_historic_schema_versions, - // if not, output latest encoded. - let Some((latest_version_name, latest_schema_version)) = named_versions.last() else { - let mut error = String::new(); - writeln!( - &mut error, - "You must provide at least one named schema version." - ) - .unwrap(); - writeln!(&mut error, "Use a relevant name (for example, the current software version name), and save the current schema as follows:").unwrap(); - writeln!(&mut error, "{}", current.encode_to_hex()).unwrap(); - panic!("{error}"); - }; - - // Part 1 - Check that latest is equal to the last historic schema version - let result = - latest_schema_version.compare_with(¤t, &SchemaComparisonSettings::require_equality()); - - if let Some(error_message) = result.error_message(latest_version_name, "current") { - let mut error = String::new(); - writeln!(&mut error, "The most recent named version ({latest_version_name}) DOES NOT PASS CHECKS, likely because it is not equal to the current version.").unwrap(); - writeln!(&mut error).unwrap(); - write!(&mut error, "{error_message}").unwrap(); - writeln!(&mut error).unwrap(); - writeln!(&mut error, "You will either want to:").unwrap(); - writeln!( - &mut error, - "(A) Add a new named version to the list, to be supported going forward." - ) - .unwrap(); - writeln!( - &mut error, - "(B) Replace the latest version. ONLY do this if the version has not yet been in use." - ) - .unwrap(); - writeln!(&mut error).unwrap(); - writeln!(&mut error, "The latest version is:").unwrap(); - writeln!(&mut error, "{}", current.encode_to_hex()).unwrap(); - panic!("{error}"); - } - - // Part 2 - Check that (N, N + 1) schemas respect the comparison settings, pairwise - for i in 0..named_versions.len() - 1 { - let (previous_version_name, previous_schema) = named_versions.get_index(i).unwrap(); - let (next_version_name, next_schema) = named_versions.get_index(i + 1).unwrap(); - - previous_schema - .compare_with(next_schema, schema_comparison_settings) - .assert_valid(previous_version_name, &next_version_name); - } -} - -/// A serializable record of the schema of a single type. -/// Intended for historical backwards compatibility checking of a single type. -#[derive(Debug, Clone, Sbor)] -#[sbor(child_types = "S::CustomLocalTypeKind, S::CustomTypeValidation")] -pub struct SingleTypeSchema { - pub schema: VersionedSchema, - pub type_id: LocalTypeId, -} - -impl SingleTypeSchema { - pub fn new(schema: VersionedSchema, type_id: LocalTypeId) -> Self { - Self { schema, type_id } - } - - pub fn from>(from: T) -> Self { - from.into_schema() - } - - pub fn for_type + ?Sized>() -> Self { - generate_single_type_schema::() - } -} - -impl ComparableSchema for SingleTypeSchema { - fn compare_with<'s>( - &'s self, - compared: &'s Self, - settings: &SchemaComparisonSettings, - ) -> SchemaComparisonResult<'s, S> { - SchemaComparisonKernel::new( - &self.schema.as_unique_version(), - &compared.schema.as_unique_version(), - settings, - ) - .compare_using_fixed_type_roots(&[ComparisonTypeRoot { - name: "root".to_string(), - base_type_id: self.type_id, - compared_type_id: compared.type_id, - }]) - } -} - -impl IntoSchema for SingleTypeSchema { - fn into_schema(&self) -> Self { - self.clone() - } -} - -/// A serializable record of the schema of a set of named types. -/// Intended for historical backwards compatibility of a collection -/// of types in a single schema. -/// -/// For example, traits, or blueprint interfaces. -#[derive(Debug, Clone, Sbor)] -#[sbor(child_types = "S::CustomLocalTypeKind, S::CustomTypeValidation")] -pub struct TypeCollectionSchema { - pub schema: VersionedSchema, - pub type_ids: IndexMap, -} - -impl TypeCollectionSchema { - pub fn new(schema: VersionedSchema, type_ids: IndexMap) -> Self { - Self { schema, type_ids } - } - - pub fn from>(from: &T) -> Self { - from.into_schema() - } - - pub fn from_aggregator(aggregator: TypeAggregator) -> Self { - aggregator.generate_type_collection_schema::() - } -} - -impl ComparableSchema for TypeCollectionSchema { - fn compare_with<'s>( - &'s self, - compared: &'s Self, - settings: &SchemaComparisonSettings, - ) -> SchemaComparisonResult<'s, S> { - SchemaComparisonKernel::new( - &self.schema.as_unique_version(), - &compared.schema.as_unique_version(), - settings, - ) - .compare_using_named_type_roots(&self.type_ids, &compared.type_ids) - } -} - -impl IntoSchema for TypeCollectionSchema { - fn into_schema(&self) -> Self { - self.clone() - } -} - -/// Marker trait for SingleTypeSchema and NamedTypesSchema which includes named pointers to types, -/// which can be used for comparisons of different versions of the same schema. -pub trait ComparableSchema: Clone + VecSbor { - fn encode_to_bytes(&self) -> Vec { - vec_encode::(self, BASIC_SBOR_V1_MAX_DEPTH).unwrap() - } - - fn encode_to_hex(&self) -> String { - hex::encode(&self.encode_to_bytes()) - } - - fn decode_from_bytes(bytes: &[u8]) -> Self { - vec_decode_with_nice_error::( - bytes, - BASIC_SBOR_V1_MAX_DEPTH, - ) - .unwrap_or_else(|err| { - panic!( - "Could not SBOR decode bytes into {} with {}: {:?}", - core::any::type_name::(), - core::any::type_name::(), - err, - ) - }) - } - - fn decode_from_hex(hex: &str) -> Self { - let bytes = hex::decode(hex) - .unwrap_or_else(|err| panic!("Provided string was not valid hex: {err}")); - Self::decode_from_bytes(&bytes) - } - - fn compare_with<'s>( - &'s self, - compared: &'s Self, - settings: &SchemaComparisonSettings, - ) -> SchemaComparisonResult<'s, S>; -} - -pub trait IntoSchema, S: CustomSchema> { - fn into_schema(&self) -> C; -} - -impl<'a, C: ComparableSchema, S: CustomSchema, T: IntoSchema + ?Sized> IntoSchema - for &'a T -{ - fn into_schema(&self) -> C { - >::into_schema(*self) - } -} - -impl, S: CustomSchema> IntoSchema for [u8] { - fn into_schema(&self) -> C { - C::decode_from_bytes(self) - } -} - -impl, S: CustomSchema> IntoSchema for Vec { - fn into_schema(&self) -> C { - C::decode_from_bytes(self) - } -} - -impl, S: CustomSchema> IntoSchema for String { - fn into_schema(&self) -> C { - C::decode_from_hex(self) - } -} - -impl, S: CustomSchema> IntoSchema for str { - fn into_schema(&self) -> C { - C::decode_from_hex(self) - } -} - -/// Marker traits intended to be implemented by the SborAssert macros -pub trait CheckedFixedSchema: CheckedBackwardsCompatibleSchema {} -pub trait CheckedBackwardsCompatibleSchema {} - -// NOTE: Types are in sbor-tests/tests/schema_comparison.rs diff --git a/sbor/src/schema/schema_comparison/comparable_schema.rs b/sbor/src/schema/schema_comparison/comparable_schema.rs new file mode 100644 index 00000000000..618091b1022 --- /dev/null +++ b/sbor/src/schema/schema_comparison/comparable_schema.rs @@ -0,0 +1,165 @@ +use super::*; + +/// A list of named comparable schemas, intended to capture various versions +/// of the same schema over time. +pub struct NamedSchemaVersions> { + ordered_versions: IndexMap, + custom_schema: PhantomData, +} + +impl> NamedSchemaVersions { + pub fn new() -> Self { + Self { + ordered_versions: Default::default(), + custom_schema: Default::default(), + } + } + + pub fn from, K: AsRef, V: IntoComparableSchema>( + from: F, + ) -> Self { + Self { + ordered_versions: from + .into_iter() + .map(|(name, version)| (name.as_ref().to_string(), version.into_schema())) + .collect(), + custom_schema: Default::default(), + } + } + + pub fn register_version( + mut self, + name: impl AsRef, + version: impl IntoComparableSchema, + ) -> Self { + self.ordered_versions + .insert(name.as_ref().to_string(), version.into_schema()); + self + } + + pub fn get_versions(&self) -> &IndexMap { + &self.ordered_versions + } +} + +/// Marker trait for [`SingleTypeSchema`] and [`TypeCollectionSchema`] which +/// includes named pointers to types, and can be used for comparisons of +/// different versions of the same schema. +pub trait ComparableSchema: Clone + VecSbor { + fn encode_to_bytes(&self) -> Vec { + vec_encode::(self, BASIC_SBOR_V1_MAX_DEPTH).unwrap() + } + + fn encode_to_hex(&self) -> String { + hex::encode(&self.encode_to_bytes()) + } + + fn decode_from_bytes(bytes: &[u8]) -> Self { + vec_decode_with_nice_error::( + bytes, + BASIC_SBOR_V1_MAX_DEPTH, + ) + .unwrap_or_else(|err| { + panic!( + "Could not SBOR decode bytes into {} with {}: {:?}", + core::any::type_name::(), + core::any::type_name::(), + err, + ) + }) + } + + fn decode_from_hex(hex: &str) -> Self { + let bytes = hex::decode(hex) + .unwrap_or_else(|err| panic!("Provided string was not valid hex: {err}")); + Self::decode_from_bytes(&bytes) + } + + fn compare_with<'s>( + &'s self, + compared: &'s Self, + settings: &SchemaComparisonSettings, + ) -> SchemaComparisonResult<'s, S>; +} + +impl ComparableSchema for SingleTypeSchema { + fn compare_with<'s>( + &'s self, + compared: &'s Self, + settings: &SchemaComparisonSettings, + ) -> SchemaComparisonResult<'s, S> { + SchemaComparisonKernel::new( + &self.schema.as_unique_version(), + &compared.schema.as_unique_version(), + settings, + ) + .compare_using_fixed_type_roots(&[ComparisonTypeRoot { + name: "root".to_string(), + base_type_id: self.type_id, + compared_type_id: compared.type_id, + }]) + } +} + +impl ComparableSchema for TypeCollectionSchema { + fn compare_with<'s>( + &'s self, + compared: &'s Self, + settings: &SchemaComparisonSettings, + ) -> SchemaComparisonResult<'s, S> { + SchemaComparisonKernel::new( + &self.schema.as_unique_version(), + &compared.schema.as_unique_version(), + settings, + ) + .compare_using_named_type_roots(&self.type_ids, &compared.type_ids) + } +} + +pub trait IntoComparableSchema, S: CustomSchema> { + fn into_schema(&self) -> C; +} + +impl IntoComparableSchema for SingleTypeSchema { + fn into_schema(&self) -> Self { + self.clone() + } +} + +impl IntoComparableSchema for TypeCollectionSchema { + fn into_schema(&self) -> Self { + self.clone() + } +} + +impl<'a, C: ComparableSchema, S: CustomSchema, T: IntoComparableSchema + ?Sized> + IntoComparableSchema for &'a T +{ + fn into_schema(&self) -> C { + >::into_schema(*self) + } +} + +impl, S: CustomSchema> IntoComparableSchema for [u8] { + fn into_schema(&self) -> C { + C::decode_from_bytes(self) + } +} + +impl, S: CustomSchema> IntoComparableSchema for Vec { + fn into_schema(&self) -> C { + C::decode_from_bytes(self) + } +} + +impl, S: CustomSchema> IntoComparableSchema for String { + fn into_schema(&self) -> C { + C::decode_from_hex(self) + } +} + +impl, S: CustomSchema> IntoComparableSchema for str { + fn into_schema(&self) -> C { + C::decode_from_hex(self) + } +} diff --git a/sbor/src/schema/schema_comparison/comparisons_and_assertions.rs b/sbor/src/schema/schema_comparison/comparisons_and_assertions.rs new file mode 100644 index 00000000000..58d0f51a2c5 --- /dev/null +++ b/sbor/src/schema/schema_comparison/comparisons_and_assertions.rs @@ -0,0 +1,213 @@ +use super::*; + +/// Designed for basic comparisons between two types, e.g. equality checking. +/// +/// ## Example usage +/// ```no_run +/// # use radix_rust::prelude::*; +/// # use sbor::NoCustomSchema; +/// # use sbor::SchemaComparison::*; +/// # type ScryptoCustomSchema = NoCustomSchema; +/// # struct MyType; +/// let base = SingleTypeSchema::from("5b...."); +/// let current = SingleTypeSchema::for_type::(); +/// compare_single_type_schema::( +/// &SchemaComparisonSettings::require_equality(), +/// &base, +/// ¤t, +/// ).assert_valid("base", "compared"); +/// ``` +pub fn compare_single_type_schemas<'s, S: CustomSchema>( + comparison_settings: &SchemaComparisonSettings, + base: &'s SingleTypeSchema, + compared: &'s SingleTypeSchema, +) -> SchemaComparisonResult<'s, S> { + base.compare_with(compared, comparison_settings) +} + +/// Designed for ensuring a type is only altered in ways which ensure +/// backwards compatibility in SBOR serialization (i.e. that old payloads +/// can be deserialized correctly by the latest type). +/// +/// This function: +/// * Checks that the type's current schema is equal to the latest version +/// * Checks that each schema is consistent with the previous schema - but +/// can be an extension (e.g. enums can have new variants) +/// +/// The version registry is be a map from a version name to some encoding +/// of a `SingleTypeSchema` - including as-is, or hex-encoded sbor-encoded. +/// The version name is only used for a more useful message on error. +/// +/// ## Example usage +/// ```no_run +/// # use radix_rust::prelude::*; +/// # use sbor::NoCustomSchema; +/// # use sbor::SchemaComparison::*; +/// # type ScryptoCustomSchema = NoCustomSchema; +/// # struct MyType; +/// assert_type_backwards_compatibility::( +/// |v| { +/// v.register_version("babylon_launch", "5b...") +/// .register_version("bottlenose", "5b...") +/// }, +/// ); +/// ``` +/// ## Setup +/// To generate the encoded schema, just run the method with an empty `indexmap!` +/// and the assertion will include the encoded schemas, for copying into the assertion. +/// +/// ``` +pub fn assert_type_backwards_compatibility< + S: CustomSchema, + T: Describe, +>( + versions_builder: impl FnOnce( + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) { + assert_type_compatibility::( + &SchemaComparisonSettings::allow_extension(), + versions_builder, + ) +} + +/// Designed for ensuring a type is only altered in ways which ensure +/// backwards compatibility in SBOR serialization (i.e. that old payloads +/// can be deserialized correctly by the latest type). +/// +/// This function: +/// * Checks that the type's current schema is equal to the latest version +/// * Checks that each schema is consistent with the previous schema - but +/// can be an extension (e.g. enums can have new variants) +/// +/// The version registry is be a map from a version name to some encoding +/// of a `SingleTypeSchema` - including as-is, or hex-encoded sbor-encoded. +/// The version name is only used for a more useful message on error. +/// +/// ## Example usage +/// ```no_run +/// # use radix_rust::prelude::*; +/// # use sbor::NoCustomSchema; +/// # use sbor::SchemaComparison::*; +/// # type ScryptoCustomSchema = NoCustomSchema; +/// # struct MyType; +/// assert_type_compatibility::( +/// SchemaComparisonSettings::allow_extension(), +/// |v| { +/// v.register_version("babylon_launch", "5b...") +/// .register_version("bottlenose", "5b...") +/// }, +/// ); +/// ``` +/// ## Setup +/// To generate the encoded schema, just run the method with an empty `indexmap!` +/// and the assertion will include the encoded schemas, for copying into the assertion. +/// +/// ``` +pub fn assert_type_compatibility>( + comparison_settings: &SchemaComparisonSettings, + versions_builder: impl FnOnce( + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) { + let current = generate_single_type_schema::(); + assert_schema_compatibility( + comparison_settings, + ¤t, + &versions_builder(NamedSchemaVersions::new()), + ) +} + +pub fn compare_type_collection_schemas<'s, S: CustomSchema>( + comparison_settings: &SchemaComparisonSettings, + base: &'s TypeCollectionSchema, + compared: &'s TypeCollectionSchema, +) -> SchemaComparisonResult<'s, S> { + base.compare_with(compared, comparison_settings) +} + +pub fn assert_type_collection_backwards_compatibility( + current: TypeCollectionSchema, + versions_builder: impl FnOnce( + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) { + assert_type_collection_compatibility( + &SchemaComparisonSettings::allow_extension(), + current, + versions_builder, + ) +} + +pub fn assert_type_collection_compatibility( + comparison_settings: &SchemaComparisonSettings, + current: TypeCollectionSchema, + versions_builder: impl FnOnce( + NamedSchemaVersions>, + ) -> NamedSchemaVersions>, +) { + assert_schema_compatibility( + comparison_settings, + ¤t, + &versions_builder(NamedSchemaVersions::new()), + ) +} + +fn assert_schema_compatibility>( + schema_comparison_settings: &SchemaComparisonSettings, + current: &C, + named_versions: &NamedSchemaVersions, +) { + let named_versions = named_versions.get_versions(); + + // Part 0 - Check that there is at least one named_historic_schema_versions, + // if not, output latest encoded. + let Some((latest_version_name, latest_schema_version)) = named_versions.last() else { + let mut error = String::new(); + writeln!( + &mut error, + "You must provide at least one named schema version." + ) + .unwrap(); + writeln!(&mut error, "Use a relevant name (for example, the current software version name), and save the current schema as follows:").unwrap(); + writeln!(&mut error, "{}", current.encode_to_hex()).unwrap(); + panic!("{error}"); + }; + + // Part 1 - Check that latest is equal to the last historic schema version + let result = + latest_schema_version.compare_with(¤t, &SchemaComparisonSettings::require_equality()); + + if let Some(error_message) = result.error_message(latest_version_name, "current") { + let mut error = String::new(); + writeln!(&mut error, "The most recent named version ({latest_version_name}) DOES NOT PASS CHECKS, likely because it is not equal to the current version.").unwrap(); + writeln!(&mut error).unwrap(); + write!(&mut error, "{error_message}").unwrap(); + writeln!(&mut error).unwrap(); + writeln!(&mut error, "You will either want to:").unwrap(); + writeln!( + &mut error, + "(A) Add a new named version to the list, to be supported going forward." + ) + .unwrap(); + writeln!( + &mut error, + "(B) Replace the latest version. ONLY do this if the version has not yet been in use." + ) + .unwrap(); + writeln!(&mut error).unwrap(); + writeln!(&mut error, "The latest version is:").unwrap(); + writeln!(&mut error, "{}", current.encode_to_hex()).unwrap(); + panic!("{error}"); + } + + // Part 2 - Check that (N, N + 1) schemas respect the comparison settings, pairwise + for i in 0..named_versions.len() - 1 { + let (previous_version_name, previous_schema) = named_versions.get_index(i).unwrap(); + let (next_version_name, next_schema) = named_versions.get_index(i + 1).unwrap(); + + previous_schema + .compare_with(next_schema, schema_comparison_settings) + .assert_valid(previous_version_name, &next_version_name); + } +} diff --git a/sbor/src/schema/schema_comparison/mod.rs b/sbor/src/schema/schema_comparison/mod.rs new file mode 100644 index 00000000000..800e38f6fcf --- /dev/null +++ b/sbor/src/schema/schema_comparison/mod.rs @@ -0,0 +1,25 @@ +use basic_well_known_types::ANY_TYPE; + +use crate::internal_prelude::*; +use crate::schema::*; +use crate::traversal::*; +use crate::BASIC_SBOR_V1_MAX_DEPTH; +use radix_rust::rust::fmt::Write; + +mod comparable_schema; +mod comparisons_and_assertions; +mod schema_comparison_kernel; +mod schema_comparison_result; +mod schema_comparison_settings; + +pub use comparable_schema::*; +pub use comparisons_and_assertions::*; +use schema_comparison_kernel::*; +pub use schema_comparison_result::*; +pub use schema_comparison_settings::*; + +/// Marker traits intended to be implemented by the SborAssert macros +pub trait CheckedFixedSchema: CheckedBackwardsCompatibleSchema {} +pub trait CheckedBackwardsCompatibleSchema {} + +// NOTE: Types are in sbor-tests/tests/schema_comparison.rs diff --git a/sbor/src/schema/schema_comparison/schema_comparison_kernel.rs b/sbor/src/schema/schema_comparison/schema_comparison_kernel.rs new file mode 100644 index 00000000000..c7d064eda75 --- /dev/null +++ b/sbor/src/schema/schema_comparison/schema_comparison_kernel.rs @@ -0,0 +1,1043 @@ +use super::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ComparisonTypeRoot { + pub(crate) name: String, + pub(crate) base_type_id: LocalTypeId, + pub(crate) compared_type_id: LocalTypeId, +} + +pub(crate) struct SchemaComparisonKernel<'s, 'o, S: CustomSchema> { + base_schema: &'s Schema, + compared_schema: &'s Schema, + settings: &'o SchemaComparisonSettings, + /// A matrix tracking if two types have been compared shallowly + cached_located_type_comparisons: + NonIterMap<(LocalTypeId, LocalTypeId), LocatedTypeComparisonResult>, + /// A list of pending comparisons + pending_comparison_work_list: Vec, + /// Used to cheaply capture whether we've seen a local type, for completeness checking + base_local_types_reachable_from_a_root: NonIterMap, + /// Used to cheaply capture whether we've seen a local type, for completeness checking + compared_local_types_reachable_from_a_root: NonIterMap, + + /// Tracking all the errors discovered + errors: ErrorsAggregator, +} + +impl<'s, 'o, S: CustomSchema> SchemaComparisonKernel<'s, 'o, S> { + /// This assumes the schemas provided are valid, and can panic if they're not. + /// + /// NOTE: This is NOT designed to be used: + /// * In situations where the schemas are untrusted. + /// The worst case runtime performance here for malicious schemas is O((N + W)^2) + /// where N is the number of schema types and W is the number of well known types. + /// * In situations where performance matters. + /// Whilst the expected performance for normal schemas is O(N), this + /// isn't designed in a very optimal way (e.g. there are lots of allocations, some + /// cloning etc). + pub fn new( + base_schema: &'s Schema, + compared_schema: &'s Schema, + settings: &'o SchemaComparisonSettings, + ) -> Self { + Self { + base_schema, + compared_schema, + settings, + cached_located_type_comparisons: Default::default(), + pending_comparison_work_list: Default::default(), + base_local_types_reachable_from_a_root: Default::default(), + compared_local_types_reachable_from_a_root: Default::default(), + errors: ErrorsAggregator::new(), + } + } + + pub fn compare_using_fixed_type_roots( + mut self, + type_roots: &[ComparisonTypeRoot], + ) -> SchemaComparisonResult<'s, S> { + // NOTE: While providing 0 type_roots is typically an accident, it isn't technically incorrect. + // There are some auto-generated cases (e.g. an empty interface) where it may make sense / be easiest + // to check an empty list of type roots. + for ComparisonTypeRoot { + name, + base_type_id, + compared_type_id, + } in type_roots.iter() + { + self.deep_compare_root_types(name, base_type_id, compared_type_id); + self.mark_root_reachable_base_types(base_type_id); + self.mark_root_reachable_compared_types(compared_type_id); + } + + self.check_for_completeness(); + self.into_result() + } + + pub fn compare_using_named_type_roots( + mut self, + base_type_roots: &IndexMap, + compared_type_roots: &IndexMap, + ) -> SchemaComparisonResult<'s, S> { + // First, let's loop through the base types, and compare them against the corresponding compared types. + // It is an error for a base named type not to exist in the corresponding compared list. + for (base_root_type_name, base_type_id) in base_type_roots.iter() { + if let Some(compared_type_id) = compared_type_roots.get(base_root_type_name) { + self.deep_compare_root_types(base_root_type_name, base_type_id, compared_type_id); + self.mark_root_reachable_base_types(base_type_id); + self.mark_root_reachable_compared_types(compared_type_id); + } else { + self.errors.record_error_with_unvisited_location( + SchemaComparisonErrorDetail::NamedRootTypeMissingInComparedSchema { + root_type_name: base_root_type_name.clone(), + }, + ); + self.mark_root_reachable_base_types(base_type_id); + } + } + + // We now loop through the compared types not covered in the above loop over base types + for (compared_root_type_name, compared_type_id) in compared_type_roots.iter() { + if !base_type_roots.contains_key(compared_root_type_name) { + if !self + .settings + .completeness + .allow_compared_to_have_more_root_types + { + self.errors.record_error_with_unvisited_location( + SchemaComparisonErrorDetail::DisallowedNewRootTypeInComparedSchema { + root_type_name: compared_root_type_name.clone(), + }, + ); + } + self.mark_root_reachable_compared_types(compared_type_id); + } + } + + self.check_for_completeness(); + self.into_result() + } + + fn deep_compare_root_types( + &mut self, + root_type_identifier: &str, + base_type_id: &LocalTypeId, + compared_type_id: &LocalTypeId, + ) { + self.pending_comparison_work_list + .push(PendingComparisonRequest { + base_type_id: *base_type_id, + compared_type_id: *compared_type_id, + ancestor_path: TypeAncestorPath { + root_type_identifier: root_type_identifier.to_string(), + ancestor_path: vec![], + }, + }); + // Run all comparison analysis we can perform. + // Due to the cache of shallow results over (TypesInBase * TypesInCompared), this must end. + while let Some(request) = self.pending_comparison_work_list.pop() { + self.run_single_type_comparison(request); + } + } + + fn mark_root_reachable_base_types(&mut self, root_base_type_id: &LocalTypeId) { + // Due to the cache, we do max O(TypesInBase) work. + // Note that reachability analysis needs to be performed separately to comparison analysis, because + // sometimes with comparisons of MyTuple(A) and MyTuple(B1, B2), we still want to perform reachability + // analysis on A, B1 and B2; but we can't make any sensible comparisons between them. + let LocalTypeId::SchemaLocalIndex(root_base_local_index) = root_base_type_id else { + return; + }; + let mut base_reachability_work_list = vec![*root_base_local_index]; + while let Some(base_type_index) = base_reachability_work_list.pop() { + match self + .base_local_types_reachable_from_a_root + .entry(base_type_index) + { + hash_map::Entry::Occupied(_) => continue, + hash_map::Entry::Vacant(vacant_entry) => vacant_entry.insert(()), + }; + let type_id = LocalTypeId::SchemaLocalIndex(base_type_index); + let type_kind = self + .base_schema + .resolve_type_kind(type_id) + .unwrap_or_else(|| { + panic!("Invalid base schema - type kind for {type_id:?} not found") + }); + visit_type_kind_children(type_kind, |_child_locator, child_type_kind| { + if let LocalTypeId::SchemaLocalIndex(local_index) = child_type_kind { + base_reachability_work_list.push(local_index); + }; + }) + } + } + + fn mark_root_reachable_compared_types(&mut self, root_compared_type_id: &LocalTypeId) { + let LocalTypeId::SchemaLocalIndex(root_compared_local_index) = root_compared_type_id else { + return; + }; + let mut compared_reachability_work_list = vec![*root_compared_local_index]; + while let Some(compared_local_index) = compared_reachability_work_list.pop() { + match self + .compared_local_types_reachable_from_a_root + .entry(compared_local_index) + { + hash_map::Entry::Occupied(_) => continue, + hash_map::Entry::Vacant(vacant_entry) => vacant_entry.insert(()), + }; + let type_id = LocalTypeId::SchemaLocalIndex(compared_local_index); + let type_kind = self + .compared_schema + .resolve_type_kind(type_id) + .unwrap_or_else(|| { + panic!("Invalid compared schema - type kind for {type_id:?} not found") + }); + visit_type_kind_children(type_kind, |_child_locator, child_type_kind| { + if let LocalTypeId::SchemaLocalIndex(local_index) = child_type_kind { + compared_reachability_work_list.push(local_index); + }; + }) + } + } + + fn run_single_type_comparison(&mut self, request: PendingComparisonRequest) { + let PendingComparisonRequest { + base_type_id, + compared_type_id, + ancestor_path: example_location, + } = request; + let status_key = (base_type_id, compared_type_id); + + if self + .cached_located_type_comparisons + .contains_key(&status_key) + { + return; + } + + let result = self.compare_types_internal(&example_location, base_type_id, compared_type_id); + for (child_locator, child_base_type_id, child_compared_type_id) in + result.child_checks_required + { + if self + .cached_located_type_comparisons + .contains_key(&(child_base_type_id, child_compared_type_id)) + { + continue; + } + let child_example_location = TypeAncestorPath { + root_type_identifier: example_location.root_type_identifier.clone(), + ancestor_path: { + let mut path = example_location.ancestor_path.clone(); + path.push(SchemaComparisonPathSegment::of( + &base_type_id, + &compared_type_id, + child_locator, + )); + path + }, + }; + self.pending_comparison_work_list + .push(PendingComparisonRequest { + base_type_id: child_base_type_id, + compared_type_id: child_compared_type_id, + ancestor_path: child_example_location, + }) + } + let located_result = LocatedTypeComparisonResult { + shallow_status: result.shallow_status, + example_location, + }; + self.cached_located_type_comparisons + .insert(status_key, located_result); + } + + fn compare_types_internal( + &mut self, + example_location: &TypeAncestorPath, + base_type_id: LocalTypeId, + compared_type_id: LocalTypeId, + ) -> ShallowTypeComparisonResult { + // Quick short-circuit when comparing equal well-known types + match (base_type_id, compared_type_id) { + ( + LocalTypeId::WellKnown(base_well_known), + LocalTypeId::WellKnown(compared_well_known), + ) => { + if base_well_known == compared_well_known { + return ShallowTypeComparisonResult::no_child_checks_required( + TypeComparisonStatus::Pass, + ); + } + } + _ => {} + } + + // Load type data from each schema + let (base_type_kind, base_type_metadata, base_type_validation) = self + .base_schema + .resolve_type_data(base_type_id) + .unwrap_or_else(|| { + panic!("Base schema was not valid - no type data for {base_type_id:?}") + }); + let (compared_type_kind, compared_type_metadata, compared_type_validation) = self + .compared_schema + .resolve_type_data(compared_type_id) + .unwrap_or_else(|| { + panic!("Compared schema was not valid - no type data for {compared_type_id:?}") + }); + + // Type Kind Comparison + let further_checks_required = { + let TypeKindComparisonResult { + errors, + children_needing_checking, + } = self.compare_type_kind_internal(base_type_kind, compared_type_kind); + + if errors.len() > 0 { + for error in errors { + self.errors.record_error( + error, + example_location, + base_type_id, + compared_type_id, + ); + } + // If the type kind comparison fails, then the metadata and validation comparisons aren't helpful information, + // so we can abort here without further tests. + return ShallowTypeComparisonResult { + shallow_status: TypeComparisonStatus::Failure, + child_checks_required: children_needing_checking, + }; + } + + children_needing_checking + }; + + let mut error_recorded = false; + + // Type Metadata Comparison + { + let TypeMetadataComparisonResult { errors } = self.compare_type_metadata_internal( + base_type_kind, + base_type_metadata, + compared_type_metadata, + ); + + for error in errors { + error_recorded = true; + self.errors + .record_error(error, example_location, base_type_id, compared_type_id); + } + } + + // Type Validation Comparison + { + let TypeValidationComparisonResult { errors } = self + .compare_type_validation_internal(base_type_validation, compared_type_validation); + + for error in errors { + error_recorded = true; + self.errors + .record_error(error, example_location, base_type_id, compared_type_id); + } + } + + return ShallowTypeComparisonResult { + shallow_status: if error_recorded { + TypeComparisonStatus::Failure + } else { + TypeComparisonStatus::Pass + }, + child_checks_required: further_checks_required, + }; + } + + fn compare_type_kind_internal( + &self, + base_type_kind: &LocalTypeKind, + compared_type_kind: &LocalTypeKind, + ) -> TypeKindComparisonResult { + // The returned children to check should be driven from the base type kind, + // because these are the children where we have to maintain backwards-compatibility + + let mut result = TypeKindComparisonResult::new(); + let settings = self.settings.structure; + if *compared_type_kind == TypeKind::Any + && *base_type_kind != TypeKind::Any + && settings.allow_replacing_with_any + { + // If we allow replacing any type with TypeKind::Any, and the new schema is Any, then the check is valid. + // + // That said, we should still check any children against Any: + // * In case they fail other checks (e.g. ancestor types on the base side required particular type names, + // which have now disappeared because the Compared side is Any) + // * To ensure we pass completeness checks on the base side + visit_type_kind_children(&base_type_kind, |child_type_locator, child_type_kind| { + result.add_child_to_check( + child_type_locator, + child_type_kind, + LocalTypeId::WellKnown(ANY_TYPE), + ); + }); + return result; + } + + match base_type_kind { + TypeKind::Any + | TypeKind::Bool + | TypeKind::I8 + | TypeKind::I16 + | TypeKind::I32 + | TypeKind::I64 + | TypeKind::I128 + | TypeKind::U8 + | TypeKind::U16 + | TypeKind::U32 + | TypeKind::U64 + | TypeKind::U128 + | TypeKind::String => { + if compared_type_kind != base_type_kind { + return result.with_mismatch_error(base_type_kind, compared_type_kind); + } + } + TypeKind::Array { + element_type: base_element_type, + } => { + let TypeKind::Array { + element_type: compared_element_type, + } = compared_type_kind + else { + return result.with_mismatch_error(base_type_kind, compared_type_kind); + }; + result.add_child_to_check( + ChildTypeLocator::Array {}, + *base_element_type, + *compared_element_type, + ); + } + TypeKind::Tuple { + field_types: base_field_types, + } => { + let TypeKind::Tuple { + field_types: compared_field_types, + } = compared_type_kind + else { + return result.with_mismatch_error(base_type_kind, compared_type_kind); + }; + if base_field_types.len() != compared_field_types.len() { + return result.with_error( + SchemaComparisonErrorDetail::TupleFieldCountMismatch { + base_field_count: base_field_types.len(), + compared_field_count: compared_field_types.len(), + }, + ); + } + let matched_field_types = base_field_types + .iter() + .cloned() + .zip(compared_field_types.iter().cloned()) + .enumerate(); + for (field_index, (base, compared)) in matched_field_types { + result.add_child_to_check( + ChildTypeLocator::Tuple { field_index }, + base, + compared, + ); + } + } + TypeKind::Enum { + variants: base_variants, + } => { + let TypeKind::Enum { + variants: compared_variants, + } = compared_type_kind + else { + return result.with_mismatch_error(base_type_kind, compared_type_kind); + }; + + let base_variants_missing_in_compared: IndexSet<_> = base_variants + .keys() + .filter(|base_variant_id| !compared_variants.contains_key(*base_variant_id)) + .cloned() + .collect(); + let compared_variants_missing_in_base: IndexSet<_> = compared_variants + .keys() + .filter(|compared_variant_id| !base_variants.contains_key(*compared_variant_id)) + .cloned() + .collect(); + + if base_variants_missing_in_compared.len() > 0 + || (compared_variants_missing_in_base.len() > 0 + && !settings.allow_new_enum_variants) + { + result.add_error(SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { + base_variants_missing_in_compared, + compared_variants_missing_in_base, + }); + } + + for (discriminator, base_field_type_ids) in base_variants.iter() { + let Some(compared_field_type_ids) = compared_variants.get(discriminator) else { + // We have already output a EnumSupportedVariantsMismatch error above for this. + // But let's continue to see if we can match / compare further variants structurally, + // to get as many errors as we can. + continue; + }; + let discriminator = *discriminator; + + if base_field_type_ids.len() != compared_field_type_ids.len() { + result.add_error( + SchemaComparisonErrorDetail::EnumVariantFieldCountMismatch { + variant_discriminator: discriminator, + base_field_count: base_field_type_ids.len(), + compared_field_count: compared_field_type_ids.len(), + }, + ); + } else { + let paired_child_ids = base_field_type_ids + .iter() + .zip(compared_field_type_ids.iter()) + .enumerate(); + for (field_index, (base_child_type_id, compared_child_type_id)) in + paired_child_ids + { + result.add_child_to_check( + ChildTypeLocator::EnumVariant { + discriminator, + field_index, + }, + *base_child_type_id, + *compared_child_type_id, + ); + } + } + } + } + TypeKind::Map { + key_type: base_key_type, + value_type: base_value_type, + } => { + let TypeKind::Map { + key_type: compared_key_type, + value_type: compared_value_type, + } = compared_type_kind + else { + return result.with_mismatch_error(base_type_kind, compared_type_kind); + }; + + result.add_child_to_check( + ChildTypeLocator::Map { + entry_part: MapEntryPart::Key, + }, + *base_key_type, + *compared_key_type, + ); + result.add_child_to_check( + ChildTypeLocator::Map { + entry_part: MapEntryPart::Value, + }, + *base_value_type, + *compared_value_type, + ); + } + // Assume for now that custom types are leaf types. + // Therefore we can directly run equality on the types, like the simple types. + TypeKind::Custom(_) => { + if compared_type_kind != base_type_kind { + return result.with_mismatch_error(base_type_kind, compared_type_kind); + } + } + } + + result + } + + fn compare_type_metadata_internal( + &self, + base_type_kind: &LocalTypeKind, + base_type_metadata: &TypeMetadata, + compared_type_metadata: &TypeMetadata, + ) -> TypeMetadataComparisonResult { + let settings = self.settings.metadata; + let mut result = TypeMetadataComparisonResult::new(); + if !settings.checks_required() { + return result; + } + if let Err(error) = NameChange::of_changed_option( + base_type_metadata.type_name.as_deref(), + compared_type_metadata.type_name.as_deref(), + ) + .validate(settings.type_name_changes) + { + result.add_error(SchemaComparisonErrorDetail::TypeNameChangeError(error)); + } + + // NOTE: For these tests, we assume that the schema is valid - that is, that the type metadata + // aligns with the underlying type kinds. + // Also, we have already tested for consistency of the compared type kind against the base type kind. + // So we can drive field/variant metadata iteration off the base type kind. + match base_type_kind { + TypeKind::Tuple { field_types } => { + for field_index in 0..field_types.len() { + if let Err(error) = NameChange::of_changed_option( + base_type_metadata.get_field_name(field_index), + compared_type_metadata.get_field_name(field_index), + ) + .validate(settings.field_name_changes) + { + result.add_error(SchemaComparisonErrorDetail::FieldNameChangeError { + field_index, + error, + }); + } + } + } + TypeKind::Enum { variants } => { + for (variant_discriminator, base_variant_types) in variants.iter() { + let variant_discriminator = *variant_discriminator; + let base_variant_metadata = base_type_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Base schema was not valid - base did not have enum child names for an enum variant"); + let compared_variant_metadata = compared_type_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Compared schema was not valid - base and compared agreed on structural equality of an enum, but compared did not have variant metadata for a base variant"); + + if let Err(error) = NameChange::of_changed_option( + base_variant_metadata.type_name.as_deref(), + compared_variant_metadata.type_name.as_deref(), + ) + .validate(settings.field_name_changes) + { + result.add_error(SchemaComparisonErrorDetail::EnumVariantNameChangeError { + variant_discriminator, + error, + }); + } + + for field_index in 0..base_variant_types.len() { + if let Err(error) = NameChange::of_changed_option( + base_variant_metadata.get_field_name(field_index), + compared_variant_metadata.get_field_name(field_index), + ) + .validate(settings.field_name_changes) + { + result.add_error( + SchemaComparisonErrorDetail::EnumVariantFieldNameChangeError { + variant_discriminator, + field_index, + error, + }, + ); + } + } + } + } + _ => { + // We can assume the schema is valid, therefore the only valid value is ChildNames::None + // So validation passes trivially + } + } + + result + } + + fn compare_type_validation_internal( + &self, + base_type_validation: &TypeValidation, + compared_type_validation: &TypeValidation, + ) -> TypeValidationComparisonResult { + let settings = self.settings.validation; + let mut result = TypeValidationComparisonResult::new(); + + let validation_change = match (base_type_validation, compared_type_validation) { + (TypeValidation::None, TypeValidation::None) => ValidationChange::Unchanged, + // Strictly a provided validation might be equivalent to None, for example: + // (for example NumericValidation { min: None, max: None } or NumericValidation:: { min: 0, max: 255 }) + // but for now assume that it's different + (_, TypeValidation::None) => ValidationChange::Weakened, + (TypeValidation::None, _) => ValidationChange::Strengthened, + // Now test equal validations + (TypeValidation::I8(base), TypeValidation::I8(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::I16(base), TypeValidation::I16(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::I32(base), TypeValidation::I32(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::I64(base), TypeValidation::I64(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::I128(base), TypeValidation::I128(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U8(base), TypeValidation::U8(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U16(base), TypeValidation::U16(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U32(base), TypeValidation::U32(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U64(base), TypeValidation::U64(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::U128(base), TypeValidation::U128(compared)) => { + NumericValidation::compare(base, compared) + } + (TypeValidation::String(base), TypeValidation::String(compared)) => { + LengthValidation::compare(base, compared) + } + (TypeValidation::Array(base), TypeValidation::Array(compared)) => { + LengthValidation::compare(base, compared) + } + (TypeValidation::Map(base), TypeValidation::Map(compared)) => { + LengthValidation::compare(base, compared) + } + (TypeValidation::Custom(base), TypeValidation::Custom(compared)) => { + <::CustomTypeValidation as CustomTypeValidation>::compare( + base, compared, + ) + } + // Otherwise assume they are incomparable + _ => ValidationChange::Incomparable, + }; + let is_valid = match validation_change { + ValidationChange::Unchanged => true, + ValidationChange::Strengthened => false, + ValidationChange::Weakened => settings.allow_validation_weakening, + ValidationChange::Incomparable => false, + }; + if !is_valid { + result.add_error(SchemaComparisonErrorDetail::TypeValidationChangeError { + change: validation_change, + old: base_type_validation.clone(), + new: compared_type_validation.clone(), + }) + } + result + } + + fn check_for_completeness(&mut self) { + if !self + .settings + .completeness + .allow_root_unreachable_types_in_base_schema + { + if self.base_local_types_reachable_from_a_root.len() + < self.base_schema.type_metadata.len() + { + for (local_type_index, metadata) in + self.base_schema.type_metadata.iter().enumerate() + { + if !self + .base_local_types_reachable_from_a_root + .contains_key(&local_type_index) + { + let type_name = metadata.type_name.as_ref().map(|n| n.clone().into_owned()); + self.errors.record_error_with_unvisited_location( + SchemaComparisonErrorDetail::TypeUnreachableFromRootInBaseSchema { + local_type_index, + type_name, + }, + ) + } + } + } + } + if !self + .settings + .completeness + .allow_root_unreachable_types_in_compared_schema + { + if self.compared_local_types_reachable_from_a_root.len() + < self.compared_schema.type_metadata.len() + { + for (local_type_index, metadata) in + self.compared_schema.type_metadata.iter().enumerate() + { + if !self + .compared_local_types_reachable_from_a_root + .contains_key(&local_type_index) + { + let type_name = metadata.type_name.as_ref().map(|n| n.clone().into_owned()); + self.errors.record_error_with_unvisited_location( + SchemaComparisonErrorDetail::TypeUnreachableFromRootInComparedSchema { + local_type_index, + type_name, + }, + ) + } + } + } + } + } + + fn into_result(self) -> SchemaComparisonResult<'s, S> { + SchemaComparisonResult { + base_schema: self.base_schema, + compared_schema: self.compared_schema, + errors: self.errors.errors, + } + } +} + +fn visit_type_kind_children>( + type_kind: &TypeKind, + mut visitor: impl FnMut(ChildTypeLocator, LocalTypeId), +) { + return match type_kind { + TypeKind::Any + | TypeKind::Bool + | TypeKind::I8 + | TypeKind::I16 + | TypeKind::I32 + | TypeKind::I64 + | TypeKind::I128 + | TypeKind::U8 + | TypeKind::U16 + | TypeKind::U32 + | TypeKind::U64 + | TypeKind::U128 + | TypeKind::String => {} + TypeKind::Array { element_type } => { + visitor(ChildTypeLocator::Array {}, *element_type); + } + TypeKind::Tuple { field_types } => { + for (field_index, field_type) in field_types.iter().enumerate() { + visitor(ChildTypeLocator::Tuple { field_index }, *field_type) + } + } + TypeKind::Enum { variants } => { + for (discriminator, field_types) in variants { + for (field_index, field_type) in field_types.iter().enumerate() { + visitor( + ChildTypeLocator::EnumVariant { + discriminator: *discriminator, + field_index, + }, + *field_type, + ) + } + } + } + TypeKind::Map { + key_type, + value_type, + } => { + visitor( + ChildTypeLocator::Map { + entry_part: MapEntryPart::Key, + }, + *key_type, + ); + visitor( + ChildTypeLocator::Map { + entry_part: MapEntryPart::Value, + }, + *value_type, + ); + } + // At present, assume that custom types are leaf types. + TypeKind::Custom(_) => {} + }; +} + +struct ErrorsAggregator { + errors: Vec>, +} + +impl ErrorsAggregator { + fn new() -> Self { + Self { errors: vec![] } + } + + fn record_error( + &mut self, + error_detail: SchemaComparisonErrorDetail, + example_location: &TypeAncestorPath, + base_type_id: LocalTypeId, + compared_type_id: LocalTypeId, + ) { + self.errors.push(SchemaComparisonError { + error_detail, + example_location: Some(TypeFullPath { + root_type_identifier: example_location.root_type_identifier.clone(), + ancestor_path: example_location.ancestor_path.clone(), + leaf_base_type_id: base_type_id, + leaf_compared_type_id: compared_type_id, + }), + }) + } + + fn record_error_with_unvisited_location( + &mut self, + error_detail: SchemaComparisonErrorDetail, + ) { + self.errors.push(SchemaComparisonError { + error_detail, + example_location: None, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct PendingComparisonRequest { + base_type_id: LocalTypeId, + compared_type_id: LocalTypeId, + ancestor_path: TypeAncestorPath, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct LocatedTypeComparisonResult { + shallow_status: TypeComparisonStatus, + example_location: TypeAncestorPath, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TypeAncestorPath { + root_type_identifier: String, + ancestor_path: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SchemaComparisonPathSegment { + pub(crate) parent_base_type_id: LocalTypeId, + pub(crate) parent_compared_type_id: LocalTypeId, + pub(crate) child_locator: ChildTypeLocator, +} + +impl SchemaComparisonPathSegment { + pub fn of( + parent_base_type_id: &LocalTypeId, + parent_compared_type_id: &LocalTypeId, + child_locator: ChildTypeLocator, + ) -> Self { + Self { + parent_base_type_id: *parent_base_type_id, + parent_compared_type_id: *parent_compared_type_id, + child_locator, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ChildTypeLocator { + Tuple { + field_index: usize, + }, + EnumVariant { + discriminator: u8, + field_index: usize, + }, + Array {}, // Unlike values, we don't have an index + Map { + entry_part: MapEntryPart, + }, // Unlike values, we don't have an index +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum TypeComparisonStatus { + Pass, + Failure, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct TypeFullPath { + pub(crate) root_type_identifier: String, + pub(crate) ancestor_path: Vec, + pub(crate) leaf_base_type_id: LocalTypeId, + pub(crate) leaf_compared_type_id: LocalTypeId, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ShallowTypeComparisonResult { + shallow_status: TypeComparisonStatus, + child_checks_required: Vec<(ChildTypeLocator, LocalTypeId, LocalTypeId)>, +} + +impl ShallowTypeComparisonResult { + pub fn no_child_checks_required(status: TypeComparisonStatus) -> Self { + Self { + shallow_status: status, + child_checks_required: vec![], + } + } +} + +struct TypeKindComparisonResult { + children_needing_checking: Vec<(ChildTypeLocator, LocalTypeId, LocalTypeId)>, + errors: Vec>, +} + +impl TypeKindComparisonResult { + fn new() -> Self { + Self { + children_needing_checking: vec![], + errors: vec![], + } + } + + fn add_error(&mut self, error: SchemaComparisonErrorDetail) { + self.errors.push(error) + } + + fn with_mismatch_error( + mut self, + base_type_kind: &LocalTypeKind, + compared_type_kind: &LocalTypeKind, + ) -> Self { + self.add_error(SchemaComparisonErrorDetail::TypeKindMismatch { + base: base_type_kind.label(), + compared: compared_type_kind.label(), + }); + self + } + + fn with_error(mut self, error: SchemaComparisonErrorDetail) -> Self { + self.add_error(error); + self + } + + fn add_child_to_check( + &mut self, + child_locator: ChildTypeLocator, + base_type_id: LocalTypeId, + compared_type_id: LocalTypeId, + ) { + self.children_needing_checking + .push((child_locator, base_type_id, compared_type_id)); + } +} + +struct TypeMetadataComparisonResult { + errors: Vec>, +} + +impl TypeMetadataComparisonResult { + fn new() -> Self { + Self { errors: vec![] } + } + + fn add_error(&mut self, error: SchemaComparisonErrorDetail) { + self.errors.push(error) + } +} + +struct TypeValidationComparisonResult { + errors: Vec>, +} + +impl TypeValidationComparisonResult { + fn new() -> Self { + Self { errors: vec![] } + } + + fn add_error(&mut self, error: SchemaComparisonErrorDetail) { + self.errors.push(error) + } +} diff --git a/sbor/src/schema/schema_comparison/schema_comparison_result.rs b/sbor/src/schema/schema_comparison/schema_comparison_result.rs new file mode 100644 index 00000000000..e7de8925858 --- /dev/null +++ b/sbor/src/schema/schema_comparison/schema_comparison_result.rs @@ -0,0 +1,557 @@ +use super::*; + +pub enum NameChange<'a> { + Unchanged, + NameAdded { + new_name: &'a str, + }, + NameRemoved { + old_name: &'a str, + }, + NameChanged { + old_name: &'a str, + new_name: &'a str, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NameChangeError { + change: OwnedNameChange, + rule_broken: NameChangeRule, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OwnedNameChange { + Unchanged, + NameAdded { new_name: String }, + NameRemoved { old_name: String }, + NameChanged { old_name: String, new_name: String }, +} + +impl<'a> NameChange<'a> { + pub fn of_changed_option(from: Option<&'a str>, to: Option<&'a str>) -> Self { + match (from, to) { + (Some(old_name), Some(new_name)) if old_name == new_name => NameChange::Unchanged, + (Some(old_name), Some(new_name)) => NameChange::NameChanged { old_name, new_name }, + (Some(old_name), None) => NameChange::NameRemoved { old_name }, + (None, Some(new_name)) => NameChange::NameAdded { new_name }, + (None, None) => NameChange::Unchanged, + } + } + + pub fn validate(&self, rule: NameChangeRule) -> Result<(), NameChangeError> { + let passes = match (self, rule) { + (NameChange::Unchanged, _) => true, + (_, NameChangeRule::AllowAllChanges) => true, + (_, NameChangeRule::DisallowAllChanges) => false, + (NameChange::NameAdded { .. }, NameChangeRule::AllowAddingNames) => true, + (NameChange::NameRemoved { .. }, NameChangeRule::AllowAddingNames) => false, + (NameChange::NameChanged { .. }, NameChangeRule::AllowAddingNames) => false, + }; + if passes { + Ok(()) + } else { + Err(NameChangeError { + rule_broken: rule, + change: self.into_owned(), + }) + } + } + + fn into_owned(&self) -> OwnedNameChange { + match *self { + NameChange::Unchanged => OwnedNameChange::Unchanged, + NameChange::NameAdded { new_name } => OwnedNameChange::NameAdded { + new_name: new_name.to_string(), + }, + NameChange::NameRemoved { old_name } => OwnedNameChange::NameRemoved { + old_name: old_name.to_string(), + }, + NameChange::NameChanged { old_name, new_name } => OwnedNameChange::NameChanged { + old_name: old_name.to_string(), + new_name: new_name.to_string(), + }, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ValidationChange { + Unchanged, + Strengthened, + Weakened, + Incomparable, +} + +impl ValidationChange { + pub fn combine(self, other: ValidationChange) -> Self { + match (self, other) { + (ValidationChange::Incomparable, _) => ValidationChange::Incomparable, + (_, ValidationChange::Incomparable) => ValidationChange::Incomparable, + (ValidationChange::Unchanged, other) => other, + (other, ValidationChange::Unchanged) => other, + (ValidationChange::Strengthened, ValidationChange::Strengthened) => { + ValidationChange::Strengthened + } + (ValidationChange::Strengthened, ValidationChange::Weakened) => { + ValidationChange::Incomparable + } + (ValidationChange::Weakened, ValidationChange::Strengthened) => { + ValidationChange::Incomparable + } + (ValidationChange::Weakened, ValidationChange::Weakened) => ValidationChange::Weakened, + } + } +} + +#[must_use = "You must read / handle the comparison result"] +pub struct SchemaComparisonResult<'s, S: CustomSchema> { + pub(crate) base_schema: &'s Schema, + pub(crate) compared_schema: &'s Schema, + pub(crate) errors: Vec>, +} + +impl<'s, S: CustomSchema> SchemaComparisonResult<'s, S> { + pub fn is_valid(&self) -> bool { + self.errors.len() == 0 + } + + pub fn error_message( + &self, + base_schema_name: &str, + compared_schema_name: &str, + ) -> Option { + if self.errors.len() == 0 { + return None; + } + let mut output = String::new(); + writeln!( + &mut output, + "Schema comparison FAILED between base schema ({}) and compared schema ({}) with {} {}:", + base_schema_name, + compared_schema_name, + self.errors.len(), + if self.errors.len() == 1 { "error" } else { "errors" }, + ).unwrap(); + for error in &self.errors { + write!(&mut output, "- ").unwrap(); + error + .write_against_schemas(&mut output, &self.base_schema, &self.compared_schema) + .unwrap(); + writeln!(&mut output).unwrap(); + } + Some(output) + } + + pub fn assert_valid(&self, base_schema_name: &str, compared_schema_name: &str) { + if let Some(error_message) = self.error_message(base_schema_name, compared_schema_name) { + panic!("{}", error_message); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SchemaComparisonError { + pub(crate) error_detail: SchemaComparisonErrorDetail, + pub(crate) example_location: Option, +} + +impl SchemaComparisonError { + fn write_against_schemas( + &self, + f: &mut F, + base_schema: &Schema, + compared_schema: &Schema, + ) -> core::fmt::Result { + if let Some(location) = &self.example_location { + let (base_type_kind, base_metadata, _) = base_schema + .resolve_type_data(location.leaf_base_type_id) + .expect("Invalid base schema - Could not find data for base type"); + let (compared_type_kind, compared_metadata, _) = compared_schema + .resolve_type_data(location.leaf_compared_type_id) + .expect("Invalid compared schema - Could not find data for compared type"); + + self.error_detail.write_with_context( + f, + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, + )?; + write!(f, " under {} at path ", location.root_type_identifier)?; + (location, base_schema, compared_schema, &self.error_detail).write_path(f)?; + } else { + write!(f, "{:?}", &self.error_detail)?; + } + Ok(()) + } +} + +fn combine_optional_names(base_name: Option<&str>, compared_name: Option<&str>) -> Option { + match (base_name, compared_name) { + (Some(base_name), Some(compared_name)) if base_name == compared_name => { + Some(base_name.to_string()) + } + (Some(base_name), Some(compared_name)) => Some(format!("{base_name}|{compared_name}")), + (Some(base_name), None) => Some(format!("{base_name}|anon")), + (None, Some(compared_name)) => Some(format!("anon|{compared_name}")), + (None, None) => None, + } +} + +fn combine_type_names( + base_metadata: &TypeMetadata, + base_type_kind: &LocalTypeKind, + compared_metadata: &TypeMetadata, + compared_type_kind: &LocalTypeKind, +) -> String { + if let Some(combined_name) = + combine_optional_names(base_metadata.get_name(), compared_metadata.get_name()) + { + return combined_name; + } + let base_category_name = base_type_kind.category_name(); + let compared_category_name = compared_type_kind.category_name(); + if base_category_name == compared_category_name { + base_category_name.to_string() + } else { + format!("{base_category_name}|{compared_category_name}") + } +} + +impl<'s, 'a, S: CustomSchema> PathAnnotate + for ( + &'a TypeFullPath, + &'a Schema, + &'a Schema, + &'a SchemaComparisonErrorDetail, + ) +{ + fn iter_ancestor_path(&self) -> Box> + '_> { + let (full_path, base_schema, compared_schema, _error_detail) = *self; + + let iterator = full_path.ancestor_path.iter().map(|path_segment| { + let base_type_id = path_segment.parent_base_type_id; + let compared_type_id = path_segment.parent_compared_type_id; + + let (base_type_kind, base_metadata, _) = base_schema + .resolve_type_data(base_type_id) + .expect("Invalid base schema - Could not find data for base type"); + let (compared_type_kind, compared_metadata, _) = compared_schema + .resolve_type_data(compared_type_id) + .expect("Invalid compared schema - Could not find data for compared type"); + + let name = Cow::Owned(combine_type_names::( + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, + )); + + let container = match path_segment.child_locator { + ChildTypeLocator::Tuple { field_index } => { + let field_name = combine_optional_names( + base_metadata.get_field_name(field_index), + compared_metadata.get_field_name(field_index), + ) + .map(Cow::Owned); + AnnotatedSborAncestorContainer::Tuple { + field_index, + field_name, + } + } + ChildTypeLocator::EnumVariant { + discriminator, + field_index, + } => { + let base_variant_metadata = base_metadata + .get_enum_variant_data(discriminator) + .expect("Base schema has variant names"); + let compared_variant_metadata = compared_metadata + .get_enum_variant_data(discriminator) + .expect("Compared schema has variant names"); + let variant_name = combine_optional_names( + base_variant_metadata.get_name(), + compared_variant_metadata.get_name(), + ) + .map(Cow::Owned); + let field_name = combine_optional_names( + base_variant_metadata.get_field_name(field_index), + compared_variant_metadata.get_field_name(field_index), + ) + .map(Cow::Owned); + AnnotatedSborAncestorContainer::EnumVariant { + discriminator, + variant_name, + field_index, + field_name, + } + } + ChildTypeLocator::Array {} => AnnotatedSborAncestorContainer::Array { index: None }, + ChildTypeLocator::Map { entry_part } => AnnotatedSborAncestorContainer::Map { + index: None, + entry_part, + }, + }; + + AnnotatedSborAncestor { name, container } + }); + + Box::new(iterator) + } + + fn annotated_leaf(&self) -> Option> { + let (full_path, base_schema, compared_schema, error_detail) = *self; + let base_type_id = full_path.leaf_base_type_id; + let compared_type_id = full_path.leaf_compared_type_id; + + let (base_type_kind, base_metadata, _) = base_schema + .resolve_type_data(base_type_id) + .expect("Invalid base schema - Could not find data for base type"); + let (compared_type_kind, compared_metadata, _) = compared_schema + .resolve_type_data(compared_type_id) + .expect("Invalid compared schema - Could not find data for compared type"); + + Some(error_detail.resolve_annotated_leaf( + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, + )) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SchemaComparisonErrorDetail { + // Type kind errors + TypeKindMismatch { + base: TypeKindLabel, + compared: TypeKindLabel, + }, + TupleFieldCountMismatch { + base_field_count: usize, + compared_field_count: usize, + }, + EnumSupportedVariantsMismatch { + base_variants_missing_in_compared: IndexSet, + compared_variants_missing_in_base: IndexSet, + }, + EnumVariantFieldCountMismatch { + base_field_count: usize, + compared_field_count: usize, + variant_discriminator: u8, + }, + // Type metadata errors + TypeNameChangeError(NameChangeError), + FieldNameChangeError { + error: NameChangeError, + field_index: usize, + }, + EnumVariantNameChangeError { + error: NameChangeError, + variant_discriminator: u8, + }, + EnumVariantFieldNameChangeError { + error: NameChangeError, + variant_discriminator: u8, + field_index: usize, + }, + // Type validation error + TypeValidationChangeError { + change: ValidationChange, + old: TypeValidation, + new: TypeValidation, + }, + // Completeness errors + NamedRootTypeMissingInComparedSchema { + root_type_name: String, + }, + DisallowedNewRootTypeInComparedSchema { + root_type_name: String, + }, + TypeUnreachableFromRootInBaseSchema { + local_type_index: usize, + type_name: Option, + }, + TypeUnreachableFromRootInComparedSchema { + local_type_index: usize, + type_name: Option, + }, +} + +impl SchemaComparisonErrorDetail { + fn resolve_annotated_leaf( + &self, + base_metadata: &TypeMetadata, + base_type_kind: &LocalTypeKind, + compared_metadata: &TypeMetadata, + compared_type_kind: &LocalTypeKind, + ) -> AnnotatedSborPartialLeaf<'_> { + AnnotatedSborPartialLeaf { + name: Cow::Owned(combine_type_names::( + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, + )), + partial_leaf_locator: self + .resolve_partial_leaf_locator(base_metadata, compared_metadata), + } + } + + fn resolve_partial_leaf_locator( + &self, + base_metadata: &TypeMetadata, + compared_metadata: &TypeMetadata, + ) -> Option> { + match *self { + SchemaComparisonErrorDetail::TypeKindMismatch { .. } => None, + SchemaComparisonErrorDetail::TupleFieldCountMismatch { .. } => None, + SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { .. } => { + // This error handles multiple variants, so we can't list them here - instead we handle it in the custom debug print + None + } + SchemaComparisonErrorDetail::EnumVariantFieldCountMismatch { + variant_discriminator, + .. + } => { + let base_variant = base_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid base schema - Could not find metadata for enum variant"); + let compared_variant = compared_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid compared schema - Could not find metadata for enum variant"); + Some(AnnotatedSborPartialLeafLocator::EnumVariant { + variant_discriminator: Some(variant_discriminator), + variant_name: combine_optional_names( + base_variant.get_name(), + compared_variant.get_name(), + ) + .map(Cow::Owned), + field_index: None, + field_name: None, + }) + } + SchemaComparisonErrorDetail::TypeNameChangeError(_) => None, + SchemaComparisonErrorDetail::FieldNameChangeError { field_index, .. } => { + let base_field_name = base_metadata.get_field_name(field_index); + let compared_field_name = compared_metadata.get_field_name(field_index); + Some(AnnotatedSborPartialLeafLocator::Tuple { + field_index: Some(field_index), + field_name: combine_optional_names(base_field_name, compared_field_name) + .map(Cow::Owned), + }) + } + SchemaComparisonErrorDetail::EnumVariantNameChangeError { + variant_discriminator, + .. + } => { + let base_variant = base_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid base schema - Could not find metadata for enum variant"); + let compared_variant = compared_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid compared schema - Could not find metadata for enum variant"); + Some(AnnotatedSborPartialLeafLocator::EnumVariant { + variant_discriminator: Some(variant_discriminator), + variant_name: combine_optional_names( + base_variant.get_name(), + compared_variant.get_name(), + ) + .map(Cow::Owned), + field_index: None, + field_name: None, + }) + } + SchemaComparisonErrorDetail::EnumVariantFieldNameChangeError { + variant_discriminator, + field_index, + .. + } => { + let base_variant = base_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid base schema - Could not find metadata for enum variant"); + let compared_variant = compared_metadata + .get_enum_variant_data(variant_discriminator) + .expect("Invalid compared schema - Could not find metadata for enum variant"); + let base_field_name = base_variant.get_field_name(field_index); + let compared_field_name = compared_variant.get_field_name(field_index); + Some(AnnotatedSborPartialLeafLocator::EnumVariant { + variant_discriminator: Some(variant_discriminator), + variant_name: combine_optional_names( + base_variant.get_name(), + compared_metadata.get_name(), + ) + .map(Cow::Owned), + field_index: Some(field_index), + field_name: combine_optional_names(base_field_name, compared_field_name) + .map(Cow::Owned), + }) + } + SchemaComparisonErrorDetail::TypeValidationChangeError { .. } => None, + SchemaComparisonErrorDetail::NamedRootTypeMissingInComparedSchema { .. } => None, + SchemaComparisonErrorDetail::DisallowedNewRootTypeInComparedSchema { .. } => None, + SchemaComparisonErrorDetail::TypeUnreachableFromRootInBaseSchema { .. } => None, + SchemaComparisonErrorDetail::TypeUnreachableFromRootInComparedSchema { .. } => None, + } + } + + fn write_with_context( + &self, + f: &mut F, + base_metadata: &TypeMetadata, + base_type_kind: &LocalTypeKind, + compared_metadata: &TypeMetadata, + compared_type_kind: &LocalTypeKind, + ) -> core::fmt::Result { + self.resolve_annotated_leaf( + base_metadata, + base_type_kind, + compared_metadata, + compared_type_kind, + ) + .write(f, true)?; + write!(f, " - ")?; + + match self { + // Handle any errors where we can add extra detail + SchemaComparisonErrorDetail::EnumSupportedVariantsMismatch { + base_variants_missing_in_compared, + compared_variants_missing_in_base, + } => { + write!( + f, + "EnumSupportedVariantsMismatch {{ base_variants_missing_in_compared: {{" + )?; + for variant_discriminator in base_variants_missing_in_compared { + let variant_data = base_metadata + .get_enum_variant_data(*variant_discriminator) + .unwrap(); + write!( + f, + "{variant_discriminator}|{}", + variant_data.get_name().unwrap_or("anon") + )?; + } + write!(f, "}}, compared_variants_missing_in_base: {{")?; + for variant_discriminator in compared_variants_missing_in_base { + let variant_data = compared_metadata + .get_enum_variant_data(*variant_discriminator) + .unwrap(); + write!( + f, + "{variant_discriminator}|{}", + variant_data.get_name().unwrap_or("anon") + )?; + } + write!(f, "}}, }}")?; + } + // All other errors already have their context added in printing the annotated leaf + _ => { + write!(f, "{self:?}")?; + } + } + + Ok(()) + } +} diff --git a/sbor/src/schema/schema_comparison/schema_comparison_settings.rs b/sbor/src/schema/schema_comparison/schema_comparison_settings.rs new file mode 100644 index 00000000000..b72fc032365 --- /dev/null +++ b/sbor/src/schema/schema_comparison/schema_comparison_settings.rs @@ -0,0 +1,194 @@ +use super::*; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SchemaComparisonSettings { + pub(crate) completeness: SchemaComparisonCompletenessSettings, + pub(crate) structure: SchemaComparisonStructureSettings, + pub(crate) metadata: SchemaComparisonMetadataSettings, + pub(crate) validation: SchemaComparisonValidationSettings, +} + +impl SchemaComparisonSettings { + /// A set of defaults intended to enforce effective equality of the schemas, + /// but with clear error messages if they diverge + pub const fn require_equality() -> Self { + Self { + completeness: SchemaComparisonCompletenessSettings::enforce_type_roots_cover_schema_disallow_new_root_types(), + structure: SchemaComparisonStructureSettings::equality(), + metadata: SchemaComparisonMetadataSettings::equality(), + validation: SchemaComparisonValidationSettings::equality(), + } + } + + /// A set of defaults intended to capture a pretty tight definition of structural extension. + /// + /// This captures that: + /// * Payloads which are valid/decodable against the old schema are valid against the new schema + /// * Programmatic SBOR JSON is unchanged (that is, type/field/variant names are also unchanged) + /// + /// Notably: + /// * Type roots can be added in the compared schema, but we check that the type roots + /// provided completely cover both schemas + /// * Types must be structurally identical on their intersection, except new enum variants can be added + /// * Type metadata (e.g. names) must be identical on their intersection + /// * Type validation must be equal or strictly weaker in the new schema + pub const fn allow_extension() -> Self { + Self { + completeness: SchemaComparisonCompletenessSettings::enforce_type_roots_cover_schema_allow_new_root_types(), + structure: SchemaComparisonStructureSettings::allow_extension(), + metadata: SchemaComparisonMetadataSettings::equality(), + validation: SchemaComparisonValidationSettings::allow_weakening(), + } + } + + pub const fn completeness_settings( + mut self, + checks: SchemaComparisonCompletenessSettings, + ) -> Self { + self.completeness = checks; + self + } + + pub const fn structure_settings(mut self, checks: SchemaComparisonStructureSettings) -> Self { + self.structure = checks; + self + } + + pub const fn metadata_settings(mut self, checks: SchemaComparisonMetadataSettings) -> Self { + self.metadata = checks; + self + } + + pub const fn validation_settings(mut self, checks: SchemaComparisonValidationSettings) -> Self { + self.validation = checks; + self + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub struct SchemaComparisonCompletenessSettings { + pub(crate) allow_root_unreachable_types_in_base_schema: bool, + pub(crate) allow_root_unreachable_types_in_compared_schema: bool, + /// This is only relevant in the "multiple named roots" mode + pub(crate) allow_compared_to_have_more_root_types: bool, +} + +impl SchemaComparisonCompletenessSettings { + pub const fn allow_type_roots_not_to_cover_schema() -> Self { + Self { + allow_root_unreachable_types_in_base_schema: true, + allow_root_unreachable_types_in_compared_schema: true, + allow_compared_to_have_more_root_types: true, + } + } + + pub const fn enforce_type_roots_cover_schema_allow_new_root_types() -> Self { + Self { + allow_root_unreachable_types_in_base_schema: false, + allow_root_unreachable_types_in_compared_schema: false, + allow_compared_to_have_more_root_types: true, + } + } + + pub const fn enforce_type_roots_cover_schema_disallow_new_root_types() -> Self { + Self { + allow_root_unreachable_types_in_base_schema: false, + allow_root_unreachable_types_in_compared_schema: false, + allow_compared_to_have_more_root_types: false, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub struct SchemaComparisonStructureSettings { + pub(crate) allow_new_enum_variants: bool, + pub(crate) allow_replacing_with_any: bool, +} + +impl SchemaComparisonStructureSettings { + pub const fn equality() -> Self { + Self { + allow_new_enum_variants: false, + allow_replacing_with_any: false, + } + } + + pub const fn allow_extension() -> Self { + Self { + allow_new_enum_variants: true, + allow_replacing_with_any: true, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SchemaComparisonMetadataSettings { + pub(crate) type_name_changes: NameChangeRule, + pub(crate) field_name_changes: NameChangeRule, + pub(crate) variant_name_changes: NameChangeRule, +} + +impl SchemaComparisonMetadataSettings { + pub const fn equality() -> Self { + Self { + type_name_changes: NameChangeRule::equality(), + field_name_changes: NameChangeRule::equality(), + variant_name_changes: NameChangeRule::equality(), + } + } + + pub const fn allow_adding_names() -> Self { + Self { + type_name_changes: NameChangeRule::AllowAddingNames, + field_name_changes: NameChangeRule::AllowAddingNames, + variant_name_changes: NameChangeRule::AllowAddingNames, + } + } + + pub const fn allow_all_changes() -> Self { + Self { + type_name_changes: NameChangeRule::AllowAllChanges, + field_name_changes: NameChangeRule::AllowAllChanges, + variant_name_changes: NameChangeRule::AllowAllChanges, + } + } + + pub(crate) fn checks_required(&self) -> bool { + let everything_allowed = self.type_name_changes == NameChangeRule::AllowAllChanges + && self.field_name_changes == NameChangeRule::AllowAllChanges + && self.variant_name_changes == NameChangeRule::AllowAllChanges; + !everything_allowed + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NameChangeRule { + DisallowAllChanges, + AllowAddingNames, + AllowAllChanges, +} + +impl NameChangeRule { + pub const fn equality() -> Self { + Self::DisallowAllChanges + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SchemaComparisonValidationSettings { + pub(crate) allow_validation_weakening: bool, +} + +impl SchemaComparisonValidationSettings { + pub const fn equality() -> Self { + Self { + allow_validation_weakening: false, + } + } + + pub const fn allow_weakening() -> Self { + Self { + allow_validation_weakening: true, + } + } +} diff --git a/sbor/src/schema/type_aggregator.rs b/sbor/src/schema/type_aggregator.rs index d39ab100bbb..673c27fe719 100644 --- a/sbor/src/schema/type_aggregator.rs +++ b/sbor/src/schema/type_aggregator.rs @@ -18,7 +18,7 @@ pub fn generate_single_type_schema< SingleTypeSchema::new(schema, type_id) } -/// You may wish to use the newer `schema.generate_named_types_schema_version()` +/// You may wish to use the newer `aggregator.generate_type_collection_schema()` /// which, in tandom with `add_named_root_type_and_descendents` /// also captures named root types to give more structure to enable schema /// comparisons over time. From 16a9293c5fa22409bbb0feed5bacb10642587518 Mon Sep 17 00:00:00 2001 From: David Edey Date: Wed, 7 Aug 2024 15:21:43 +0100 Subject: [PATCH 18/19] markups: Add comment on `end` --- sbor/src/traversal/untyped/traverser.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sbor/src/traversal/untyped/traverser.rs b/sbor/src/traversal/untyped/traverser.rs index 812222b0944..08b60027b15 100644 --- a/sbor/src/traversal/untyped/traverser.rs +++ b/sbor/src/traversal/untyped/traverser.rs @@ -270,6 +270,9 @@ impl<'de, T: CustomTraversal> VecTraverser<'de, T> { } } None => { + // We are due to read another element and exit but have no parent + // This is because we have finished reading the `root` value. + // Therefore we call `end`. ActionHandler::new_from_current_offset(ancestor_path, decoder).end(config) } } From fa3d4464c13fe7ce23343c9afca7e219100c5087 Mon Sep 17 00:00:00 2001 From: David Edey Date: Wed, 7 Aug 2024 17:06:44 +0100 Subject: [PATCH 19/19] markups: Clarify comparison settings names --- .../schema_comparison_settings.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sbor/src/schema/schema_comparison/schema_comparison_settings.rs b/sbor/src/schema/schema_comparison/schema_comparison_settings.rs index b72fc032365..91e4b85de42 100644 --- a/sbor/src/schema/schema_comparison/schema_comparison_settings.rs +++ b/sbor/src/schema/schema_comparison/schema_comparison_settings.rs @@ -14,9 +14,9 @@ impl SchemaComparisonSettings { pub const fn require_equality() -> Self { Self { completeness: SchemaComparisonCompletenessSettings::enforce_type_roots_cover_schema_disallow_new_root_types(), - structure: SchemaComparisonStructureSettings::equality(), - metadata: SchemaComparisonMetadataSettings::equality(), - validation: SchemaComparisonValidationSettings::equality(), + structure: SchemaComparisonStructureSettings::require_identical_structure(), + metadata: SchemaComparisonMetadataSettings::require_identical_metadata(), + validation: SchemaComparisonValidationSettings::require_identical_validation(), } } @@ -36,7 +36,7 @@ impl SchemaComparisonSettings { Self { completeness: SchemaComparisonCompletenessSettings::enforce_type_roots_cover_schema_allow_new_root_types(), structure: SchemaComparisonStructureSettings::allow_extension(), - metadata: SchemaComparisonMetadataSettings::equality(), + metadata: SchemaComparisonMetadataSettings::require_identical_metadata(), validation: SchemaComparisonValidationSettings::allow_weakening(), } } @@ -106,7 +106,7 @@ pub struct SchemaComparisonStructureSettings { } impl SchemaComparisonStructureSettings { - pub const fn equality() -> Self { + pub const fn require_identical_structure() -> Self { Self { allow_new_enum_variants: false, allow_replacing_with_any: false, @@ -129,7 +129,7 @@ pub struct SchemaComparisonMetadataSettings { } impl SchemaComparisonMetadataSettings { - pub const fn equality() -> Self { + pub const fn require_identical_metadata() -> Self { Self { type_name_changes: NameChangeRule::equality(), field_name_changes: NameChangeRule::equality(), @@ -180,7 +180,7 @@ pub struct SchemaComparisonValidationSettings { } impl SchemaComparisonValidationSettings { - pub const fn equality() -> Self { + pub const fn require_identical_validation() -> Self { Self { allow_validation_weakening: false, }