diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7631a57b..312f4533 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: features: rustc-rayon - rust: stable features: serde + - rust: stable + features: borsh - rust: stable features: std - rust: beta @@ -119,8 +121,10 @@ jobs: with: tool: cargo-hack - run: cargo +nightly hack generate-lockfile --remove-dev-deps -Z direct-minimal-versions - - name: Build - run: cargo build --verbose --all-features + - name: Build (nightly) + run: cargo +nightly build --verbose --all-features + - name: Build (MSRV) + run: cargo build --verbose --features arbitrary,quickcheck,serde,rayon # One job that "summarizes" the success state of this pipeline. This can then be added to branch # protection, rather than having to add each job separately. diff --git a/Cargo.toml b/Cargo.toml index 95216563..13a54dea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ equivalent = { version = "1.0", default-features = false } arbitrary = { version = "1.0", optional = true, default-features = false } quickcheck = { version = "1.0", optional = true, default-features = false } serde = { version = "1.0", optional = true, default-features = false } +borsh = { version = "1.2", optional = true, default-features = false } rayon = { version = "1.5.3", optional = true } # Internal feature, only used when building as part of rustc, @@ -54,7 +55,7 @@ no-dev-version = true tag-name = "{{version}}" [package.metadata.docs.rs] -features = ["arbitrary", "quickcheck", "serde", "rayon"] +features = ["arbitrary", "quickcheck", "serde", "borsh", "rayon"] rustdoc-args = ["--cfg", "docsrs"] [workspace] diff --git a/src/borsh.rs b/src/borsh.rs new file mode 100644 index 00000000..7b4afdc4 --- /dev/null +++ b/src/borsh.rs @@ -0,0 +1,123 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "borsh")))] + +use alloc::vec::Vec; +use core::hash::BuildHasher; +use core::hash::Hash; +use core::iter::ExactSizeIterator; +use core::mem::size_of; + +use borsh::error::ERROR_ZST_FORBIDDEN; +use borsh::io::{Error, ErrorKind, Read, Result, Write}; +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::map::IndexMap; +use crate::set::IndexSet; + +impl<K, V, S> BorshSerialize for IndexMap<K, V, S> +where + K: BorshSerialize, + V: BorshSerialize, +{ + #[inline] + fn serialize<W: Write>(&self, writer: &mut W) -> Result<()> { + check_zst::<K>()?; + + let iterator = self.iter(); + + u32::try_from(iterator.len()) + .map_err(|_| ErrorKind::InvalidData)? + .serialize(writer)?; + + for (key, value) in iterator { + key.serialize(writer)?; + value.serialize(writer)?; + } + + Ok(()) + } +} + +impl<K, V, S> BorshDeserialize for IndexMap<K, V, S> +where + K: BorshDeserialize + Eq + Hash, + V: BorshDeserialize, + S: BuildHasher + Default, +{ + #[inline] + fn deserialize_reader<R: Read>(reader: &mut R) -> Result<Self> { + check_zst::<K>()?; + let vec = <Vec<(K, V)>>::deserialize_reader(reader)?; + Ok(vec.into_iter().collect::<IndexMap<K, V, S>>()) + } +} + +impl<T, S> BorshSerialize for IndexSet<T, S> +where + T: BorshSerialize, +{ + #[inline] + fn serialize<W: Write>(&self, writer: &mut W) -> Result<()> { + check_zst::<T>()?; + + let iterator = self.iter(); + + u32::try_from(iterator.len()) + .map_err(|_| ErrorKind::InvalidData)? + .serialize(writer)?; + + for item in iterator { + item.serialize(writer)?; + } + + Ok(()) + } +} + +impl<T, S> BorshDeserialize for IndexSet<T, S> +where + T: BorshDeserialize + Eq + Hash, + S: BuildHasher + Default, +{ + #[inline] + fn deserialize_reader<R: Read>(reader: &mut R) -> Result<Self> { + check_zst::<T>()?; + let vec = <Vec<T>>::deserialize_reader(reader)?; + Ok(vec.into_iter().collect::<IndexSet<T, S>>()) + } +} + +fn check_zst<T>() -> Result<()> { + if size_of::<T>() == 0 { + return Err(Error::new(ErrorKind::InvalidData, ERROR_ZST_FORBIDDEN)); + } + Ok(()) +} + +#[cfg(test)] +mod borsh_tests { + use super::*; + + #[test] + fn map_borsh_roundtrip() { + let original_map: IndexMap<i32, i32> = { + let mut map = IndexMap::new(); + map.insert(1, 2); + map.insert(3, 4); + map.insert(5, 6); + map + }; + let serialized_map = borsh::to_vec(&original_map).unwrap(); + let deserialized_map: IndexMap<i32, i32> = + BorshDeserialize::try_from_slice(&serialized_map).unwrap(); + assert_eq!(original_map, deserialized_map); + } + + #[test] + fn set_borsh_roundtrip() { + let original_map: IndexSet<i32> = [1, 2, 3, 4, 5, 6].into_iter().collect(); + let serialized_map = borsh::to_vec(&original_map).unwrap(); + let deserialized_map: IndexSet<i32> = + BorshDeserialize::try_from_slice(&serialized_map).unwrap(); + assert_eq!(original_map, deserialized_map); + } +} diff --git a/src/lib.rs b/src/lib.rs index b88c1bce..d6d3ede9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,8 @@ //! to [`IndexMap`] and [`IndexSet`]. Alternative implementations for //! (de)serializing [`IndexMap`] as an ordered sequence are available in the //! [`map::serde_seq`] module. +//! * `borsh`: Adds implementations for [`BorshSerialize`] and [`BorshDeserialize`] +//! to [`IndexMap`] and [`IndexSet`]. //! * `arbitrary`: Adds implementations for the [`arbitrary::Arbitrary`] trait //! to [`IndexMap`] and [`IndexSet`]. //! * `quickcheck`: Adds implementations for the [`quickcheck::Arbitrary`] trait @@ -46,6 +48,8 @@ //! [`no_std`]: #no-standard-library-targets //! [`Serialize`]: `::serde::Serialize` //! [`Deserialize`]: `::serde::Deserialize` +//! [`BorshSerialize`]: `::borsh::BorshSerialize` +//! [`BorshDeserialize`]: `::borsh::BorshDeserialize` //! [`arbitrary::Arbitrary`]: `::arbitrary::Arbitrary` //! [`quickcheck::Arbitrary`]: `::quickcheck::Arbitrary` //! @@ -110,6 +114,8 @@ use alloc::vec::{self, Vec}; mod arbitrary; #[macro_use] mod macros; +#[cfg(feature = "borsh")] +mod borsh; mod mutable_keys; #[cfg(feature = "serde")] mod serde;