diff --git a/Cargo.lock b/Cargo.lock index c6d327a..3c6d7ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,90 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "malloc_size_of" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d719de8b8f230028cf8192ae4c1b25267cd6b8a99d2747d345a70b8c81aa13" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -58,7 +130,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -74,12 +146,24 @@ dependencies = [ name = "smallvec" version = "2.0.0-alpha.12" dependencies = [ + "borsh", "bytes", "malloc_size_of", "serde_core", "serde_test", ] +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.3" @@ -91,8 +175,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index 19391ff..a58f369 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,12 +18,15 @@ std = [] specialization = [] may_dangle = [] serde = ["dep:serde_core"] +borsh = ["dep:borsh"] +borsh-unstable__schema = ["borsh", "borsh/unstable__schema"] internals = [] [dependencies] bytes = { version = "1", optional = true, default-features = false } serde_core = { version = "1.0.221", optional = true, default-features = false } malloc_size_of = { version = "0.1.1", optional = true, default-features = false } +borsh = { version = "1", optional = true, default-features = false } [dev-dependencies] serde_test = "1.0" diff --git a/src/lib.rs b/src/lib.rs index f7b1bba..b49f2cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,13 @@ //! When this optional dependency is enabled, `SmallVec` implements the `serde::Serialize` and //! `serde::Deserialize` traits. //! +//! ### `borsh` +//! +//! When this optional dependency is enabled, `SmallVec` implements the `borsh::BorshSerialize` +//! and `borsh::BorshDeserialize` traits. Enabling the additional `borsh-unstable__schema` +//! feature also implements `borsh::BorshSchema`, mirroring borsh's own `unstable__schema` +//! feature (unstable because it depends on borsh's derive machinery). +//! //! ### `specialization` //! //! **This feature is unstable and requires a nightly build of the Rust toolchain.** @@ -84,6 +91,13 @@ use core::ptr::copy; use core::ptr::copy_nonoverlapping; use core::ptr::NonNull; +#[cfg(feature = "borsh-unstable__schema")] +use borsh::BorshSchema; +#[cfg(feature = "borsh")] +use borsh::{ + io::{Read, Result as BorshIoResult, Write}, + BorshDeserialize, BorshSerialize, +}; #[cfg(feature = "bytes")] use bytes::{buf::UninitSlice, BufMut}; #[cfg(feature = "malloc_size_of")] @@ -2915,6 +2929,63 @@ where } } +#[cfg(feature = "borsh")] +#[cfg_attr(docsrs, doc(cfg(feature = "borsh")))] +impl BorshSerialize for SmallVec +where + T: BorshSerialize, +{ + #[inline] + fn serialize(&self, writer: &mut W) -> BorshIoResult<()> { + self.as_slice().serialize(writer) + } +} + +#[cfg(feature = "borsh")] +#[cfg_attr(docsrs, doc(cfg(feature = "borsh")))] +impl BorshDeserialize for SmallVec +where + T: BorshDeserialize, +{ + #[inline] + fn deserialize_reader(reader: &mut R) -> BorshIoResult { + // Deserialize element-by-element (same wire format as Vec: a u32 length + // prefix followed by that many elements) rather than building a Vec and + // converting with `from_vec`, which would always keep the heap allocation - + // even a result with `len <= N` would spill. Pushing keeps it inline as long + // as it fits, matching how the `serde` Deserialize impl above behaves. + let len = u32::deserialize_reader(reader)?; + let mut result = SmallVec::new(); + result.try_reserve(len as usize).map_err(|_| { + borsh::io::Error::new(borsh::io::ErrorKind::InvalidData, "allocation failure") + })?; + for _ in 0..len { + result.push(T::deserialize_reader(reader)?); + } + Ok(result) + } +} + +#[cfg(feature = "borsh-unstable__schema")] +#[cfg_attr(docsrs, doc(cfg(feature = "borsh-unstable__schema")))] +impl BorshSchema for SmallVec +where + T: BorshSchema, +{ + fn add_definitions_recursively( + definitions: &mut alloc::collections::BTreeMap< + borsh::schema::Declaration, + borsh::schema::Definition, + >, + ) { + as BorshSchema>::add_definitions_recursively(definitions); + } + + fn declaration() -> borsh::schema::Declaration { + as BorshSchema>::declaration() + } +} + #[cfg(feature = "malloc_size_of")] impl MallocShallowSizeOf for SmallVec { fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { diff --git a/src/tests.rs b/src/tests.rs index cfd801b..a7b32c9 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -820,6 +820,33 @@ fn test_serde() { ); } +#[cfg(feature = "borsh")] +#[test] +fn test_borsh() { + use borsh::{from_slice, to_vec}; + + let mut small_vec: SmallVec = SmallVec::new(); + small_vec.extend([1, 2, 3, 4]); + + let bytes = to_vec(&small_vec).unwrap(); + let decoded: SmallVec = from_slice(&bytes).unwrap(); + assert_eq!(small_vec, decoded); + assert!(decoded.spilled()); // 4 elements > inline capacity of 2 + + // Round-trip should also match a plain Vec's encoding. + let vec_bytes = to_vec(&alloc::vec![1i32, 2, 3, 4]).unwrap(); + assert_eq!(bytes, vec_bytes); + + // A result that fits inline should deserialize inline, not via a heap Vec + // conversion (regression test: an earlier version always spilled here). + let mut small: SmallVec = SmallVec::new(); + small.extend([1, 2]); + let small_bytes = to_vec(&small).unwrap(); + let small_decoded: SmallVec = from_slice(&small_bytes).unwrap(); + assert_eq!(small, small_decoded); + assert!(!small_decoded.spilled()); +} + #[test] fn grow_to_shrink() { let mut v: SmallVec = SmallVec::new();