Skip to content

Commit 0e31299

Browse files
committed
feat: add rkyv and bytecheck features with a combined rkyv-full
1 parent 82aa258 commit 0e31299

6 files changed

Lines changed: 292 additions & 5 deletions

File tree

Cargo.toml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ serde = ["dep:serde_core"]
2828
compat-0_14 = ["dep:generic_array-0_14"]
2929
as_slice = ["dep:as-slice"]
3030
bitvec = ["dep:bitvec", "const-default"]
31+
rkyv-0_8 = ["dep:rkyv"]
32+
bytecheck-0_8 = ["dep:bytecheck"]
33+
rkyv-0_8-full = ["rkyv-0_8", "bytecheck-0_8"]
3134

3235
[dependencies]
3336
typenum = { version = "1.19", features = ["const-generics"] }
@@ -42,6 +45,8 @@ arbitrary = { version = "1", optional = true, default-features = false }
4245
bytemuck = { version = "1", optional = true, default-features = false }
4346
as-slice = { version = "0.2", optional = true, default-features = false }
4447
bitvec = { version = "1", optional = true, default-features = false }
48+
rkyv = { version = "0.8", optional = true, default-features = false }
49+
bytecheck = { version = "0.8", optional = true, default-features = false }
4550

4651
generic_array-0_14 = { package = "generic-array", version = "0.14", optional = true, default-features = false }
4752
hybrid-array-0_4 = { package = "hybrid-array", version = "0.4", optional = true, default-features = false }
@@ -53,6 +58,11 @@ bincode = "1.0"
5358
criterion = { version = "0.5", features = ["html_reports"] }
5459
rand = "0.9"
5560
aes = { version = "0.8.4", default-features = false }
61+
rkyv = { version = "0.8", default-features = false, features = [
62+
"alloc",
63+
"bytecheck",
64+
] }
65+
bytecheck = { version = "0.8", default-features = false }
5666

5767
[[bench]]
5868
name = "hex"
@@ -66,7 +76,21 @@ codegen-units = 1
6676

6777
[package.metadata.docs.rs]
6878
# all but "internals", don't show those on docs.rs
69-
features = ["serde", "zeroize", "const-default", "alloc", "hybrid-array-0_4", "subtle", "arbitrary", "bytemuck", "bitvec", "as_slice"]
79+
features = [
80+
"serde",
81+
"zeroize",
82+
"const-default",
83+
"alloc",
84+
"hybrid-array-0_4",
85+
"subtle",
86+
"arbitrary",
87+
"bytemuck",
88+
"bitvec",
89+
"as_slice",
90+
"rkyv-08",
91+
"bytecheck-08",
92+
"rkyv-08-full",
93+
]
7094
rustdoc-args = ["--cfg", "docsrs"]
7195

7296
[package.metadata.playground]

README.md

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

77
This crate implements a structure that can be used as a generic array type.
88

9-
**Requires minimum Rust version of 1.65.0
9+
\*\*Requires minimum Rust version of 1.65.0
1010

1111
[Documentation on GH Pages](https://fizyk20.github.io/generic-array/generic_array/) may be required to view certain types on foreign crates.
1212

@@ -133,6 +133,9 @@ features = [
133133
"bytemuck", # Enables `bytemuck` crate support
134134
"bitvec", # Enables `bitvec` crate support to use GenericArray as a storage backend for bit arrays
135135
"compat-0_14", # Enables interoperability with `generic-array` 0.14
136-
"hybrid-array-0_4" # Enables interoperability with `hybrid-array` 0.4
136+
"hybrid-array-0_4", # Enables interoperability with `hybrid-array` 0.4
137+
"rkyv-0_8", # Zero copy Serialize/Deserialize implementation using `rkyv` 0.8 crate
138+
"bytecheck-0_8", # Enables interoperability with `bytecheck` 0.8 crate
139+
"rkyv-0_8-full" # Combined feature for `rkyv` and `bytecheck` allowing validation of rkyv deserialized types
137140
]
138-
```
141+
```

src/ext_impls/impl_bytecheck.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use core::fmt;
2+
3+
use bytecheck::{
4+
rancor::{Fallible, ResultExt, Trace},
5+
CheckBytes,
6+
};
7+
8+
use crate::{ArrayLength, GenericArray};
9+
10+
// Mirrors `bytecheck::ArrayCheckContext` and the `CheckBytes` impl for `[T; N]` in
11+
// `bytecheck-0.8/src/lib.rs`, renamed so the trace message identifies the type as a
12+
// `GenericArray`.
13+
#[derive(Debug)]
14+
struct GenericArrayCheckContext {
15+
index: usize,
16+
}
17+
18+
impl fmt::Display for GenericArrayCheckContext {
19+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20+
write!(f, "while checking index '{}' of GenericArray", self.index)
21+
}
22+
}
23+
24+
// SAFETY: `check_bytes` only returns `Ok` if each element of the array is
25+
// valid. If each element of the array is valid then the whole array is also
26+
// valid.
27+
unsafe impl<T, N: ArrayLength, C> CheckBytes<C> for GenericArray<T, N>
28+
where
29+
T: CheckBytes<C>,
30+
C: Fallible + ?Sized,
31+
C::Error: Trace,
32+
{
33+
#[inline]
34+
unsafe fn check_bytes(value: *const Self, context: &mut C) -> Result<(), C::Error> {
35+
let base = value.cast::<T>();
36+
for index in 0..N::USIZE {
37+
// SAFETY: The caller has guaranteed that `value` points to enough
38+
// bytes for this array and is properly aligned, so we can create
39+
// pointers to each element and check them.
40+
unsafe {
41+
T::check_bytes(base.add(index), context)
42+
.with_trace(|| GenericArrayCheckContext { index })?;
43+
}
44+
}
45+
Ok(())
46+
}
47+
}
48+
49+
#[cfg(test)]
50+
mod tests {
51+
use crate::typenum::{U0, U4};
52+
use crate::{arr, GenericArray};
53+
use bytecheck::check_bytes;
54+
use bytecheck::rancor::Error;
55+
56+
#[test]
57+
fn test_check_bytes_valid_u8() {
58+
let array: GenericArray<u8, U4> = arr![1, 2, 3, 4];
59+
// SAFETY: pointer is to an aligned, fully-initialized GenericArray<u8, U4>.
60+
unsafe {
61+
check_bytes::<GenericArray<u8, U4>, Error>(&array).unwrap();
62+
}
63+
}
64+
65+
#[test]
66+
fn test_check_bytes_empty() {
67+
let array: GenericArray<u8, U0> = arr![];
68+
// SAFETY: empty arrays are always valid.
69+
unsafe {
70+
check_bytes::<GenericArray<u8, U0>, Error>(&array).unwrap();
71+
}
72+
}
73+
74+
#[test]
75+
fn test_check_bytes_invalid_bool() {
76+
// 0 and 1 are valid bool bit patterns; 2 is not.
77+
let bytes: [u8; 4] = [1, 0, 1, 2];
78+
let ptr = &bytes as *const [u8; 4] as *const GenericArray<bool, U4>;
79+
// SAFETY: pointer is aligned (u8 == bool alignment) and points to 4 initialized bytes.
80+
let result = unsafe { check_bytes::<GenericArray<bool, U4>, Error>(ptr) };
81+
assert!(result.is_err());
82+
}
83+
}

src/ext_impls/impl_rkyv.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// The `Archive`, `Serialize` and `Deserialize` impls below mirror rkyv's own impls for
2+
// `[T; N]` (`rkyv-0.8/src/impls/core/mod.rs`) 1:1. In particular, they share the same
3+
// behavior on a failed element-wise serialize/deserialize: previously-written entries in
4+
// the resolver/result `MaybeUninit` are leaked rather than dropped. For typical rkyv
5+
// `Resolver`s (often `()`) and `Copy` element types this is a non-issue; staying in sync
6+
// with upstream is preferable to diverging here.
7+
8+
use core::mem;
9+
10+
use rkyv::{
11+
rancor::Fallible,
12+
traits::{CopyOptimization, NoUndef},
13+
Archive, Deserialize, Place, Portable, Serialize,
14+
};
15+
16+
use crate::{ArrayLength, GenericArray};
17+
18+
// SAFETY: `GenericArray<T, N>` is a `T` array and so is portable as long as `T` is also
19+
// `Portable`.
20+
unsafe impl<T: Portable, N: ArrayLength> Portable for GenericArray<T, N> {}
21+
// SAFETY: `GenericArray<T, N>` is a `T` array and so has no uninitialized bytes as long as
22+
// `T` also has no uninitialized bytes.
23+
unsafe impl<T: NoUndef, N: ArrayLength> NoUndef for GenericArray<T, N> {}
24+
25+
/// Gets a `Place` to the `i`-th element of the array.
26+
///
27+
/// # Safety
28+
///
29+
/// `i` must be in-bounds for the array pointed to by this place.
30+
///
31+
/// This is a 1:1 copy of [`Place<[T; N]>::index`]
32+
unsafe fn index_for_place_generic_array<T, N: ArrayLength>(
33+
place: Place<GenericArray<T, N>>,
34+
i: usize,
35+
) -> Place<T> {
36+
// SAFETY: The caller has guaranteed that `i` is in-bounds for the array
37+
// pointed to by this place.
38+
let ptr = unsafe { place.ptr().cast::<T>().add(i) };
39+
// SAFETY: `ptr` is an element of `self`, and so is also properly
40+
// aligned, dereferenceable, and all of its bytes are initialized.
41+
unsafe { Place::new_unchecked(place.pos() + i * mem::size_of::<T>(), ptr) }
42+
}
43+
44+
impl<T: Archive, N: ArrayLength> Archive for GenericArray<T, N> {
45+
const COPY_OPTIMIZATION: CopyOptimization<Self> =
46+
unsafe { CopyOptimization::enable_if(T::COPY_OPTIMIZATION.is_enabled()) };
47+
48+
type Archived = GenericArray<T::Archived, N>;
49+
type Resolver = GenericArray<T::Resolver, N>;
50+
51+
fn resolve(&self, resolver: Self::Resolver, out: Place<Self::Archived>) {
52+
for (i, (value, resolver)) in self.iter().zip(resolver).enumerate() {
53+
let out_i = unsafe { index_for_place_generic_array(out, i) };
54+
value.resolve(resolver, out_i);
55+
}
56+
}
57+
}
58+
59+
impl<T, S, N: ArrayLength> Serialize<S> for GenericArray<T, N>
60+
where
61+
T: Serialize<S>,
62+
S: Fallible + ?Sized,
63+
{
64+
fn serialize(&self, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
65+
let mut result = core::mem::MaybeUninit::<Self::Resolver>::uninit();
66+
let result_ptr = result.as_mut_ptr().cast::<T::Resolver>();
67+
for (i, value) in self.iter().enumerate() {
68+
unsafe {
69+
result_ptr.add(i).write(value.serialize(serializer)?);
70+
}
71+
}
72+
unsafe { Ok(result.assume_init()) }
73+
}
74+
}
75+
76+
impl<T, D, N: ArrayLength> Deserialize<GenericArray<T, N>, D> for GenericArray<T::Archived, N>
77+
where
78+
T: Archive,
79+
T::Archived: Deserialize<T, D>,
80+
D: Fallible + ?Sized,
81+
{
82+
fn deserialize(&self, deserializer: &mut D) -> Result<GenericArray<T, N>, D::Error> {
83+
let mut result = core::mem::MaybeUninit::<GenericArray<T, N>>::uninit();
84+
let result_ptr = result.as_mut_ptr().cast::<T>();
85+
for (i, value) in self.iter().enumerate() {
86+
unsafe {
87+
result_ptr.add(i).write(value.deserialize(deserializer)?);
88+
}
89+
}
90+
unsafe { Ok(result.assume_init()) }
91+
}
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use crate::typenum::{U0, U32, U6};
97+
use crate::{arr, GenericArray};
98+
use rkyv::rancor::Error;
99+
use rkyv::traits::{NoUndef, Portable};
100+
101+
const fn assert_portable_noundef<T: Portable + NoUndef>() {}
102+
const _: () = assert_portable_noundef::<GenericArray<u8, U32>>();
103+
const _: () = assert_portable_noundef::<GenericArray<u8, U0>>();
104+
105+
#[test]
106+
fn test_rkyv_roundtrip() {
107+
let array: GenericArray<u32, U6> = arr![1, 2, 3, 4, 5, 6];
108+
let bytes = rkyv::to_bytes::<Error>(&array).unwrap();
109+
let archived =
110+
unsafe { rkyv::access_unchecked::<rkyv::Archived<GenericArray<u32, U6>>>(&bytes) };
111+
for (i, el) in archived.iter().enumerate() {
112+
assert_eq!(el.to_native(), array[i]);
113+
}
114+
let deserialized: GenericArray<u32, U6> =
115+
rkyv::deserialize::<GenericArray<u32, U6>, Error>(archived).unwrap();
116+
assert_eq!(deserialized, array);
117+
}
118+
119+
// Exercises a `T` with a non-trivial `Resolver` and `Drop` (`String` archives via an
120+
// out-of-line buffer, so `Resolver` carries position metadata that owns nothing but
121+
// the deserialized `T` does). A regression in either Serialize or Deserialize that
122+
// miscounted indices would surface here as a corrupted string or a leak under Miri.
123+
#[cfg(feature = "alloc")]
124+
#[test]
125+
fn test_rkyv_roundtrip_string() {
126+
use alloc::string::String;
127+
use typenum::U3;
128+
129+
let array: GenericArray<String, U3> = arr![
130+
String::from("hello"),
131+
String::from("rkyv"),
132+
String::from("world")
133+
];
134+
let bytes = rkyv::to_bytes::<Error>(&array).unwrap();
135+
let archived =
136+
unsafe { rkyv::access_unchecked::<rkyv::Archived<GenericArray<String, U3>>>(&bytes) };
137+
for (i, el) in archived.iter().enumerate() {
138+
assert_eq!(el.as_str(), array[i].as_str());
139+
}
140+
let deserialized: GenericArray<String, U3> =
141+
rkyv::deserialize::<GenericArray<String, U3>, Error>(archived).unwrap();
142+
assert_eq!(deserialized, array);
143+
}
144+
}
145+
146+
#[cfg(all(test, feature = "bytecheck-0_8"))]
147+
mod tests_full {
148+
use crate::typenum::U6;
149+
use crate::{arr, GenericArray};
150+
use rkyv::rancor::Error;
151+
152+
#[test]
153+
fn test_validated_roundtrip() {
154+
let array: GenericArray<u32, U6> = arr![10, 20, 30, 40, 50, 60];
155+
let bytes = rkyv::to_bytes::<Error>(&array).unwrap();
156+
let deserialized: GenericArray<u32, U6> =
157+
rkyv::from_bytes::<GenericArray<u32, U6>, Error>(&bytes).unwrap();
158+
assert_eq!(deserialized, array);
159+
}
160+
161+
#[test]
162+
fn test_validation_rejects_truncated() {
163+
let array: GenericArray<u32, U6> = arr![1, 2, 3, 4, 5, 6];
164+
let bytes = rkyv::to_bytes::<Error>(&array).unwrap();
165+
let truncated = &bytes[..bytes.len() - 1];
166+
let result = rkyv::access::<rkyv::Archived<GenericArray<u32, U6>>, Error>(truncated);
167+
assert!(result.is_err());
168+
}
169+
}

src/ext_impls/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@ mod impl_as_slice;
2424

2525
#[cfg(feature = "bitvec")]
2626
mod impl_bitvec;
27+
28+
#[cfg(feature = "rkyv-0_8")]
29+
mod impl_rkyv;
30+
31+
#[cfg(feature = "bytecheck-0_8")]
32+
mod impl_bytecheck;

tests/iter.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ fn test_into_iter_as_slice() {
3838
assert_eq!(into_iter.as_slice(), &['b', 'c']);
3939
let _ = into_iter.next().unwrap();
4040
let _ = into_iter.next().unwrap();
41-
assert_eq!(into_iter.as_slice(), &[]);
41+
// Explicit type annotation needed because `rend` (pulled in by the `rkyv` feature) adds
42+
// additional `PartialEq` impls for `char`, leaving `&[]`'s element type ambiguous.
43+
assert_eq!(into_iter.as_slice(), &[] as &[char]);
4244
}
4345

4446
#[test]

0 commit comments

Comments
 (0)