Skip to content

Commit e47732e

Browse files
feat: borsh support and compile time limits to schemas
added tests clean up tuning package avoid conflicts with serde traits clippy fixes u32::MAX limit for borsh fail fast chores
1 parent 39207d4 commit e47732e

4 files changed

Lines changed: 230 additions & 7 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: 222 additions & 6 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>,
@@ -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")]
583779
mod 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

Comments
 (0)