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..0632a7a 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, 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 } @@ -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"] } diff --git a/src/signed.rs b/src/signed.rs index ec7484c..b4ca3f1 100644 --- a/src/signed.rs +++ b/src/signed.rs @@ -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)) +)] #[repr(transparent)] pub struct Int { value: T, @@ -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 for ArchivedInt +where + C::Error: rkyv::bytecheck::rancor::Source, + Int: Integer, + T: From, + T::Archived: Copy, +{ + 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(()) + } +} + // 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 64579e4..209f7c3 100644 --- a/src/unsigned.rs +++ b/src/unsigned.rs @@ -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 { value: T, @@ -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 for ArchivedUInt +where + C::Error: rkyv::bytecheck::rancor::Source, + UInt: Integer, + T: From, + T::Archived: Copy, +{ + 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(()) + } +} + // 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 dd55855..4530561 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -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::(&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); + } + + #[test] + fn invalid_out_of_range() { + 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(); + } + + #[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. + // 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 {