@@ -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 > ,
@@ -38,8 +38,11 @@ pub enum BoundedVecOutOfBounds {
3838
3939/// Module for type witnesses used to prove vector bounds at compile time
4040pub mod witnesses {
41-
42- // NOTE: we can have proves if needed for some cases like 8/16/32/64 upper bound, so can make memory and serde more compile safe and efficient
41+ // NOTE:
42+ // we can have proves if needed for some cases like 8/16/32/64 upper bound and operating range,
43+ // and make memory layout more efficient:
44+ // - decide stackalloc or smallvec or std::vec, depending on range * size_of at compile time
45+ // - make some values of vec to be not usize, but other numbers
4346
4447 /// Compile-time proof of valid bounds. Must be consturcted with same bounds to instantiate `BoundedVec`.
4548 #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
@@ -59,13 +62,30 @@ pub mod witnesses {
5962 panic ! ( "L must be less than or equal to U" )
6063 }
6164
65+ serde :: < U > ( ) ;
6266 NonEmpty :: < L , U > ( ( ) )
6367 }
6468 }
6569
70+ const fn serde < const U : usize > ( ) {
71+ #[ cfg( feature = "schema" ) ]
72+ if U as u128 > u32:: MAX as u128 {
73+ // there is not const safe way to cast usize to u32, nor to other bigger number
74+ panic ! ( "`schemars` encodes `maxLength` as u32, so `U` must be less than or equal to `u32::MAX`" )
75+ }
76+
77+ #[ cfg( feature = "borsh" ) ]
78+ if U as u128 > u32:: MAX as u128 {
79+ panic ! ( "`borsh` specifies size of dynamic containers as u32, so `U` must be less than or equal to `u32::MAX`" )
80+ }
81+ }
82+
6683 /// Type a compile-time proof for possibly empty vector with upper bound
6784 pub const fn empty < const U : usize > ( ) -> Empty < U > {
68- const { Empty :: < U > ( ( ) ) }
85+ const {
86+ serde :: < U > ( ) ;
87+ Empty :: < U > ( ( ) )
88+ }
6989 }
7090}
7191
@@ -88,7 +108,7 @@ impl<T, const U: usize> BoundedVec<T, 0, U, witnesses::Empty<U>> {
88108 /// BoundedVec::<_, 0, 8, witnesses::Empty<8>>::from_vec(vec![1u8, 2]).unwrap();
89109 /// ```
90110 pub fn from_vec ( items : Vec < T > ) -> Result < Self , BoundedVecOutOfBounds > {
91- let _witness = witnesses:: empty :: < U > ( ) ;
111+ let _ = witnesses:: empty :: < U > ( ) ;
92112 let len = items. len ( ) ;
93113 if len > U {
94114 Err ( BoundedVecOutOfBounds :: UpperBoundError {
@@ -238,7 +258,7 @@ impl<T, const L: usize, const U: usize> BoundedVec<T, L, U, witnesses::NonEmpty<
238258 /// BoundedVec::<_, 2, 8, witnesses::NonEmpty<2, 8>>::from_vec(vec![1u8, 2]).unwrap();
239259 /// ```
240260 pub fn from_vec ( items : Vec < T > ) -> Result < Self , BoundedVecOutOfBounds > {
241- let _witness = witnesses:: non_empty :: < L , U > ( ) ;
261+ let _ = witnesses:: non_empty :: < L , U > ( ) ;
242262 let len = items. len ( ) ;
243263 if len < L {
244264 Err ( BoundedVecOutOfBounds :: LowerBoundError {
@@ -578,6 +598,184 @@ impl<T, const L: usize, const U: usize> OptBoundedVecToVec<T>
578598 }
579599}
580600
601+ /// Suports encoding and decoding with [borsh](https://crates.io/crates/borsh), and BorshSchema.
602+ ///
603+ /// By default Borsh uses u32 as length prefix for sequences.
604+ /// For bounded we used u8, u16 or u32 depending on the U.
605+ /// Increase or decreaasing U may not always be backward compatible.
606+ #[ cfg( feature = "borsh" ) ]
607+ mod borsh_impl {
608+ use super :: * ;
609+ use alloc:: collections:: btree_map:: { BTreeMap , Entry } ;
610+ use borsh:: { BorshDeserialize , BorshSchema , BorshSerialize } ;
611+
612+ impl < T : BorshSerialize , const L : usize , const U : usize , W > BorshSerialize
613+ for BoundedVec < T , L , U , W >
614+ {
615+ fn serialize < Writer : borsh:: io:: Write > (
616+ & self ,
617+ writer : & mut Writer ,
618+ ) -> borsh:: io:: Result < ( ) > {
619+ let len = self . inner . len ( ) ;
620+ if U <= usize:: from ( u8:: MAX ) {
621+ #[ expect( clippy:: expect_used) ]
622+ let len: u8 = len. try_into ( ) . expect ( "proved by design" ) ;
623+ len. serialize ( writer) ?;
624+ } else if U <= usize:: from ( u16:: MAX ) {
625+ #[ expect( clippy:: expect_used) ]
626+ let len: u16 = len. try_into ( ) . expect ( "proved by design" ) ;
627+ len. serialize ( writer) ?;
628+ } else {
629+ #[ expect( clippy:: expect_used) ]
630+ let len: u32 = len. try_into ( ) . expect ( "proved by design" ) ;
631+ len. serialize ( writer) ?;
632+ } ;
633+
634+ // adapted from internals of borsh-rs
635+ let data = self . as_slice ( ) ;
636+ if let Some ( u8_slice) = T :: u8_slice ( data) {
637+ writer. write_all ( u8_slice) ?;
638+ } else {
639+ for item in data {
640+ item. serialize ( writer) ?;
641+ }
642+ }
643+ Ok ( ( ) )
644+ }
645+ }
646+
647+ impl < T : BorshDeserialize , const L : usize , const U : usize , W > BorshDeserialize
648+ for BoundedVec < T , L , U , W >
649+ {
650+ fn deserialize_reader < R : borsh:: io:: Read > ( reader : & mut R ) -> borsh:: io:: Result < Self > {
651+ let len = if U <= usize:: from ( u8:: MAX ) {
652+ usize:: from ( u8:: deserialize_reader ( reader) ?)
653+ } else if U <= usize:: from ( u16:: MAX ) {
654+ usize:: from ( u16:: deserialize_reader ( reader) ?)
655+ } else {
656+ let len = u32:: deserialize_reader ( reader) ?;
657+ usize:: try_from ( len) . map_err ( |_| {
658+ borsh:: io:: Error :: new (
659+ borsh:: io:: ErrorKind :: Other ,
660+ alloc:: format!( "Length overflow: got {}" , len) ,
661+ )
662+ } ) ?
663+ } ;
664+ if len < L {
665+ return Err ( borsh:: io:: Error :: new (
666+ borsh:: io:: ErrorKind :: Other ,
667+ alloc:: format!( "Lower bound violation: got {} (expected >= {})" , len, L ) ,
668+ ) ) ;
669+ } else if len > U {
670+ return Err ( borsh:: io:: Error :: new (
671+ borsh:: io:: ErrorKind :: Other ,
672+ alloc:: format!( "Upper bound violation: got {} (expected <= {})" , len, U ) ,
673+ ) ) ;
674+ }
675+ // adapted from internals for borsh-rs
676+ let data = if len == 0 {
677+ Vec :: new ( )
678+ } else if let Some ( vec_bytes) = T :: vec_from_reader ( len as u32 , reader) ? {
679+ vec_bytes
680+ } else {
681+ let el_size = core:: mem:: size_of :: < T > ( ) as u32 ;
682+ let cautious =
683+ core:: cmp:: max ( core:: cmp:: min ( len as u32 , 4096 / el_size) , 1 ) as usize ;
684+
685+ // TODO(16): return capacity allocation when we can safely do that.
686+ let mut result = Vec :: with_capacity ( cautious) ;
687+ for _ in 0 ..len {
688+ result. push ( T :: deserialize_reader ( reader) ?) ;
689+ }
690+ result
691+ } ;
692+
693+ Ok ( Self {
694+ inner : data,
695+ _marker : core:: marker:: PhantomData ,
696+ } )
697+ }
698+ }
699+
700+ impl < T : BorshSchema , const L : usize , const U : usize > BorshSchema for BoundedVec < T , L , U > {
701+ fn add_definitions_recursively (
702+ definitions : & mut BTreeMap < borsh:: schema:: Declaration , borsh:: schema:: Definition > ,
703+ ) {
704+ let len_width = if U <= usize:: from ( u8:: MAX ) {
705+ 1
706+ } else if U <= usize:: from ( u16:: MAX ) {
707+ 2
708+ } else {
709+ 4 // proven by design
710+ } ;
711+
712+ let definition = borsh:: schema:: Definition :: Sequence {
713+ length_width : len_width,
714+ #[ expect( clippy:: expect_used) ]
715+ length_range : core:: ops:: RangeInclusive :: < u64 > :: new (
716+ u64:: try_from ( L ) . expect ( "proved by design" ) ,
717+ u64:: try_from ( U ) . expect ( "proved by design" ) ,
718+ ) ,
719+ elements : T :: declaration ( ) ,
720+ } ;
721+ match definitions. entry ( Self :: declaration ( ) ) {
722+ Entry :: Occupied ( occ) => {
723+ let existing_def = occ. get ( ) ;
724+ assert_eq ! (
725+ existing_def,
726+ & definition,
727+ "Redefining type schema for {}. Types with the same names are not supported." ,
728+ occ. key( )
729+ ) ;
730+ }
731+ Entry :: Vacant ( vac) => {
732+ vac. insert ( definition) ;
733+ }
734+ }
735+ T :: add_definitions_recursively ( definitions) ;
736+ }
737+
738+ fn declaration ( ) -> borsh:: schema:: Declaration {
739+ alloc:: format!( "BoundedVec<{}, {}, {}>" , T :: declaration( ) , L , U )
740+ }
741+ }
742+
743+ #[ cfg( test) ]
744+ mod tests {
745+ use borsh:: schema:: BorshSchemaContainer ;
746+
747+ use super :: * ;
748+ #[ test]
749+ #[ allow( clippy:: expect_used) ]
750+ fn borsh_encdec ( ) {
751+ let data: BoundedVec < u8 , 2 , 8 > = vec ! [ 1u8 , 2 ] . try_into ( ) . expect ( "borsh works" ) ;
752+ let buf = & mut Vec :: new ( ) ;
753+ data. serialize ( buf) . expect ( "borsh works" ) ;
754+ let decoded =
755+ BoundedVec :: < u8 , 2 , 8 > :: deserialize ( & mut buf. as_slice ( ) ) . expect ( "borsh works" ) ;
756+ let compatible_decoded =
757+ BoundedVec :: < u8 , 1 , 255 > :: deserialize ( & mut buf. as_slice ( ) ) . expect ( "borsh works" ) ;
758+ assert_eq ! ( data. get( 0 ) , decoded. get( 0 ) ) ;
759+ assert_eq ! ( data. get( 1 ) , decoded. get( 1 ) ) ;
760+ assert_eq ! ( data. get( 0 ) , compatible_decoded. get( 0 ) ) ;
761+ assert_eq ! ( data. get( 1 ) , compatible_decoded. get( 1 ) ) ;
762+ assert ! ( BoundedVec :: <u8 , 1 , 257 >:: deserialize( & mut buf. as_slice( ) ) . is_err( ) ) ;
763+
764+ let schema = BorshSchemaContainer :: for_type :: < BoundedVec < u8 , 2 , 8 > > ( ) ;
765+ let schema = schema
766+ . get_definition ( "BoundedVec<u8, 2, 8>" )
767+ . expect ( "borsh works" ) ;
768+ assert ! ( matches!(
769+ schema,
770+ borsh:: schema:: Definition :: Sequence {
771+ length_width: 1 ,
772+ ..
773+ }
774+ ) ) ;
775+ }
776+ }
777+ }
778+
581779#[ allow( clippy:: unwrap_used) ]
582780#[ cfg( feature = "arbitrary" ) ]
583781mod arbitrary {
@@ -653,8 +851,8 @@ mod serde_impl {
653851 use schemars:: schema:: { InstanceType , SchemaObject } ;
654852 use schemars:: JsonSchema ;
655853
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 > {
854+ // we cannot use `serde` attributes, because these do not work with `const`, only numeric literals supported
855+ impl < T : JsonSchema , const L : usize , const U : usize , W > JsonSchema for BoundedVec < T , L , U , W > {
658856 fn schema_name ( ) -> alloc:: string:: String {
659857 alloc:: format!( "BoundedVec{}Min{}Max{}" , T :: schema_name( ) , L , U )
660858 }
@@ -666,15 +864,35 @@ mod serde_impl {
666864 items : Some ( schemars:: schema:: SingleOrVec :: Single (
667865 T :: json_schema ( gen) . into ( ) ,
668866 ) ) ,
669- min_items : Some ( L as u32 ) ,
670- max_items : Some ( U as u32 ) ,
867+ #[ expect( clippy:: expect_used) ] // design time failure
868+ min_items : Some (
869+ u32:: try_from ( L ) . expect ( "JSON schema does not support so large ranges" ) ,
870+ ) ,
871+ #[ expect( clippy:: expect_used) ] // design time failure
872+ max_items : Some (
873+ u32:: try_from ( U ) . expect ( "JSON schema does not support so large ranges" ) ,
874+ ) ,
671875 ..Default :: default ( )
672876 } ) ) ,
673877 ..Default :: default ( )
674878 }
675879 . into ( )
676880 }
677881 }
882+
883+ #[ cfg( test) ]
884+ mod tests {
885+ use super :: * ;
886+ use schemars:: schema_for;
887+ #[ test]
888+ fn json_schema ( ) {
889+ let schema = schema_for ! ( BoundedVec <u8 , 2 , 8 >) ;
890+ let min_items = schema. schema . array . as_ref ( ) . unwrap ( ) . min_items . unwrap ( ) ;
891+ let max_items = schema. schema . array . as_ref ( ) . unwrap ( ) . max_items . unwrap ( ) ;
892+ assert_eq ! ( min_items, 2 ) ;
893+ assert_eq ! ( max_items, 8 ) ;
894+ }
895+ }
678896 }
679897}
680898
0 commit comments