Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduces CRCs #98

Merged
merged 4 commits into from
Apr 22, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
Cargo.lock
.vscode
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,21 @@ path = "./postcard-derive"
version = "0.1.1"
optional = true

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

[dependencies.paste]
version = "1.0.12"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some help with our declarative macros.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to make this optional and gate it on the crc feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, although perhaps that adds a little complexity in case we need it elsewhere?

Alternatively, let's get rid of the macros and the need for this. :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 2c9ed55 makes the paste crate optional and includes it when use-crc is enabled.


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

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

# Experimental features!
#
Expand Down
215 changes: 117 additions & 98 deletions src/de/flavors.rs
jamesmunns marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
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, u32>) -> 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, u32>) -> 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
Original file line number Diff line number Diff line change
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;

jamesmunns marked this conversation as resolved.
Show resolved Hide resolved
////////////////////////////////////////////////////////////////////////////////

#[cfg(feature = "heapless")]
Expand Down
15 changes: 15 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
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;

jamesmunns marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(test)]
mod test {
#[test]
Expand Down
Loading