@@ -415,23 +415,105 @@ fn try_json_eq(expected: &str, actual: &str) -> Result<(), ProtocolTestFailure>
415415 }
416416}
417417
418+ /// Compares two `ciborium::value::Value` instances for semantic equality.
419+ ///
420+ /// This function recursively compares two CBOR values, correctly handling arrays and maps
421+ /// according to the CBOR specification. Arrays are compared element-wise in order,
422+ /// while maps are compared without considering the order of key-value pairs.
423+ fn cbor_values_equal (
424+ a : & ciborium:: value:: Value ,
425+ b : & ciborium:: value:: Value ,
426+ ) -> Result < bool , ProtocolTestFailure > {
427+ match ( a, b) {
428+ ( ciborium:: value:: Value :: Array ( a_array) , ciborium:: value:: Value :: Array ( b_array) ) => {
429+ // Both arrays should be equal in size.
430+ if a_array. len ( ) != b_array. len ( ) {
431+ return Ok ( false ) ;
432+ }
433+ // Compare arrays element-wise.
434+ for ( a_elem, b_elem) in a_array. iter ( ) . zip ( b_array. iter ( ) ) {
435+ if !cbor_values_equal ( a_elem, b_elem) ? {
436+ return Ok ( false ) ;
437+ }
438+ }
439+ Ok ( true )
440+ }
441+
442+ // Convert `ciborium::value::Value::Map` to a `HashMap`, and then compare the values of
443+ // each key in `a` with those in `b`.
444+ ( ciborium:: value:: Value :: Map ( a_map) , ciborium:: value:: Value :: Map ( b_map) ) => {
445+ if a_map. len ( ) != b_map. len ( ) {
446+ return Ok ( false ) ;
447+ }
448+
449+ let b_hashmap = ciborium_map_to_hashmap ( b_map) ?;
450+ // Each key in `a` should exist in `b`, and the values should match.
451+ for a_key_value in a_map. iter ( ) {
452+ let ( a_key, a_value) = get_text_key_value ( a_key_value) ?;
453+ match b_hashmap. get ( a_key) {
454+ Some ( b_value) => {
455+ if !cbor_values_equal ( a_value, b_value) ? {
456+ return Ok ( false ) ;
457+ }
458+ }
459+ None => return Ok ( false ) ,
460+ }
461+ }
462+ Ok ( true )
463+ }
464+
465+ ( ciborium:: value:: Value :: Float ( a_float) , ciborium:: value:: Value :: Float ( b_float) ) => {
466+ Ok ( a_float == b_float || ( a_float. is_nan ( ) && b_float. is_nan ( ) ) )
467+ }
468+
469+ _ => Ok ( a == b) ,
470+ }
471+ }
472+
473+ /// Converts a `ciborium::value::Value::Map` into a `HashMap<&String, &ciborium::value::Value>`.
474+ ///
475+ /// CBOR maps (`Value::Map`) are internally represented as vectors of key-value pairs,
476+ /// and direct comparison is affected by the order of these pairs.
477+ /// Since the CBOR specification treats maps as unordered collections,
478+ /// this function transforms the vector into a `HashMap`, for order-independent comparisons
479+ /// between maps.
480+ fn ciborium_map_to_hashmap (
481+ cbor_map : & [ ( ciborium:: value:: Value , ciborium:: value:: Value ) ] ,
482+ ) -> Result < std:: collections:: HashMap < & String , & ciborium:: value:: Value > , ProtocolTestFailure > {
483+ cbor_map. iter ( ) . map ( get_text_key_value) . collect ( )
484+ }
485+
486+ /// Extracts a string key and its associated value from a CBOR key-value pair.
487+ /// Returns a `ProtocolTestFailure::InvalidBodyFormat` error if the key is not a text value.
488+ fn get_text_key_value (
489+ ( key, value) : & ( ciborium:: value:: Value , ciborium:: value:: Value ) ,
490+ ) -> Result < ( & String , & ciborium:: value:: Value ) , ProtocolTestFailure > {
491+ match key {
492+ ciborium:: value:: Value :: Text ( key_str) => Ok ( ( key_str, value) ) ,
493+ _ => Err ( ProtocolTestFailure :: InvalidBodyFormat {
494+ expected : "a text key as map entry" . to_string ( ) ,
495+ found : format ! ( "{:?}" , key) ,
496+ } ) ,
497+ }
498+ }
499+
418500fn try_cbor_eq < T : AsRef < [ u8 ] > + Debug > (
419501 actual_body : T ,
420502 expected_body : & str ,
421503) -> Result < ( ) , ProtocolTestFailure > {
422504 let decoded = base64_simd:: STANDARD
423505 . decode_to_vec ( expected_body)
424506 . expect ( "smithy protocol test `body` property is not properly base64 encoded" ) ;
425- let expected_cbor_value: serde_cbor :: Value =
426- serde_cbor :: from_slice ( decoded. as_slice ( ) ) . expect ( "expected value must be valid CBOR" ) ;
427- let actual_cbor_value: serde_cbor :: Value = serde_cbor :: from_slice ( actual_body. as_ref ( ) )
507+ let expected_cbor_value: ciborium :: value :: Value =
508+ ciborium :: de :: from_reader ( decoded. as_slice ( ) ) . expect ( "expected value must be valid CBOR" ) ;
509+ let actual_cbor_value: ciborium :: value :: Value = ciborium :: de :: from_reader ( actual_body. as_ref ( ) )
428510 . map_err ( |e| ProtocolTestFailure :: InvalidBodyFormat {
429511 expected : "cbor" . to_owned ( ) ,
430512 found : format ! ( "{} {:?}" , e, actual_body) ,
431513 } ) ?;
432514 let actual_body_base64 = base64_simd:: STANDARD . encode_to_string ( & actual_body) ;
433515
434- if expected_cbor_value != actual_cbor_value {
516+ if ! cbor_values_equal ( & expected_cbor_value, & actual_cbor_value) ? {
435517 let expected_body_annotated_hex: String = cbor_diag:: parse_bytes ( & decoded)
436518 . expect ( "smithy protocol test `body` property is not valid CBOR" )
437519 . to_hex ( ) ;
@@ -599,6 +681,46 @@ mod tests {
599681 validate_body ( actual, expected, MediaType :: Json ) . expect_err ( "bodies do not match" ) ;
600682 }
601683
684+ #[ test]
685+ fn test_validate_cbor_body ( ) {
686+ let base64_encode = |v : & [ u8 ] | base64_simd:: STANDARD . encode_to_string ( v) ;
687+
688+ // The following is the CBOR representation of `{"abc": 5 }`.
689+ let actual = [ 0xbf , 0x63 , 0x61 , 0x62 , 0x63 , 0x05 , 0xff ] ;
690+ // The following is the base64-encoded CBOR representation of `{"abc": 5 }` using a definite length map.
691+ let expected_base64 = base64_encode ( & [ 0xA1 , 0x63 , 0x61 , 0x62 , 0x63 , 0x05 ] ) ;
692+
693+ validate_body ( actual, expected_base64. as_str ( ) , MediaType :: Cbor )
694+ . expect ( "unexpected mismatch between CBOR definite and indefinite map encodings" ) ;
695+
696+ // The following is the CBOR representation of `{"a":1, "b":2}`.
697+ let actual = [ 0xBF , 0x61 , 0x61 , 0x01 , 0x61 , 0x62 , 0x02 , 0xFF ] ;
698+ // The following is the base64-encoded CBOR representation of `{"b":2, "a":1}`.
699+ let expected_base64 = base64_encode ( & [ 0xBF , 0x61 , 0x62 , 0x02 , 0x61 , 0x61 , 0x01 , 0xFF ] ) ;
700+ validate_body ( actual, expected_base64. as_str ( ) , MediaType :: Cbor )
701+ . expect ( "different ordering in CBOR decoded maps do not match" ) ;
702+
703+ // The following is the CBOR representation of `{"a":[1,2,{"b":3, "c":4}]}`.
704+ let actual = [
705+ 0xBF , 0x61 , 0x61 , 0x9F , 0x01 , 0x02 , 0xBF , 0x61 , 0x62 , 0x03 , 0x61 , 0x63 , 0x04 , 0xFF ,
706+ 0xFF , 0xFF ,
707+ ] ;
708+ // The following is the base64-encoded CBOR representation of `{"a":[1,2,{"c":4, "b":3}]}`.
709+ let expected_base64 = base64_encode ( & [
710+ 0xBF , 0x61 , 0x61 , 0x9F , 0x01 , 0x02 , 0xBF , 0x61 , 0x63 , 0x04 , 0x61 , 0x62 , 0x03 , 0xFF ,
711+ 0xFF , 0xFF ,
712+ ] ) ;
713+ validate_body ( actual, expected_base64. as_str ( ) , MediaType :: Cbor )
714+ . expect ( "different ordering in CBOR decoded maps do not match" ) ;
715+
716+ // The following is the CBOR representation of `{"a":[1,2]}`.
717+ let actual = [ 0xBF , 0x61 , 0x61 , 0x9F , 0x01 , 0x02 , 0xFF , 0xFF ] ;
718+ // The following is the CBOR representation of `{"a":[2,1]}`.
719+ let expected_base64 = base64_encode ( & [ 0xBF , 0x61 , 0x61 , 0x9F , 0x02 , 0x01 , 0xFF , 0xFF ] ) ;
720+ validate_body ( actual, expected_base64. as_str ( ) , MediaType :: Cbor )
721+ . expect_err ( "arrays in CBOR should follow strict ordering" ) ;
722+ }
723+
602724 #[ test]
603725 fn test_validate_xml_body ( ) {
604726 let expected = r#"<a>
0 commit comments