Skip to content

Commit

Permalink
Introduces CRCs (#98)
Browse files Browse the repository at this point in the history
* Introduces CRCs

A `crc` feature has been added that brings in the `crc` crate and a new `CrcModifier` flavour. This modifier flavour is similar to the COBS implementation in how it wraps another flavour.

* Convenience functions

Convenience functions for serde usage

* Optionally include paste

The paste crate is included now only if `use-crc` feature is enabled. This may help improve build performance where CRCs are not required.

* Fix use of paste macro

This uses the correct digest sizes, and adds a test for
a random CRC8 variant.

---------

Co-authored-by: James Munns <james@onevariable.com>
  • Loading branch information
huntc and jamesmunns committed Apr 22, 2023
1 parent 978b561 commit a0ef8a2
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 103 deletions.
3 changes: 2 additions & 1 deletion .gitignore
@@ -1,3 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
Cargo.lock
.vscode
9 changes: 9 additions & 0 deletions Cargo.toml
Expand Up @@ -52,13 +52,22 @@ path = "./postcard-derive"
version = "0.1.1"
optional = true

[dependencies.crc]
version = "3.0.1"
optional = true

[dependencies.paste]
version = "1.0.12"
optional = true

[features]
default = ["heapless-cas"]

use-std = ["serde/std", "alloc"]
heapless-cas = ["heapless", "heapless/cas"]
alloc = ["serde/alloc"]
use-defmt = ["defmt"]
use-crc = ["crc", "paste"]

# Experimental features!
#
Expand Down
215 changes: 117 additions & 98 deletions src/de/flavors.rs
Expand Up @@ -163,120 +163,139 @@ impl<'de> Flavor<'de> for Slice<'de> {
}
}

// This is a terrible checksum implementation to make sure that we can effectively
// use the deserialization flavor. This is kept as a test (and not published)
// because an 8-bit checksum is not ACTUALLY useful for almost anything.
//
// You could certainly do something similar with a CRC32, cryptographic sig,
// or something else
#[cfg(test)]
mod test {
use super::*;
use serde::{Deserialize, Serialize};
////////////////////////////////////////
// CRC
////////////////////////////////////////

struct Checksum<'de, F>
where
F: Flavor<'de> + 'de,
{
flav: F,
checksum: u8,
_plt: PhantomData<&'de ()>,
}
/// This Cyclic Redundancy Check flavor applies [the CRC crate's `Algorithm`](https://docs.rs/crc/latest/crc/struct.Algorithm.html) struct on
/// the serialized data. The flavor will check the CRC assuming that it has been appended to the bytes.
///
/// CRCs are used for error detection when reading data back.
///
/// The `crc` feature requires enabling to use this module.
///
/// More on CRCs: <https://en.wikipedia.org/wiki/Cyclic_redundancy_check>.
#[cfg(feature = "use-crc")]
pub mod crc {
use core::convert::TryInto;

use crc::Digest;
use crc::Width;
use serde::Deserialize;

impl<'de, F> Checksum<'de, F>
use super::Flavor;
use super::Slice;

use crate::Deserializer;
use crate::Error;
use crate::Result;
use paste::paste;

/// Manages CRC modifications as a flavor.
pub struct CrcModifier<'de, B, W>
where
F: Flavor<'de> + 'de,
B: Flavor<'de>,
W: Width,
{
pub fn from_flav(flav: F) -> Self {
Self {
flav,
checksum: 0,
_plt: PhantomData,
}
}
flav: B,
digest: Digest<'de, W>,
}

impl<'de, F> Flavor<'de> for Checksum<'de, F>
impl<'de, B, W> CrcModifier<'de, B, W>
where
F: Flavor<'de> + 'de,
B: Flavor<'de>,
W: Width,
{
type Remainder = (<F as Flavor<'de>>::Remainder, u8);
type Source = F;

fn pop(&mut self) -> Result<u8> {
match self.flav.pop() {
Ok(u) => {
self.checksum = self.checksum.wrapping_add(u);
Ok(u)
}
Err(e) => Err(e),
}
}
fn try_take_n(&mut self, ct: usize) -> Result<&'de [u8]> {
match self.flav.try_take_n(ct) {
Ok(u) => {
u.iter().for_each(|u| {
self.checksum = self.checksum.wrapping_add(*u);
});
Ok(u)
}
Err(e) => Err(e),
}
}
fn finalize(self) -> Result<Self::Remainder> {
Ok((self.flav.finalize()?, self.checksum))
/// Create a new Crc modifier Flavor.
pub fn new(bee: B, digest: Digest<'de, W>) -> Self {
Self { flav: bee, digest }
}
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct SomeData<'a> {
#[serde(borrow)]
sli: &'a [u8],
sts: &'a str,
foo: u64,
bar: u128,
}
macro_rules! impl_flavor {
($( $int:ty ),*) => {
$(
paste! {
impl<'de, B> Flavor<'de> for CrcModifier<'de, B, $int>
where
B: Flavor<'de>,
{
type Remainder = B::Remainder;

#[test]
fn smoke() {
const EXPECTED: &[u8] = &[
4, 255, 1, 34, 51, 19, 116, 104, 105, 115, 32, 105, 115, 32, 97, 32, 103, 111, 111,
100, 32, 116, 101, 115, 116, 170, 213, 170, 213, 170, 213, 170, 213, 170, 1, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127,
];
type Source = B::Source;

// Calculate simple 8-bit checksum
let mut check: u8 = 0;
EXPECTED.iter().for_each(|u| check = check.wrapping_add(*u));
#[inline]
fn pop(&mut self) -> Result<u8> {
match self.flav.pop() {
Ok(byte) => {
self.digest.update(&[byte]);
Ok(byte)
}
e @ Err(_) => e,
}
}

let mut buf = [0u8; 256];
let data = SomeData {
sli: &[0xFF, 0x01, 0x22, 0x33],
sts: "this is a good test",
foo: (u64::MAX / 3) * 2,
bar: u128::MAX / 4,
};
let used = crate::to_slice(&data, &mut buf).unwrap();
assert_eq!(used, EXPECTED);
let used = used.len();
#[inline]
fn try_take_n(&mut self, ct: usize) -> Result<&'de [u8]> {
match self.flav.try_take_n(ct) {
Ok(bytes) => {
self.digest.update(bytes);
Ok(bytes)
}
e @ Err(_) => e,
}
}

// Put the checksum at the end
buf[used] = check;
fn finalize(mut self) -> Result<Self::Remainder> {
match self.flav.try_take_n(core::mem::size_of::<$int>()) {
Ok(prev_crc_bytes) => match self.flav.finalize() {
Ok(remainder) => {
let crc = self.digest.finalize();
let le_bytes = prev_crc_bytes
.try_into()
.map_err(|_| Error::DeserializeBadEncoding)?;
let prev_crc = <$int>::from_le_bytes(le_bytes);
if crc == prev_crc {
Ok(remainder)
} else {
Err(Error::DeserializeBadEncoding)
}
}
e @ Err(_) => e,
},
Err(e) => Err(e),
}
}
}

let mut deser = crate::de::Deserializer::from_flavor(Checksum::from_flav(Slice::new(&buf)));
/// Deserialize a message of type `T` from a byte slice with a Crc. The unused portion (if any)
/// of the byte slice is not returned.
pub fn [<from_bytes_ $int>]<'a, T>(s: &'a [u8], digest: Digest<'a, $int>) -> Result<T>
where
T: Deserialize<'a>,
{
let flav = CrcModifier::new(Slice::new(s), digest);
let mut deserializer = Deserializer::from_flavor(flav);
let r = T::deserialize(&mut deserializer)?;
let _ = deserializer.finalize()?;
Ok(r)
}

let t = SomeData::<'_>::deserialize(&mut deser).unwrap();
assert_eq!(t, data);

// Normally, you'd probably expect the check
let (rem, cksm) = deser.finalize().unwrap();

// The pre-calculated checksum we stuffed at the end is the
// first "unused" byte
assert_eq!(rem[0], check);

// the one we calculated during serialization matches the
// pre-calculated one
assert_eq!(cksm, check);
/// Deserialize a message of type `T` from a byte slice with a Crc. The unused portion (if any)
/// of the byte slice is returned for further usage
pub fn [<take_from_bytes_ $int>]<'a, T>(s: &'a [u8], digest: Digest<'a, $int>) -> Result<(T, &'a [u8])>
where
T: Deserialize<'a>,
{
let flav = CrcModifier::new(Slice::new(s), digest);
let mut deserializer = Deserializer::from_flavor(flav);
let t = T::deserialize(&mut deserializer)?;
Ok((t, deserializer.finalize()?))
}
}
)*
};
}

impl_flavor![u8, u16, u32, u64, u128];
}
20 changes: 18 additions & 2 deletions src/de/mod.rs
Expand Up @@ -20,7 +20,7 @@ where

/// Deserialize a message of type `T` from a cobs-encoded byte slice. The
/// unused portion (if any) of the byte slice is not returned.
/// The used portion of the input slice is modified during deserialization (even if an error is returned).
/// The used portion of the input slice is modified during deserialization (even if an error is returned).
/// Therefore, if this is not desired, pass a clone of the original slice.
pub fn from_bytes_cobs<'a, T>(s: &'a mut [u8]) -> Result<T>
where
Expand All @@ -32,7 +32,7 @@ where

/// Deserialize a message of type `T` from a cobs-encoded byte slice. The
/// unused portion (if any) of the byte slice is returned for further usage.
/// The used portion of the input slice is modified during deserialization (even if an error is returned).
/// The used portion of the input slice is modified during deserialization (even if an error is returned).
/// Therefore, if this is not desired, pass a clone of the original slice.
pub fn take_from_bytes_cobs<'a, T>(s: &'a mut [u8]) -> Result<(T, &'a mut [u8])>
where
Expand Down Expand Up @@ -67,6 +67,22 @@ where
Ok((t, deserializer.finalize()?))
}

/// Conveniently deserialize a message of type `T` from a byte slice with a Crc. The unused portion (if any)
/// of the byte slice is not returned.
///
/// See the `de_flavors::crc` module for the complete set of functions.
///
#[cfg(feature = "use-crc")]
pub use flavors::crc::from_bytes_u32 as from_bytes_crc32;

/// Conveniently deserialize a message of type `T` from a byte slice with a Crc. The unused portion (if any)
/// of the byte slice is returned for further usage
///
/// See the `de_flavors::crc` module for the complete set of functions.
///
#[cfg(feature = "use-crc")]
pub use flavors::crc::take_from_bytes_u32 as take_from_bytes_crc32;

////////////////////////////////////////////////////////////////////////////////

#[cfg(feature = "heapless")]
Expand Down
15 changes: 15 additions & 0 deletions src/lib.rs
Expand Up @@ -91,6 +91,21 @@ pub use ser::{to_stdvec, to_stdvec_cobs};
#[cfg(feature = "alloc")]
pub use ser::{to_allocvec, to_allocvec_cobs};

#[cfg(feature = "use-crc")]
pub use {
de::{from_bytes_crc32, take_from_bytes_crc32},
ser::to_slice_crc32,
};

#[cfg(all(feature = "use-crc", feature = "heapless"))]
pub use ser::to_vec_crc32;

#[cfg(all(feature = "use-crc", feature = "use-std"))]
pub use ser::to_stdvec_crc32;

#[cfg(all(feature = "use-crc", feature = "alloc"))]
pub use ser::to_allocvec_crc32;

#[cfg(test)]
mod test {
#[test]
Expand Down

0 comments on commit a0ef8a2

Please sign in to comment.