Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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 }
Expand All @@ -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"
Expand All @@ -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]
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
]
```
```
83 changes: 83 additions & 0 deletions src/ext_impls/impl_bytecheck.rs
Original file line number Diff line number Diff line change
@@ -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<T, N: ArrayLength, C> CheckBytes<C> for GenericArray<T, N>
where
T: CheckBytes<C>,
C: Fallible + ?Sized,
C::Error: Trace,
{
#[inline]
unsafe fn check_bytes(value: *const Self, context: &mut C) -> Result<(), C::Error> {
let base = value.cast::<T>();
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<u8, U4> = arr![1, 2, 3, 4];
// SAFETY: pointer is to an aligned, fully-initialized GenericArray<u8, U4>.
unsafe {
check_bytes::<GenericArray<u8, U4>, Error>(&array).unwrap();
}
}

#[test]
fn test_check_bytes_empty() {
let array: GenericArray<u8, U0> = arr![];
// SAFETY: empty arrays are always valid.
unsafe {
check_bytes::<GenericArray<u8, U0>, 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<bool, U4>;
// SAFETY: pointer is aligned (u8 == bool alignment) and points to 4 initialized bytes.
let result = unsafe { check_bytes::<GenericArray<bool, U4>, Error>(ptr) };
assert!(result.is_err());
}
}
169 changes: 169 additions & 0 deletions src/ext_impls/impl_rkyv.rs
Original file line number Diff line number Diff line change
@@ -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<T, N>` is a `T` array and so is portable as long as `T` is also
// `Portable`.
unsafe impl<T: Portable, N: ArrayLength> Portable for GenericArray<T, N> {}
// SAFETY: `GenericArray<T, N>` is a `T` array and so has no uninitialized bytes as long as
// `T` also has no uninitialized bytes.
unsafe impl<T: NoUndef, N: ArrayLength> NoUndef for GenericArray<T, N> {}

/// 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<T, N: ArrayLength>(
place: Place<GenericArray<T, N>>,
i: usize,
) -> Place<T> {
// SAFETY: The caller has guaranteed that `i` is in-bounds for the array
// pointed to by this place.
let ptr = unsafe { place.ptr().cast::<T>().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::<T>(), ptr) }
}

impl<T: Archive, N: ArrayLength> Archive for GenericArray<T, N> {
const COPY_OPTIMIZATION: CopyOptimization<Self> =
unsafe { CopyOptimization::enable_if(T::COPY_OPTIMIZATION.is_enabled()) };

type Archived = GenericArray<T::Archived, N>;
type Resolver = GenericArray<T::Resolver, N>;

fn resolve(&self, resolver: Self::Resolver, out: Place<Self::Archived>) {
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<T, S, N: ArrayLength> Serialize<S> for GenericArray<T, N>
where
T: Serialize<S>,
S: Fallible + ?Sized,
{
fn serialize(&self, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
let mut result = core::mem::MaybeUninit::<Self::Resolver>::uninit();
let result_ptr = result.as_mut_ptr().cast::<T::Resolver>();
for (i, value) in self.iter().enumerate() {
unsafe {
result_ptr.add(i).write(value.serialize(serializer)?);
}
}
unsafe { Ok(result.assume_init()) }
}
}

impl<T, D, N: ArrayLength> Deserialize<GenericArray<T, N>, D> for GenericArray<T::Archived, N>
where
T: Archive,
T::Archived: Deserialize<T, D>,
D: Fallible + ?Sized,
{
fn deserialize(&self, deserializer: &mut D) -> Result<GenericArray<T, N>, D::Error> {
let mut result = core::mem::MaybeUninit::<GenericArray<T, N>>::uninit();
let result_ptr = result.as_mut_ptr().cast::<T>();
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<T: Portable + NoUndef>() {}
const _: () = assert_portable_noundef::<GenericArray<u8, U32>>();
const _: () = assert_portable_noundef::<GenericArray<u8, U0>>();

#[test]
fn test_rkyv_roundtrip() {
let array: GenericArray<u32, U6> = arr![1, 2, 3, 4, 5, 6];
let bytes = rkyv::to_bytes::<Error>(&array).unwrap();
let archived =
unsafe { rkyv::access_unchecked::<rkyv::Archived<GenericArray<u32, U6>>>(&bytes) };
for (i, el) in archived.iter().enumerate() {
assert_eq!(el.to_native(), array[i]);
}
let deserialized: GenericArray<u32, U6> =
rkyv::deserialize::<GenericArray<u32, U6>, 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<String, U3> = arr![
String::from("hello"),
String::from("rkyv"),
String::from("world")
];
let bytes = rkyv::to_bytes::<Error>(&array).unwrap();
let archived =
unsafe { rkyv::access_unchecked::<rkyv::Archived<GenericArray<String, U3>>>(&bytes) };
for (i, el) in archived.iter().enumerate() {
assert_eq!(el.as_str(), array[i].as_str());
}
let deserialized: GenericArray<String, U3> =
rkyv::deserialize::<GenericArray<String, U3>, 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<u32, U6> = arr![10, 20, 30, 40, 50, 60];
let bytes = rkyv::to_bytes::<Error>(&array).unwrap();
let deserialized: GenericArray<u32, U6> =
rkyv::from_bytes::<GenericArray<u32, U6>, Error>(&bytes).unwrap();
assert_eq!(deserialized, array);
}

#[test]
fn test_validation_rejects_truncated() {
let array: GenericArray<u32, U6> = arr![1, 2, 3, 4, 5, 6];
let bytes = rkyv::to_bytes::<Error>(&array).unwrap();
let truncated = &bytes[..bytes.len() - 1];
let result = rkyv::access::<rkyv::Archived<GenericArray<u32, U6>>, Error>(truncated);
assert!(result.is_err());
}
}
6 changes: 6 additions & 0 deletions src/ext_impls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 3 additions & 1 deletion tests/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading