@@ -8,7 +8,7 @@ use thiserror::Error;
88///
99/// # Type Parameters
1010///
11- /// * `W` - witness type to prove vector ranges and shape if interface accordingly
11+ /// * `W` - witness type to prove vector ranges and shape it interface accordingly
1212#[ derive( PartialEq , Eq , Debug , Clone , Hash , PartialOrd , Ord ) ]
1313pub struct BoundedVec < T , const L : usize , const U : usize , W = witnesses:: NonEmpty < L , U > > {
1414 inner : Vec < T > ,
@@ -59,13 +59,31 @@ pub mod witnesses {
5959 panic ! ( "L must be less than or equal to U" )
6060 }
6161
62+ serde :: < U > ( ) ;
63+
6264 NonEmpty :: < L , U > ( ( ) )
6365 }
6466 }
6567
68+ const fn serde < const U : usize > ( ) {
69+ #[ cfg( feature = "schema" ) ]
70+ if U as u128 > u32:: MAX as u128 {
71+ // there is not const safe way to cast usize to u32, nor to other bigger number
72+ panic ! ( "`schemars` encodes `maxLength` as u32, so `U` must be less than or equal to `u32::MAX`" )
73+ }
74+
75+ #[ cfg( feature = "borsh" ) ]
76+ if U as u128 > u32:: MAX as u128 {
77+ panic ! ( "`borsh` specifies size of dynamic containers as u32, so `U` must be less than or equal to `u32::MAX`" )
78+ }
79+ }
80+
6681 /// Type a compile-time proof for possibly empty vector with upper bound
6782 pub const fn empty < const U : usize > ( ) -> Empty < U > {
68- const { Empty :: < U > ( ( ) ) }
83+ const {
84+ serde :: < U > ( ) ;
85+ Empty :: < U > ( ( ) )
86+ }
6987 }
7088}
7189
@@ -578,6 +596,184 @@ impl<T, const L: usize, const U: usize> OptBoundedVecToVec<T>
578596 }
579597}
580598
599+ /// Suports encoding and decoding with [borsh](https://crates.io/crates/borsh), and BorshSchema.
600+ ///
601+ /// By default Borsh uses u32 as length prefix for sequences.
602+ /// For bounded we used u8, u16 or u32 depending on the U.
603+ /// Increase or decreaasing U may not always be backward compatible.
604+ #[ cfg( feature = "borsh" ) ]
605+ mod borsh_impl {
606+ use super :: * ;
607+ use alloc:: collections:: btree_map:: { BTreeMap , Entry } ;
608+ use borsh:: { BorshDeserialize , BorshSchema , BorshSerialize } ;
609+
610+ impl < T : BorshSerialize , const L : usize , const U : usize , W > BorshSerialize
611+ for BoundedVec < T , L , U , W >
612+ {
613+ fn serialize < Writer : borsh:: io:: Write > (
614+ & self ,
615+ writer : & mut Writer ,
616+ ) -> borsh:: io:: Result < ( ) > {
617+ let len = self . inner . len ( ) ;
618+ if U <= usize:: from ( u8:: MAX ) {
619+ #[ expect( clippy:: expect_used) ]
620+ let len: u8 = len. try_into ( ) . expect ( "proved by design" ) ;
621+ len. serialize ( writer) ?;
622+ } else if U <= usize:: from ( u16:: MAX ) {
623+ #[ expect( clippy:: expect_used) ]
624+ let len: u16 = len. try_into ( ) . expect ( "proved by design" ) ;
625+ len. serialize ( writer) ?;
626+ } else {
627+ #[ expect( clippy:: expect_used) ]
628+ let len: u32 = len. try_into ( ) . expect ( "proved by design" ) ;
629+ len. serialize ( writer) ?;
630+ } ;
631+
632+ // adapted from internals of borsh-rs
633+ let data = self . as_slice ( ) ;
634+ if let Some ( u8_slice) = T :: u8_slice ( data) {
635+ writer. write_all ( u8_slice) ?;
636+ } else {
637+ for item in data {
638+ item. serialize ( writer) ?;
639+ }
640+ }
641+ Ok ( ( ) )
642+ }
643+ }
644+
645+ impl < T : BorshDeserialize , const L : usize , const U : usize , W > BorshDeserialize
646+ for BoundedVec < T , L , U , W >
647+ {
648+ fn deserialize_reader < R : borsh:: io:: Read > ( reader : & mut R ) -> borsh:: io:: Result < Self > {
649+ let len = if U <= usize:: from ( u8:: MAX ) {
650+ usize:: from ( u8:: deserialize_reader ( reader) ?)
651+ } else if U <= usize:: from ( u16:: MAX ) {
652+ usize:: from ( u16:: deserialize_reader ( reader) ?)
653+ } else {
654+ let len = u32:: deserialize_reader ( reader) ?;
655+ usize:: try_from ( len) . map_err ( |_| {
656+ borsh:: io:: Error :: new (
657+ borsh:: io:: ErrorKind :: Other ,
658+ alloc:: format!( "Length overflow: got {}" , len) ,
659+ )
660+ } ) ?
661+ } ;
662+ if len < L {
663+ return Err ( borsh:: io:: Error :: new (
664+ borsh:: io:: ErrorKind :: Other ,
665+ alloc:: format!( "Lower bound violation: got {} (expected >= {})" , len, L ) ,
666+ ) ) ;
667+ } else if len > U {
668+ return Err ( borsh:: io:: Error :: new (
669+ borsh:: io:: ErrorKind :: Other ,
670+ alloc:: format!( "Upper bound violation: got {} (expected <= {})" , len, U ) ,
671+ ) ) ;
672+ }
673+ // adapted from internals for borsh-rs
674+ let data = if len == 0 {
675+ Vec :: new ( )
676+ } else if let Some ( vec_bytes) = T :: vec_from_reader ( len as u32 , reader) ? {
677+ vec_bytes
678+ } else {
679+ let el_size = core:: mem:: size_of :: < T > ( ) as u32 ;
680+ let cautious =
681+ core:: cmp:: max ( core:: cmp:: min ( len as u32 , 4096 / el_size) , 1 ) as usize ;
682+
683+ // TODO(16): return capacity allocation when we can safely do that.
684+ let mut result = Vec :: with_capacity ( cautious) ;
685+ for _ in 0 ..len {
686+ result. push ( T :: deserialize_reader ( reader) ?) ;
687+ }
688+ result
689+ } ;
690+
691+ Ok ( Self {
692+ inner : data,
693+ _marker : core:: marker:: PhantomData ,
694+ } )
695+ }
696+ }
697+
698+ impl < T : BorshSchema , const L : usize , const U : usize > BorshSchema for BoundedVec < T , L , U > {
699+ fn add_definitions_recursively (
700+ definitions : & mut BTreeMap < borsh:: schema:: Declaration , borsh:: schema:: Definition > ,
701+ ) {
702+ let len_width = if U <= usize:: from ( u8:: MAX ) {
703+ 1
704+ } else if U <= usize:: from ( u16:: MAX ) {
705+ 2
706+ } else {
707+ 4 // proven by design
708+ } ;
709+
710+ let definition = borsh:: schema:: Definition :: Sequence {
711+ length_width : len_width,
712+ #[ expect( clippy:: expect_used) ]
713+ length_range : core:: ops:: RangeInclusive :: < u64 > :: new (
714+ u64:: try_from ( L ) . expect ( "proved by design" ) ,
715+ u64:: try_from ( U ) . expect ( "proved by design" ) ,
716+ ) ,
717+ elements : T :: declaration ( ) ,
718+ } ;
719+ match definitions. entry ( Self :: declaration ( ) ) {
720+ Entry :: Occupied ( occ) => {
721+ let existing_def = occ. get ( ) ;
722+ assert_eq ! (
723+ existing_def,
724+ & definition,
725+ "Redefining type schema for {}. Types with the same names are not supported." ,
726+ occ. key( )
727+ ) ;
728+ }
729+ Entry :: Vacant ( vac) => {
730+ vac. insert ( definition) ;
731+ }
732+ }
733+ T :: add_definitions_recursively ( definitions) ;
734+ }
735+
736+ fn declaration ( ) -> borsh:: schema:: Declaration {
737+ alloc:: format!( "BoundedVec<{}, {}, {}>" , T :: declaration( ) , L , U )
738+ }
739+ }
740+
741+ #[ cfg( test) ]
742+ mod tests {
743+ use borsh:: schema:: BorshSchemaContainer ;
744+
745+ use super :: * ;
746+ #[ test]
747+ #[ allow( clippy:: expect_used) ]
748+ fn borsh_encdec ( ) {
749+ let data: BoundedVec < u8 , 2 , 8 > = vec ! [ 1u8 , 2 ] . try_into ( ) . expect ( "borsh works" ) ;
750+ let buf = & mut Vec :: new ( ) ;
751+ data. serialize ( buf) . expect ( "borsh works" ) ;
752+ let decoded =
753+ BoundedVec :: < u8 , 2 , 8 > :: deserialize ( & mut buf. as_slice ( ) ) . expect ( "borsh works" ) ;
754+ let compatible_decoded =
755+ BoundedVec :: < u8 , 1 , 255 > :: deserialize ( & mut buf. as_slice ( ) ) . expect ( "borsh works" ) ;
756+ assert_eq ! ( data. get( 0 ) , decoded. get( 0 ) ) ;
757+ assert_eq ! ( data. get( 1 ) , decoded. get( 1 ) ) ;
758+ assert_eq ! ( data. get( 0 ) , compatible_decoded. get( 0 ) ) ;
759+ assert_eq ! ( data. get( 1 ) , compatible_decoded. get( 1 ) ) ;
760+ assert ! ( BoundedVec :: <u8 , 1 , 257 >:: deserialize( & mut buf. as_slice( ) ) . is_err( ) ) ;
761+
762+ let schema = BorshSchemaContainer :: for_type :: < BoundedVec < u8 , 2 , 8 > > ( ) ;
763+ let schema = schema
764+ . get_definition ( "BoundedVec<u8, 2, 8>" )
765+ . expect ( "borsh works" ) ;
766+ assert ! ( matches!(
767+ schema,
768+ borsh:: schema:: Definition :: Sequence {
769+ length_width: 1 ,
770+ ..
771+ }
772+ ) ) ;
773+ }
774+ }
775+ }
776+
581777#[ allow( clippy:: unwrap_used) ]
582778#[ cfg( feature = "arbitrary" ) ]
583779mod arbitrary {
@@ -653,8 +849,8 @@ mod serde_impl {
653849 use schemars:: schema:: { InstanceType , SchemaObject } ;
654850 use schemars:: JsonSchema ;
655851
656- // we cannot use attributes, because the do not work with `const`, only numeric literals supported
657- impl < T : JsonSchema , const L : usize , const U : usize > JsonSchema for BoundedVec < T , L , U > {
852+ // we cannot use `serde` attributes, because these do not work with `const`, only numeric literals supported
853+ impl < T : JsonSchema , const L : usize , const U : usize , W > JsonSchema for BoundedVec < T , L , U , W > {
658854 fn schema_name ( ) -> alloc:: string:: String {
659855 alloc:: format!( "BoundedVec{}Min{}Max{}" , T :: schema_name( ) , L , U )
660856 }
@@ -666,15 +862,35 @@ mod serde_impl {
666862 items : Some ( schemars:: schema:: SingleOrVec :: Single (
667863 T :: json_schema ( gen) . into ( ) ,
668864 ) ) ,
669- min_items : Some ( L as u32 ) ,
670- max_items : Some ( U as u32 ) ,
865+ #[ expect( clippy:: expect_used) ] // design time failure
866+ min_items : Some (
867+ u32:: try_from ( L ) . expect ( "JSON schema does not support so large ranges" ) ,
868+ ) ,
869+ #[ expect( clippy:: expect_used) ] // design time failure
870+ max_items : Some (
871+ u32:: try_from ( U ) . expect ( "JSON schema does not support so large ranges" ) ,
872+ ) ,
671873 ..Default :: default ( )
672874 } ) ) ,
673875 ..Default :: default ( )
674876 }
675877 . into ( )
676878 }
677879 }
880+
881+ #[ cfg( test) ]
882+ mod tests {
883+ use super :: * ;
884+ use schemars:: schema_for;
885+ #[ test]
886+ fn json_schema ( ) {
887+ let schema = schema_for ! ( BoundedVec <u8 , 2 , 8 >) ;
888+ let min_items = schema. schema . array . as_ref ( ) . unwrap ( ) . min_items . unwrap ( ) ;
889+ let max_items = schema. schema . array . as_ref ( ) . unwrap ( ) . max_items . unwrap ( ) ;
890+ assert_eq ! ( min_items, 2 ) ;
891+ assert_eq ! ( max_items, 8 ) ;
892+ }
893+ }
678894 }
679895}
680896
0 commit comments