Skip to content

Commit cc9eb35

Browse files
feat: borsh support and compile time limits to schemas
1 parent 39207d4 commit cc9eb35

4 files changed

Lines changed: 236 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77
<!-- next-header -->
88
## [Unreleased] - ReleaseDate
99
- prevent out of bound construction and define empty vs nonempty at compile time
10-
10+
- optional `borsh` support
11+
1112
## [0.7.1] - 2022-08-01
1213
### Added
1314
- fix `Abrbitrary` impl to honor upper(U) and lower(L) bounds;

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ serde = { version = "1.0.123", default-features = false, features = [
1515
schemars = { version = ">=0.8,<1", default-features = false, optional = true }
1616
thiserror = { version = "2", default-features = false }
1717
proptest = { version = "1.0.0", optional = true }
18+
borsh = { version = "1.5.4", default-features = false, features = ["unstable__schema"], optional = true}
1819

1920
[features]
2021
serde = ["dep:serde"]
2122
schema = ["serde", "dep:schemars"]
2223
arbitrary = ["proptest"]
24+
borsh = ["dep:borsh"]
2325

2426
[dev-dependencies]
2527
proptest = { version = "1.0.0" }

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
## bounded-vec
55
`BoundedVec<T, L, U>` - Non-empty rust `std::vec::Vec` wrapper with type guarantees on lower(`L`) and upper(`U`) bounds for items quantity. Inspired by [vec1](https://github.com/rustonaut/vec1).
6+
`EmptyBoundedVec<T,U>` if only upper bound `U` is needed.
67
This crate is `#![no_std]` compatible with `alloc`.
78

89
## Example
@@ -24,6 +25,9 @@ assert_eq!(data, [2u8,4].into());
2425
- optional(non-default) `serde` feature that adds serialization to `BoundedVec`.
2526
- optional(non-default) `schema` feature that adds JSON schema support via `schemars` (requires `serde`).
2627
- optional(non-default) `arbitrary` feature that adds `proptest::Arbitrary` implementation to `BoundedVec`.
28+
- optional(non-default) `borsh` feature that adds `borsh` binary encoding, decoding and schema
29+
- optional(nin-default) `arbitrary` for `proptest` support
30+
2731

2832
## Changelog
2933
See [CHANGELOG.md](CHANGELOG.md).

src/bounded_vec.rs

Lines changed: 228 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
1313
pub 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
4040
pub 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")]
583781
mod 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

Comments
 (0)