4747//! an attacker can influence catalog bytes. Treat `HostApiFingerprint` as a
4848//! convenience equality key, not a MAC.
4949
50+ use std:: collections:: BTreeMap ;
5051use std:: fmt;
5152
5253use serde:: de:: { self , DeserializeSeed , MapAccess , SeqAccess , VariantAccess , Visitor } ;
@@ -87,6 +88,8 @@ pub const MAX_HOST_CATALOG_FUNCTIONS: usize = 1_024;
8788
8889/// Maximum byte length of names on host parameter records.
8990pub const MAX_HOST_PARAMETER_NAME_LEN : usize = 128 ;
91+ /// Maximum byte length of fixed-shape host object field names.
92+ pub const MAX_HOST_OBJECT_FIELD_NAME_LEN : usize = 128 ;
9093
9194/// Maximum byte length of host resource/function documentation.
9295pub const MAX_HOST_DESCRIPTION_LEN : usize = 4_096 ;
@@ -355,6 +358,10 @@ pub enum HostTypeSchema {
355358 Bytes ,
356359 Array ( Box < HostTypeSchema > ) ,
357360 Map ( Box < HostTypeSchema > ) ,
361+ /// A fixed-shape record whose field names and value schemas are known.
362+ /// Runtime values use the ordinary map carrier, while compilers retain the
363+ /// structural field types for checked member access.
364+ Object ( BTreeMap < String , HostTypeSchema > ) ,
358365 Optional ( Box < HostTypeSchema > ) ,
359366 Callable {
360367 params : Vec < HostTypeSchema > ,
@@ -425,6 +432,9 @@ impl Serialize for HostTypeSchema {
425432 Self :: Map ( inner) => {
426433 serializer. serialize_newtype_variant ( "HostTypeSchema" , 9 , "Map" , inner)
427434 }
435+ Self :: Object ( fields) => {
436+ serializer. serialize_newtype_variant ( "HostTypeSchema" , 13 , "Object" , fields)
437+ }
428438 Self :: Optional ( inner) => {
429439 serializer. serialize_newtype_variant ( "HostTypeSchema" , 10 , "Optional" , inner)
430440 }
@@ -457,6 +467,7 @@ enum HostTypeSchemaVariant {
457467 Optional ,
458468 Callable ,
459469 Resource ,
470+ Object ,
460471}
461472
462473struct HostTypeSchemaSeed < ' a > {
@@ -489,7 +500,7 @@ impl<'de> DeserializeSeed<'de> for HostTypeSchemaSeed<'_> {
489500 "HostTypeSchema" ,
490501 & [
491502 "Unknown" , "Null" , "Int" , "Float" , "Number" , "Bool" , "String" , "Bytes" , "Array" ,
492- "Map" , "Optional" , "Callable" , "Resource" ,
503+ "Map" , "Optional" , "Callable" , "Resource" , "Object" ,
493504 ] ,
494505 HostTypeSchemaVisitor {
495506 budget : self . budget ,
@@ -547,6 +558,15 @@ impl<'de> Visitor<'de> for HostTypeSchemaVisitor<'_> {
547558 } )
548559 . map ( |inner| HostTypeSchema :: Map ( Box :: new ( inner) ) )
549560 }
561+ HostTypeSchemaVariant :: Object => {
562+ let depth = next_schema_depth :: < A :: Error > ( self . depth ) ?;
563+ access
564+ . newtype_variant_seed ( ObjectSchemaSeed {
565+ budget : self . budget ,
566+ depth,
567+ } )
568+ . map ( HostTypeSchema :: Object )
569+ }
550570 HostTypeSchemaVariant :: Optional => {
551571 let depth = next_schema_depth :: < A :: Error > ( self . depth ) ?;
552572 access
@@ -570,6 +590,67 @@ impl<'de> Visitor<'de> for HostTypeSchemaVisitor<'_> {
570590 }
571591}
572592
593+ struct ObjectSchemaSeed < ' a > {
594+ budget : & ' a mut ComplexityBudget ,
595+ depth : usize ,
596+ }
597+
598+ impl < ' de > DeserializeSeed < ' de > for ObjectSchemaSeed < ' _ > {
599+ type Value = BTreeMap < String , HostTypeSchema > ;
600+
601+ fn deserialize < D > ( self , deserializer : D ) -> Result < Self :: Value , D :: Error >
602+ where
603+ D : serde:: Deserializer < ' de > ,
604+ {
605+ deserializer. deserialize_map ( ObjectSchemaVisitor {
606+ budget : self . budget ,
607+ depth : self . depth ,
608+ } )
609+ }
610+ }
611+
612+ struct ObjectSchemaVisitor < ' a > {
613+ budget : & ' a mut ComplexityBudget ,
614+ depth : usize ,
615+ }
616+
617+ impl < ' de > Visitor < ' de > for ObjectSchemaVisitor < ' _ > {
618+ type Value = BTreeMap < String , HostTypeSchema > ;
619+
620+ fn expecting ( & self , formatter : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
621+ formatter. write_str ( "a bounded fixed-field object schema" )
622+ }
623+
624+ fn visit_map < A > ( self , mut map : A ) -> Result < Self :: Value , A :: Error >
625+ where
626+ A : MapAccess < ' de > ,
627+ {
628+ bounded_map_size_hint (
629+ map. size_hint ( ) ,
630+ "object schema" ,
631+ MAX_HOST_SCHEMA_PROPERTIES - self . budget . properties ,
632+ ) ?;
633+ let mut fields = BTreeMap :: new ( ) ;
634+ while let Some ( name) = map. next_key_seed ( BoundedStringSeed {
635+ field : "host object field name" ,
636+ limit : MAX_HOST_OBJECT_FIELD_NAME_LEN ,
637+ } ) ? {
638+ if fields. contains_key ( & name) {
639+ return Err ( de:: Error :: custom ( format ! (
640+ "duplicate object field {name:?}"
641+ ) ) ) ;
642+ }
643+ let schema = map. next_value_seed ( HostTypeSchemaSeed {
644+ budget : self . budget ,
645+ depth : self . depth ,
646+ property : true ,
647+ } ) ?;
648+ fields. insert ( name, schema) ;
649+ }
650+ Ok ( fields)
651+ }
652+ }
653+
573654fn next_schema_depth < E > ( depth : usize ) -> Result < usize , E >
574655where
575656 E : de:: Error ,
@@ -761,6 +842,16 @@ impl fmt::Display for HostTypeSchema {
761842 Self :: Bytes => write ! ( f, "bytes" ) ,
762843 Self :: Array ( inner) => write ! ( f, "array<{inner}>" ) ,
763844 Self :: Map ( inner) => write ! ( f, "map<{inner}>" ) ,
845+ Self :: Object ( fields) => {
846+ write ! ( f, "{{" ) ?;
847+ for ( index, ( name, schema) ) in fields. iter ( ) . enumerate ( ) {
848+ if index > 0 {
849+ write ! ( f, ", " ) ?;
850+ }
851+ write ! ( f, "{name}: {schema}" ) ?;
852+ }
853+ write ! ( f, "}}" )
854+ }
764855 Self :: Optional ( inner) => write ! ( f, "optional<{inner}>" ) ,
765856 Self :: Callable { params, result } => {
766857 write ! ( f, "fn(" ) ?;
@@ -2059,6 +2150,28 @@ where
20592150 } ) ?;
20602151 pending. push ( ( inner, child_depth) ) ;
20612152 }
2153+ HostTypeSchema :: Object ( fields) => {
2154+ budget. charge_properties ( fields. len ( ) ) ?;
2155+ let child_depth =
2156+ depth
2157+ . checked_add ( 1 )
2158+ . ok_or ( HostSchemaValidationError :: IntegerOverflow {
2159+ field : "schema depth" ,
2160+ } ) ?;
2161+ pending. try_reserve ( fields. len ( ) ) . map_err ( |_| {
2162+ HostSchemaValidationError :: AllocationFailed {
2163+ field : "schema traversal" ,
2164+ }
2165+ } ) ?;
2166+ for ( name, schema) in fields. iter ( ) . rev ( ) {
2167+ bounded_string_error (
2168+ "host object field name" ,
2169+ name. len ( ) ,
2170+ MAX_HOST_OBJECT_FIELD_NAME_LEN ,
2171+ ) ?;
2172+ pending. push ( ( schema, child_depth) ) ;
2173+ }
2174+ }
20622175 HostTypeSchema :: Callable { params, result } => {
20632176 budget. charge_properties ( params. len ( ) ) ?;
20642177 let child_depth =
@@ -3003,6 +3116,14 @@ fn try_push_type(
30033116 } ) ?;
30043117 pending. push ( inner) ;
30053118 }
3119+ HostTypeSchema :: Object ( fields) => {
3120+ push_tag ( bytes, b'o' ) ;
3121+ push_len ( bytes, fields. len ( ) ) ?;
3122+ for ( name, schema) in fields {
3123+ push_len_str ( bytes, name) ?;
3124+ try_push_type ( bytes, schema) ?;
3125+ }
3126+ }
30063127 HostTypeSchema :: Optional ( inner) => {
30073128 push_tag ( bytes, b'?' ) ;
30083129 pending. try_reserve ( 1 ) . map_err ( |_| {
@@ -3074,9 +3195,53 @@ fn fnv1a(bytes: &[u8]) -> u64 {
30743195}
30753196#[ cfg( test) ]
30763197mod tests {
3198+ use std:: collections:: BTreeMap ;
3199+
30773200 use super :: * ;
30783201 use serde_json:: json;
30793202
3203+ #[ test]
3204+ fn object_schema_rejects_oversized_field_names ( ) {
3205+ let field = "x" . repeat ( MAX_HOST_OBJECT_FIELD_NAME_LEN + 1 ) ;
3206+ let schema = HostTypeSchema :: Object ( BTreeMap :: from ( [ ( field, HostTypeSchema :: Bool ) ] ) ) ;
3207+
3208+ assert_eq ! (
3209+ schema. validate( ) ,
3210+ Err ( HostSchemaValidationError :: StringTooLong {
3211+ field: "host object field name" ,
3212+ len: MAX_HOST_OBJECT_FIELD_NAME_LEN + 1 ,
3213+ limit: MAX_HOST_OBJECT_FIELD_NAME_LEN ,
3214+ } )
3215+ ) ;
3216+ }
3217+
3218+ #[ test]
3219+ fn object_schema_serde_display_and_resource_walk_are_structural ( ) {
3220+ let schema = HostTypeSchema :: Object ( BTreeMap :: from ( [
3221+ (
3222+ "handle" . to_string ( ) ,
3223+ HostTypeSchema :: Optional ( Box :: new ( HostTypeSchema :: Resource ( io_file_key ( ) ) ) ) ,
3224+ ) ,
3225+ ( "ok" . to_string ( ) , HostTypeSchema :: Bool ) ,
3226+ ] ) ) ;
3227+
3228+ let encoded = serde_json:: to_value ( & schema) . expect ( "serialize object schema" ) ;
3229+ let decoded: HostTypeSchema =
3230+ serde_json:: from_value ( encoded) . expect ( "deserialize object schema" ) ;
3231+
3232+ assert_eq ! ( decoded, schema) ;
3233+ assert_eq ! (
3234+ schema. to_string( ) ,
3235+ "{handle: optional<resource<io.file>>, ok: bool}"
3236+ ) ;
3237+ assert ! ( schema. contains_resource( ) ) ;
3238+ assert_eq ! ( schema. resource_key( ) , None ) ;
3239+ let mut keys = Vec :: new ( ) ;
3240+ schema. collect_resource_keys ( & mut keys) ;
3241+ let expected_key = io_file_key ( ) ;
3242+ assert_eq ! ( keys, vec![ & expected_key] ) ;
3243+ }
3244+
30803245 fn io_file_key ( ) -> ResourceTypeKey {
30813246 ResourceTypeKey :: new ( "io.file" ) . expect ( "valid key" )
30823247 }
0 commit comments