From 1cb431d15fd659fb138ee1fb62b3d3c28c40fcc3 Mon Sep 17 00:00:00 2001 From: Enduriel Date: Thu, 28 May 2026 09:41:40 +0200 Subject: [PATCH] feat: add rkyv and bytecheck features with a combined rkyv-full --- Cargo.toml | 26 ++++- README.md | 9 +- src/ext_impls/impl_bytecheck.rs | 83 ++++++++++++++++ src/ext_impls/impl_rkyv.rs | 169 ++++++++++++++++++++++++++++++++ src/ext_impls/mod.rs | 6 ++ tests/iter.rs | 4 +- 6 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 src/ext_impls/impl_bytecheck.rs create mode 100644 src/ext_impls/impl_rkyv.rs diff --git a/Cargo.toml b/Cargo.toml index 9466c8c754..d9ec15b303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ serde = ["dep:serde_core"] compat-0_14 = ["dep:generic_array-0_14"] as_slice = ["dep:as-slice"] bitvec = ["dep:bitvec", "const-default"] +rkyv-0_8 = ["dep:rkyv"] +bytecheck-0_8 = ["dep:bytecheck"] +rkyv-0_8-full = ["rkyv-0_8", "bytecheck-0_8"] [dependencies] typenum = { version = "1.19", features = ["const-generics"] } @@ -42,6 +45,8 @@ arbitrary = { version = "1", optional = true, default-features = false } bytemuck = { version = "1", optional = true, default-features = false } as-slice = { version = "0.2", optional = true, default-features = false } bitvec = { version = "1", optional = true, default-features = false } +rkyv = { version = "0.8", optional = true, default-features = false } +bytecheck = { version = "0.8", optional = true, default-features = false } generic_array-0_14 = { package = "generic-array", version = "0.14", optional = true, default-features = false } hybrid-array-0_4 = { package = "hybrid-array", version = "0.4", optional = true, default-features = false } @@ -53,6 +58,11 @@ bincode = "1.0" criterion = { version = "0.5", features = ["html_reports"] } rand = "0.9" aes = { version = "0.8.4", default-features = false } +rkyv = { version = "0.8", default-features = false, features = [ + "alloc", + "bytecheck", +] } +bytecheck = { version = "0.8", default-features = false } [[bench]] name = "hex" @@ -66,7 +76,21 @@ codegen-units = 1 [package.metadata.docs.rs] # all but "internals", don't show those on docs.rs -features = ["serde", "zeroize", "const-default", "alloc", "hybrid-array-0_4", "subtle", "arbitrary", "bytemuck", "bitvec", "as_slice"] +features = [ + "serde", + "zeroize", + "const-default", + "alloc", + "hybrid-array-0_4", + "subtle", + "arbitrary", + "bytemuck", + "bitvec", + "as_slice", + "rkyv-0_8", + "bytecheck-0_8", + "rkyv-0_8-full", +] rustdoc-args = ["--cfg", "docsrs"] [package.metadata.playground] diff --git a/README.md b/README.md index 8209bd1dab..227077a56b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This crate implements a structure that can be used as a generic array type. -**Requires minimum Rust version of 1.65.0 +\*\*Requires minimum Rust version of 1.65.0 [Documentation on GH Pages](https://fizyk20.github.io/generic-array/generic_array/) may be required to view certain types on foreign crates. @@ -133,6 +133,9 @@ features = [ "bytemuck", # Enables `bytemuck` crate support "bitvec", # Enables `bitvec` crate support to use GenericArray as a storage backend for bit arrays "compat-0_14", # Enables interoperability with `generic-array` 0.14 - "hybrid-array-0_4" # Enables interoperability with `hybrid-array` 0.4 + "hybrid-array-0_4", # Enables interoperability with `hybrid-array` 0.4 + "rkyv-0_8", # Zero copy Serialize/Deserialize implementation using `rkyv` 0.8 crate + "bytecheck-0_8", # Enables interoperability with `bytecheck` 0.8 crate + "rkyv-0_8-full" # Combined feature for `rkyv` and `bytecheck` allowing validation of rkyv deserialized types ] -``` \ No newline at end of file +``` diff --git a/src/ext_impls/impl_bytecheck.rs b/src/ext_impls/impl_bytecheck.rs new file mode 100644 index 0000000000..7abb28fc25 --- /dev/null +++ b/src/ext_impls/impl_bytecheck.rs @@ -0,0 +1,83 @@ +use core::fmt; + +use bytecheck::{ + rancor::{Fallible, ResultExt, Trace}, + CheckBytes, +}; + +use crate::{ArrayLength, GenericArray}; + +// Mirrors `bytecheck::ArrayCheckContext` and the `CheckBytes` impl for `[T; N]` in +// `bytecheck-0.8/src/lib.rs`, renamed so the trace message identifies the type as a +// `GenericArray`. +#[derive(Debug)] +struct GenericArrayCheckContext { + index: usize, +} + +impl fmt::Display for GenericArrayCheckContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "while checking index '{}' of GenericArray", self.index) + } +} + +// SAFETY: `check_bytes` only returns `Ok` if each element of the array is +// valid. If each element of the array is valid then the whole array is also +// valid. +unsafe impl CheckBytes for GenericArray +where + T: CheckBytes, + C: Fallible + ?Sized, + C::Error: Trace, +{ + #[inline] + unsafe fn check_bytes(value: *const Self, context: &mut C) -> Result<(), C::Error> { + let base = value.cast::(); + for index in 0..N::USIZE { + // SAFETY: The caller has guaranteed that `value` points to enough + // bytes for this array and is properly aligned, so we can create + // pointers to each element and check them. + unsafe { + T::check_bytes(base.add(index), context) + .with_trace(|| GenericArrayCheckContext { index })?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::typenum::{U0, U4}; + use crate::{arr, GenericArray}; + use bytecheck::check_bytes; + use bytecheck::rancor::Error; + + #[test] + fn test_check_bytes_valid_u8() { + let array: GenericArray = arr![1, 2, 3, 4]; + // SAFETY: pointer is to an aligned, fully-initialized GenericArray. + unsafe { + check_bytes::, Error>(&array).unwrap(); + } + } + + #[test] + fn test_check_bytes_empty() { + let array: GenericArray = arr![]; + // SAFETY: empty arrays are always valid. + unsafe { + check_bytes::, Error>(&array).unwrap(); + } + } + + #[test] + fn test_check_bytes_invalid_bool() { + // 0 and 1 are valid bool bit patterns; 2 is not. + let bytes: [u8; 4] = [1, 0, 1, 2]; + let ptr = &bytes as *const [u8; 4] as *const GenericArray; + // SAFETY: pointer is aligned (u8 == bool alignment) and points to 4 initialized bytes. + let result = unsafe { check_bytes::, Error>(ptr) }; + assert!(result.is_err()); + } +} diff --git a/src/ext_impls/impl_rkyv.rs b/src/ext_impls/impl_rkyv.rs new file mode 100644 index 0000000000..4bd7175ab9 --- /dev/null +++ b/src/ext_impls/impl_rkyv.rs @@ -0,0 +1,169 @@ +// The `Archive`, `Serialize` and `Deserialize` impls below mirror rkyv's own impls for +// `[T; N]` (`rkyv-0.8/src/impls/core/mod.rs`) 1:1. In particular, they share the same +// behavior on a failed element-wise serialize/deserialize: previously-written entries in +// the resolver/result `MaybeUninit` are leaked rather than dropped. For typical rkyv +// `Resolver`s (often `()`) and `Copy` element types this is a non-issue; staying in sync +// with upstream is preferable to diverging here. + +use core::mem; + +use rkyv::{ + rancor::Fallible, + traits::{CopyOptimization, NoUndef}, + Archive, Deserialize, Place, Portable, Serialize, +}; + +use crate::{ArrayLength, GenericArray}; + +// SAFETY: `GenericArray` is a `T` array and so is portable as long as `T` is also +// `Portable`. +unsafe impl Portable for GenericArray {} +// SAFETY: `GenericArray` is a `T` array and so has no uninitialized bytes as long as +// `T` also has no uninitialized bytes. +unsafe impl NoUndef for GenericArray {} + +/// Gets a `Place` to the `i`-th element of the array. +/// +/// # Safety +/// +/// `i` must be in-bounds for the array pointed to by this place. +/// +/// This is a 1:1 copy of [`Place<[T; N]>::index`] +unsafe fn index_for_place_generic_array( + place: Place>, + i: usize, +) -> Place { + // SAFETY: The caller has guaranteed that `i` is in-bounds for the array + // pointed to by this place. + let ptr = unsafe { place.ptr().cast::().add(i) }; + // SAFETY: `ptr` is an element of `self`, and so is also properly + // aligned, dereferenceable, and all of its bytes are initialized. + unsafe { Place::new_unchecked(place.pos() + i * mem::size_of::(), ptr) } +} + +impl Archive for GenericArray { + const COPY_OPTIMIZATION: CopyOptimization = + unsafe { CopyOptimization::enable_if(T::COPY_OPTIMIZATION.is_enabled()) }; + + type Archived = GenericArray; + type Resolver = GenericArray; + + fn resolve(&self, resolver: Self::Resolver, out: Place) { + for (i, (value, resolver)) in self.iter().zip(resolver).enumerate() { + let out_i = unsafe { index_for_place_generic_array(out, i) }; + value.resolve(resolver, out_i); + } + } +} + +impl Serialize for GenericArray +where + T: Serialize, + S: Fallible + ?Sized, +{ + fn serialize(&self, serializer: &mut S) -> Result { + let mut result = core::mem::MaybeUninit::::uninit(); + let result_ptr = result.as_mut_ptr().cast::(); + for (i, value) in self.iter().enumerate() { + unsafe { + result_ptr.add(i).write(value.serialize(serializer)?); + } + } + unsafe { Ok(result.assume_init()) } + } +} + +impl Deserialize, D> for GenericArray +where + T: Archive, + T::Archived: Deserialize, + D: Fallible + ?Sized, +{ + fn deserialize(&self, deserializer: &mut D) -> Result, D::Error> { + let mut result = core::mem::MaybeUninit::>::uninit(); + let result_ptr = result.as_mut_ptr().cast::(); + for (i, value) in self.iter().enumerate() { + unsafe { + result_ptr.add(i).write(value.deserialize(deserializer)?); + } + } + unsafe { Ok(result.assume_init()) } + } +} + +#[cfg(test)] +mod tests { + use crate::typenum::{U0, U32, U6}; + use crate::{arr, GenericArray}; + use rkyv::rancor::Error; + use rkyv::traits::{NoUndef, Portable}; + + const fn assert_portable_noundef() {} + const _: () = assert_portable_noundef::>(); + const _: () = assert_portable_noundef::>(); + + #[test] + fn test_rkyv_roundtrip() { + let array: GenericArray = arr![1, 2, 3, 4, 5, 6]; + let bytes = rkyv::to_bytes::(&array).unwrap(); + let archived = + unsafe { rkyv::access_unchecked::>>(&bytes) }; + for (i, el) in archived.iter().enumerate() { + assert_eq!(el.to_native(), array[i]); + } + let deserialized: GenericArray = + rkyv::deserialize::, Error>(archived).unwrap(); + assert_eq!(deserialized, array); + } + + // Exercises a `T` with a non-trivial `Resolver` and `Drop` (`String` archives via an + // out-of-line buffer, so `Resolver` carries position metadata that owns nothing but + // the deserialized `T` does). A regression in either Serialize or Deserialize that + // miscounted indices would surface here as a corrupted string or a leak under Miri. + #[cfg(feature = "alloc")] + #[test] + fn test_rkyv_roundtrip_string() { + use alloc::string::String; + use typenum::U3; + + let array: GenericArray = arr![ + String::from("hello"), + String::from("rkyv"), + String::from("world") + ]; + let bytes = rkyv::to_bytes::(&array).unwrap(); + let archived = + unsafe { rkyv::access_unchecked::>>(&bytes) }; + for (i, el) in archived.iter().enumerate() { + assert_eq!(el.as_str(), array[i].as_str()); + } + let deserialized: GenericArray = + rkyv::deserialize::, Error>(archived).unwrap(); + assert_eq!(deserialized, array); + } +} + +#[cfg(all(test, feature = "bytecheck-0_8"))] +mod tests_full { + use crate::typenum::U6; + use crate::{arr, GenericArray}; + use rkyv::rancor::Error; + + #[test] + fn test_validated_roundtrip() { + let array: GenericArray = arr![10, 20, 30, 40, 50, 60]; + let bytes = rkyv::to_bytes::(&array).unwrap(); + let deserialized: GenericArray = + rkyv::from_bytes::, Error>(&bytes).unwrap(); + assert_eq!(deserialized, array); + } + + #[test] + fn test_validation_rejects_truncated() { + let array: GenericArray = arr![1, 2, 3, 4, 5, 6]; + let bytes = rkyv::to_bytes::(&array).unwrap(); + let truncated = &bytes[..bytes.len() - 1]; + let result = rkyv::access::>, Error>(truncated); + assert!(result.is_err()); + } +} diff --git a/src/ext_impls/mod.rs b/src/ext_impls/mod.rs index ee586e1203..2c58fbf727 100644 --- a/src/ext_impls/mod.rs +++ b/src/ext_impls/mod.rs @@ -24,3 +24,9 @@ mod impl_as_slice; #[cfg(feature = "bitvec")] mod impl_bitvec; + +#[cfg(feature = "rkyv-0_8")] +mod impl_rkyv; + +#[cfg(feature = "bytecheck-0_8")] +mod impl_bytecheck; diff --git a/tests/iter.rs b/tests/iter.rs index 7347e3b7f1..1637767a1f 100644 --- a/tests/iter.rs +++ b/tests/iter.rs @@ -38,7 +38,9 @@ fn test_into_iter_as_slice() { assert_eq!(into_iter.as_slice(), &['b', 'c']); let _ = into_iter.next().unwrap(); let _ = into_iter.next().unwrap(); - assert_eq!(into_iter.as_slice(), &[]); + // Explicit type annotation needed because `rend` (pulled in by the `rkyv` feature) adds + // additional `PartialEq` impls for `char`, leaving `&[]`'s element type ambiguous. + assert_eq!(into_iter.as_slice(), &[] as &[char]); } #[test]