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
15 changes: 15 additions & 0 deletions .github/workflows/test-rkyv.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: test rkyv
run-name: ${{ github.actor }}'s patch
on: [push, pull_request]
jobs:
build-and-test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: true
toolchain: nightly
- run: |
cargo test --no-default-features --features=rkyv
cargo test --no-default-features --features=rkyv,std
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Added optional `bytecheck` support. Enable using the `bytecheck` feature.
- Added optional `schemars` v1 support. Enable using the `schemars1` feature.
- Implemented `num_traits::Zero`.
- Add support for the [rkyv](https://crates.io/crates/rkyv) crate using the optional `rkyv` feature.

### Fixed

Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ defmt = ["dep:defmt"]
# Supports serde
serde = ["dep:serde"]

# Supports rkyv
rkyv = ["dep:rkyv", "bytecheck"]

borsh = ["dep:borsh"]

schemars = ["dep:schemars", "std"]
Expand All @@ -52,6 +55,7 @@ hint = []
num-traits = { version = "0.2.19", default-features = false, optional = true }
defmt = { version = "1", optional = true }
serde = { version = "1.0", optional = true, default-features = false }
rkyv = { version = "0.8.17", optional = true, default-features = false, features = ["bytecheck"] }
borsh = { version = "1.5.1", optional = true, features = ["unstable__schema"], default-features = false }
schemars = { version = "0.8.21", optional = true, features = ["derive"], default-features = false }
schemars1 = { package = "schemars", version = "1", optional = true, default-features = false }
Expand All @@ -65,3 +69,5 @@ quickcheck = { version = "1", optional = true, default-features = false }
[dev-dependencies]
serde_test = "1.0"
serde_json = "1.0"
# Need alloc for tests
rkyv = { version = "0.8.17", default-features = false, features = ["alloc"] }
26 changes: 26 additions & 0 deletions src/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ impl_signed_integer_native!((i8, u8), (i16, u16), (i32, u32), (i64, u64), (i128,
#[derive(Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "bytecheck", derive(bytecheck::CheckBytes))]
#[cfg_attr(feature = "bytecheck", bytecheck(verify))]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize),
rkyv(bytecheck(verify))
)]
Comment thread
IzawGithub marked this conversation as resolved.
#[repr(transparent)]
pub struct Int<T: SignedInteger + BuiltinInteger, const BITS: usize> {
value: T,
Expand Down Expand Up @@ -1568,6 +1573,27 @@ where
}
}

#[cfg(feature = "rkyv")]
unsafe impl<
T: SignedInteger + BuiltinInteger + rkyv::Archive,
const BITS: usize,
C: rkyv::bytecheck::rancor::Fallible + ?Sized,
> rkyv::bytecheck::Verify<C> for ArchivedInt<T, BITS>
where
C::Error: rkyv::bytecheck::rancor::Source,
Int<T, BITS>: Integer,
T: From<T::Archived>,
T::Archived: Copy,
{
fn verify(&self, _context: &mut C) -> Result<(), C::Error> {
let native: T = self.value.into();
if native > Int::<T, BITS>::MAX.value || native < Int::<T, BITS>::MIN.value {
rkyv::bytecheck::rancor::fail!(TryNewError);
}
Ok(())
}
}

// Because the methods within this macro are effectively copy-pasted for each underlying integer type,
// each documentation test gets executed five times (once for each underlying type), even though the
// tests themselves aren't specific to said underlying type. This severely slows down `cargo test`,
Expand Down
26 changes: 26 additions & 0 deletions src/unsigned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ impl_integer_native!((u8, i8), (u16, i16), (u32, i32), (u64, i64), (u128, i128))
#[derive(Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "bytecheck", derive(bytecheck::CheckBytes))]
#[cfg_attr(feature = "bytecheck", bytecheck(verify))]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize),
rkyv(bytecheck(verify))
)]
#[repr(transparent)]
pub struct UInt<T: UnsignedInteger + BuiltinInteger, const BITS: usize> {
value: T,
Expand Down Expand Up @@ -181,6 +186,27 @@ where
}
}

#[cfg(feature = "rkyv")]
unsafe impl<
T: UnsignedInteger + BuiltinInteger + rkyv::Archive,
const BITS: usize,
C: rkyv::bytecheck::rancor::Fallible + ?Sized,
> rkyv::bytecheck::Verify<C> for ArchivedUInt<T, BITS>
where
C::Error: rkyv::bytecheck::rancor::Source,
UInt<T, BITS>: Integer,
T: From<T::Archived>,
T::Archived: Copy,
{
fn verify(&self, _context: &mut C) -> Result<(), C::Error> {
let native: T = self.value.into();
if native > UInt::<T, BITS>::MAX.value {
rkyv::bytecheck::rancor::fail!(TryNewError);
}
Ok(())
}
}

// Next are specific implementations for u8, u16, u32, u64 and u128. A couple notes:
// - The existence of MAX also serves as a neat bounds-check for BITS: If BITS is too large,
// the subtraction overflows which will fail to compile. This simplifies things a lot.
Expand Down
72 changes: 72 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3796,6 +3796,78 @@ fn serde_signed() {
assert_de_tokens(&i7::new(15), &[Token::U8(15)]);
assert_de_tokens(&i7::MAX, &[Token::U8(i7::MAX.value() as u8)]);
}
#[cfg(feature = "rkyv")]
mod rkyv {
use arbitrary_int::prelude::*;
use rkyv::rancor::Error as RkyvError;

#[test]
fn unisgned() {
let expected = u7::MAX;
let bytes = rkyv::to_bytes::<RkyvError>(&expected).unwrap();
let actual = rkyv::access::<ArchivedUInt<_, _>, RkyvError>(&bytes[..])
.and_then(rkyv::deserialize)
.unwrap();

assert_eq!(expected, actual);
}

#[test]
fn signed() {
let expected = i7::MAX;
let bytes = rkyv::to_bytes::<RkyvError>(&expected).unwrap();
let actual = rkyv::access::<ArchivedInt<_, _>, RkyvError>(&bytes[..])
.and_then(rkyv::deserialize)
.unwrap();

assert_eq!(expected, actual);
}

#[test]
fn invalid_out_of_range() {
let bytes = rkyv::to_bytes::<RkyvError>(&[123_u8, 123_u8]).unwrap();
rkyv::access::<ArchivedUInt<u16, 9>, RkyvError>(&bytes)
.and_then(rkyv::deserialize)
.unwrap_err();
rkyv::access::<ArchivedInt<i16, 9>, RkyvError>(&bytes)
.and_then(rkyv::deserialize)
.unwrap_err();
}

#[test]
fn multi_byte_roundtrip() {
// Multi-byte values are stored with a fixed endianness in the archive,
// so verification must convert to native before range-checking. On a
// mismatched-endian host (or with rkyv's `big_endian` feature on a
// little-endian one), reinterpreting the archived bytes natively would
// reject valid values like these and accept out-of-range ones.
let expected = u9::new(300);
let bytes = rkyv::to_bytes::<RkyvError>(&expected).unwrap();
let actual = rkyv::access::<ArchivedUInt<u16, 9>, RkyvError>(&bytes[..])
.and_then(rkyv::deserialize)
.unwrap();
assert_eq!(expected, actual);

let expected = i9::new(-200);
let bytes = rkyv::to_bytes::<RkyvError>(&expected).unwrap();
let actual = rkyv::access::<ArchivedInt<i16, 9>, RkyvError>(&bytes[..])
.and_then(rkyv::deserialize)
.unwrap();
assert_eq!(expected, actual);
}

#[test]
fn endianness_shenanigans() {
// Rkyv internally use [rend](https://github.com/rkyv/rend) to deal with cross platform number endianness.
// This make sure that our implementation respect this.
// If it doesn't, compile fail on the derive of rkyv.
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
struct Archivable {
uint: (u1, u9, u17, u33, u65, u127),
int: (i1, i9, i17, i33, i65, i127),
}
}
}

#[cfg(feature = "num-traits")]
mod num_traits {
Expand Down
Loading