From 34f3f78be638875f8c5777010ea4db918c337098 Mon Sep 17 00:00:00 2001 From: Izaw Date: Sat, 16 May 2026 21:55:31 +0200 Subject: [PATCH 1/4] Add rkyv support (#110) --- .github/workflows/test-rkyv.yml | 15 +++++++++++++++ CHANGELOG.md | 1 + Cargo.toml | 6 ++++++ src/signed.rs | 4 ++++ src/unsigned.rs | 4 ++++ tests/tests.rs | 28 ++++++++++++++++++++++++++++ 6 files changed, 58 insertions(+) create mode 100644 .github/workflows/test-rkyv.yml diff --git a/.github/workflows/test-rkyv.yml b/.github/workflows/test-rkyv.yml new file mode 100644 index 0000000..3265666 --- /dev/null +++ b/.github/workflows/test-rkyv.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 189692a..8e0063f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index d148052..8c65c86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] @@ -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 } 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 } @@ -65,3 +69,5 @@ quickcheck = { version = "1", optional = true, default-features = false } [dev-dependencies] serde_test = "1.0" serde_json = "1.0" +# Need the default `bytecheck` feature to tests without an unsafe block +rkyv = { version = "0.8.17", default-features = true } diff --git a/src/signed.rs b/src/signed.rs index ec7484c..7d15e73 100644 --- a/src/signed.rs +++ b/src/signed.rs @@ -139,6 +139,10 @@ 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) +)] #[repr(transparent)] pub struct Int { value: T, diff --git a/src/unsigned.rs b/src/unsigned.rs index 64579e4..6f35f9c 100644 --- a/src/unsigned.rs +++ b/src/unsigned.rs @@ -127,6 +127,10 @@ 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) +)] #[repr(transparent)] pub struct UInt { value: T, diff --git a/tests/tests.rs b/tests/tests.rs index dd55855..7f5b706 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -3797,6 +3797,34 @@ fn serde_signed() { 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::(&expected).unwrap(); + let actual = rkyv::access::, RkyvError>(&bytes[..]) + .and_then(rkyv::deserialize) + .unwrap(); + + assert_eq!(expected, actual); + } + + #[test] + fn signed() { + let expected = i7::MAX; + let bytes = rkyv::to_bytes::(&expected).unwrap(); + let actual = rkyv::access::, RkyvError>(&bytes[..]) + .and_then(rkyv::deserialize) + .unwrap(); + + assert_eq!(expected, actual); + } +} + #[cfg(feature = "num-traits")] mod num_traits { use arbitrary_int::prelude::*; From 3fe243ce90fbff09b706a81f1d2761dd2e91b65e Mon Sep 17 00:00:00 2001 From: Izaw Date: Sat, 16 May 2026 21:55:31 +0200 Subject: [PATCH 2/4] Fixing value range (1/?) --- src/signed.rs | 9 ++++++++- src/unsigned.rs | 9 ++++++++- tests/tests.rs | 30 ++++++++++++++++++++++-------- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/signed.rs b/src/signed.rs index 7d15e73..29b18da 100644 --- a/src/signed.rs +++ b/src/signed.rs @@ -141,7 +141,14 @@ impl_signed_integer_native!((i8, u8), (i16, u16), (i32, u32), (i64, u64), (i128, #[cfg_attr(feature = "bytecheck", bytecheck(verify))] #[cfg_attr( feature = "rkyv", - derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize) + derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, rkyv::Portable), + rkyv( + archive_bounds( + T: rkyv::Archive + rkyv::Portable, + ::Archived: SignedInteger + BuiltinInteger, + ), + as = Self, + ) )] #[repr(transparent)] pub struct Int { diff --git a/src/unsigned.rs b/src/unsigned.rs index 6f35f9c..5c8b142 100644 --- a/src/unsigned.rs +++ b/src/unsigned.rs @@ -129,7 +129,14 @@ impl_integer_native!((u8, i8), (u16, i16), (u32, i32), (u64, i64), (u128, i128)) #[cfg_attr(feature = "bytecheck", bytecheck(verify))] #[cfg_attr( feature = "rkyv", - derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize) + derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, rkyv::Portable), + rkyv( + archive_bounds( + T: rkyv::Archive + rkyv::Portable, + ::Archived: UnsignedInteger + BuiltinInteger, + ), + as = Self, + ) )] #[repr(transparent)] pub struct UInt { diff --git a/tests/tests.rs b/tests/tests.rs index 7f5b706..3c40c77 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -3796,19 +3796,17 @@ 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; + use bytecheck::{check_bytes, Verify}; + use rkyv::rancor::{Error as RkyvError, Strategy}; #[test] fn unisgned() { let expected = u7::MAX; let bytes = rkyv::to_bytes::(&expected).unwrap(); - let actual = rkyv::access::, RkyvError>(&bytes[..]) - .and_then(rkyv::deserialize) - .unwrap(); + let actual = *rkyv::access::, RkyvError>(&bytes[..]).unwrap(); assert_eq!(expected, actual); } @@ -3817,12 +3815,28 @@ mod rkyv { fn signed() { let expected = i7::MAX; let bytes = rkyv::to_bytes::(&expected).unwrap(); - let actual = rkyv::access::, RkyvError>(&bytes[..]) - .and_then(rkyv::deserialize) - .unwrap(); + let actual = *rkyv::access::, RkyvError>(&bytes[..]).unwrap(); assert_eq!(expected, actual); } + + #[test] + fn invalid_out_of_range() { + let temp = &mut (); + let ctx: &mut Strategy<(), rkyv::rancor::Failure> = Strategy::wrap(temp); + + // This work + let u1 = unsafe { u1::new_unchecked(123) }; + u1.verify(ctx).unwrap_err(); + + // This work + let wrong = 123; + unsafe { check_bytes::((&raw const wrong).cast()).unwrap_err() }; + + // This doesn't. &[123_u8] does work + let bytes = rkyv::to_bytes::(&[123_u8, 0_u8]).unwrap(); + rkyv::access::(&bytes[..]).unwrap_err(); + } } #[cfg(feature = "num-traits")] From 2a361275062e3a5841ca1b836faa42d0055d47ed Mon Sep 17 00:00:00 2001 From: Izaw Date: Sun, 2 Aug 2026 13:18:13 +0200 Subject: [PATCH 3/4] Fixing value range(2/?) --- Cargo.toml | 6 +++--- src/signed.rs | 28 ++++++++++++++++++++-------- src/unsigned.rs | 28 ++++++++++++++++++++-------- tests/tests.rs | 42 +++++++++++++++++++++++++----------------- 4 files changed, 68 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8c65c86..0632a7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +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 } +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 } @@ -69,5 +69,5 @@ quickcheck = { version = "1", optional = true, default-features = false } [dev-dependencies] serde_test = "1.0" serde_json = "1.0" -# Need the default `bytecheck` feature to tests without an unsafe block -rkyv = { version = "0.8.17", default-features = true } +# Need alloc for tests +rkyv = { version = "0.8.17", default-features = false, features = ["alloc"] } diff --git a/src/signed.rs b/src/signed.rs index 29b18da..aeaf65e 100644 --- a/src/signed.rs +++ b/src/signed.rs @@ -141,14 +141,8 @@ impl_signed_integer_native!((i8, u8), (i16, u16), (i32, u32), (i64, u64), (i128, #[cfg_attr(feature = "bytecheck", bytecheck(verify))] #[cfg_attr( feature = "rkyv", - derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, rkyv::Portable), - rkyv( - archive_bounds( - T: rkyv::Archive + rkyv::Portable, - ::Archived: SignedInteger + BuiltinInteger, - ), - as = Self, - ) + derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize), + rkyv(bytecheck(verify)) )] #[repr(transparent)] pub struct Int { @@ -1579,6 +1573,24 @@ where } } +#[cfg(feature = "rkyv")] +unsafe impl< + T: SignedInteger + BuiltinInteger + rkyv::Archive, + const BITS: usize, + C: rkyv::bytecheck::rancor::Fallible + ?Sized, + > rkyv::bytecheck::Verify for ArchivedInt +where + C::Error: rkyv::bytecheck::rancor::Source, + Int: Integer, +{ + fn verify(&self, context: &mut C) -> Result<(), C::Error> { + let ptr: *const Int = (&raw const self.value).cast(); + // SAFETY: `in_subtree` was guaranteed called by [rkyv::api::checked::check_pos_with_context]. + // Unsafe could later be replaced if rend add a trait to generically convert from <-> into native. + unsafe { (*ptr).verify(context) } + } +} + // 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`, diff --git a/src/unsigned.rs b/src/unsigned.rs index 5c8b142..4e75f53 100644 --- a/src/unsigned.rs +++ b/src/unsigned.rs @@ -129,14 +129,8 @@ impl_integer_native!((u8, i8), (u16, i16), (u32, i32), (u64, i64), (u128, i128)) #[cfg_attr(feature = "bytecheck", bytecheck(verify))] #[cfg_attr( feature = "rkyv", - derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, rkyv::Portable), - rkyv( - archive_bounds( - T: rkyv::Archive + rkyv::Portable, - ::Archived: UnsignedInteger + BuiltinInteger, - ), - as = Self, - ) + derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize), + rkyv(bytecheck(verify)) )] #[repr(transparent)] pub struct UInt { @@ -192,6 +186,24 @@ where } } +#[cfg(feature = "rkyv")] +unsafe impl< + T: UnsignedInteger + BuiltinInteger + rkyv::Archive, + const BITS: usize, + C: rkyv::bytecheck::rancor::Fallible + ?Sized, + > rkyv::bytecheck::Verify for ArchivedUInt +where + C::Error: rkyv::bytecheck::rancor::Source, + UInt: Integer, +{ + fn verify(&self, context: &mut C) -> Result<(), C::Error> { + let ptr: *const UInt = (&raw const self.value).cast(); + // SAFETY: `in_subtree` was guaranteed called by [rkyv::api::checked::check_pos_with_context]. + // Unsafe could later be replaced if rend add a trait to generically convert from <-> into native. + unsafe { (*ptr).verify(context) } + } +} + // 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. diff --git a/tests/tests.rs b/tests/tests.rs index 3c40c77..166ff3f 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -3799,14 +3799,15 @@ fn serde_signed() { #[cfg(feature = "rkyv")] mod rkyv { use arbitrary_int::prelude::*; - use bytecheck::{check_bytes, Verify}; - use rkyv::rancor::{Error as RkyvError, Strategy}; + use rkyv::rancor::Error as RkyvError; #[test] fn unisgned() { let expected = u7::MAX; let bytes = rkyv::to_bytes::(&expected).unwrap(); - let actual = *rkyv::access::, RkyvError>(&bytes[..]).unwrap(); + let actual = rkyv::access::, RkyvError>(&bytes[..]) + .and_then(rkyv::deserialize) + .unwrap(); assert_eq!(expected, actual); } @@ -3815,27 +3816,34 @@ mod rkyv { fn signed() { let expected = i7::MAX; let bytes = rkyv::to_bytes::(&expected).unwrap(); - let actual = *rkyv::access::, RkyvError>(&bytes[..]).unwrap(); + let actual = rkyv::access::, RkyvError>(&bytes[..]) + .and_then(rkyv::deserialize) + .unwrap(); assert_eq!(expected, actual); } #[test] fn invalid_out_of_range() { - let temp = &mut (); - let ctx: &mut Strategy<(), rkyv::rancor::Failure> = Strategy::wrap(temp); - - // This work - let u1 = unsafe { u1::new_unchecked(123) }; - u1.verify(ctx).unwrap_err(); - - // This work - let wrong = 123; - unsafe { check_bytes::((&raw const wrong).cast()).unwrap_err() }; + let bytes = rkyv::to_bytes::(&[123_u8, 123_u8]).unwrap(); + rkyv::access::, RkyvError>(&bytes) + .and_then(rkyv::deserialize) + .unwrap_err(); + rkyv::access::, RkyvError>(&bytes) + .and_then(rkyv::deserialize) + .unwrap_err(); + } - // This doesn't. &[123_u8] does work - let bytes = rkyv::to_bytes::(&[123_u8, 0_u8]).unwrap(); - rkyv::access::(&bytes[..]).unwrap_err(); + #[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), + } } } From e4e7fd560c65b405682b2c71c162d6d03a206554 Mon Sep 17 00:00:00 2001 From: Daniel Lehmann Date: Mon, 3 Aug 2026 14:26:20 -0700 Subject: [PATCH 4/4] Fix endianness bug in rkyv verification and remove unsafe The Verify impls cast the archived value (e.g. rend's u16_le) to the native UInt/Int and range-checked the byte-swapped value on mismatched-endian hosts: valid archives were rejected and crafted out-of-range archives could pass verification. rend already provides From impls to native, so convert via T: From instead of the unsafe cast, and add a multi-byte round-trip test. --- src/signed.rs | 13 ++++++++----- src/unsigned.rs | 13 ++++++++----- tests/tests.rs | 22 ++++++++++++++++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/signed.rs b/src/signed.rs index aeaf65e..b4ca3f1 100644 --- a/src/signed.rs +++ b/src/signed.rs @@ -1582,12 +1582,15 @@ unsafe impl< where C::Error: rkyv::bytecheck::rancor::Source, Int: Integer, + T: From, + T::Archived: Copy, { - fn verify(&self, context: &mut C) -> Result<(), C::Error> { - let ptr: *const Int = (&raw const self.value).cast(); - // SAFETY: `in_subtree` was guaranteed called by [rkyv::api::checked::check_pos_with_context]. - // Unsafe could later be replaced if rend add a trait to generically convert from <-> into native. - unsafe { (*ptr).verify(context) } + fn verify(&self, _context: &mut C) -> Result<(), C::Error> { + let native: T = self.value.into(); + if native > Int::::MAX.value || native < Int::::MIN.value { + rkyv::bytecheck::rancor::fail!(TryNewError); + } + Ok(()) } } diff --git a/src/unsigned.rs b/src/unsigned.rs index 4e75f53..209f7c3 100644 --- a/src/unsigned.rs +++ b/src/unsigned.rs @@ -195,12 +195,15 @@ unsafe impl< where C::Error: rkyv::bytecheck::rancor::Source, UInt: Integer, + T: From, + T::Archived: Copy, { - fn verify(&self, context: &mut C) -> Result<(), C::Error> { - let ptr: *const UInt = (&raw const self.value).cast(); - // SAFETY: `in_subtree` was guaranteed called by [rkyv::api::checked::check_pos_with_context]. - // Unsafe could later be replaced if rend add a trait to generically convert from <-> into native. - unsafe { (*ptr).verify(context) } + fn verify(&self, _context: &mut C) -> Result<(), C::Error> { + let native: T = self.value.into(); + if native > UInt::::MAX.value { + rkyv::bytecheck::rancor::fail!(TryNewError); + } + Ok(()) } } diff --git a/tests/tests.rs b/tests/tests.rs index 166ff3f..4530561 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -3834,6 +3834,28 @@ mod rkyv { .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::(&expected).unwrap(); + let actual = rkyv::access::, RkyvError>(&bytes[..]) + .and_then(rkyv::deserialize) + .unwrap(); + assert_eq!(expected, actual); + + let expected = i9::new(-200); + let bytes = rkyv::to_bytes::(&expected).unwrap(); + let actual = rkyv::access::, 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.