Skip to content

Commit

Permalink
Add LengthDelimited struct to parse <len, "bytes"> payloads. (Factbir…
Browse files Browse the repository at this point in the history
…dHQ#186)

* Add LengthDelimited struct to parse <len, "bytes"> payloads.

* Also handle non-quoted payloads
  • Loading branch information
ijager authored Dec 7, 2023
1 parent b3d8e95 commit 3af3d58
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 4 deletions.
2 changes: 1 addition & 1 deletion serde_at/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ version = "0.20.0"
[dependencies]
heapless = { version = "^0.8", features = ["serde"] }
serde = { version = "^1", default-features = false }
heapless-bytes = { version = "0.3.0" }

[dependencies.num-traits]
version = "0.2"
default-features = false

[dev-dependencies]
serde_derive = "^1"
heapless-bytes = { version = "0.3.0" }
serde_bytes = { version = "0.11.5", default-features = false }

[features]
Expand Down
83 changes: 83 additions & 0 deletions serde_at/src/de/length_delimited.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! Parsing of length delimited byte strings.
//!
use core::fmt;

use heapless_bytes::Bytes;
use serde::{de, Deserialize, Deserializer};

/// Structure for parsing a length delimited bytes payload.
///
/// This supports both quoted and non-quoted payloads.
/// For "quoted" payloads the length is assumed to be excluding the surrounding double quotes.
///
/// For example:
///
/// For both this response: `+QMTRECV: 1,0,"topic",4,"ABCD"`
/// and this response: `+QTRECV: 1,0,"topic",4,ABCD`
///
/// We can parse the last two parameters as a 'LengthDelimited' object which yields:
/// `'4,"ABCD"' => LengthDelimited { len: 4, bytes: [65, 66, 67, 68] }`
///
#[derive(Clone, Debug)]
pub struct LengthDelimited<const N: usize> {
/// The number of bytes in the payload. This is actually
/// redundant since the `bytes` field also knows its own length.
pub len: usize,
/// The payload bytes
pub bytes: Bytes<N>,
}

impl<'de, const N: usize> Deserialize<'de> for LengthDelimited<N> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// Ideally we use deserializer.deserialize_bytes but since it clips the payload
// at the first comma we cannot use it.
// Instead we use deserialize_tuple as it wasn't used yet.
deserializer.deserialize_tuple(2, LengthDelimitedVisitor::<N>) // The '2' is dummy.
}
}
struct LengthDelimitedVisitor<const N: usize>;

impl<'de, const N: usize> de::Visitor<'de> for LengthDelimitedVisitor<N> {
type Value = LengthDelimited<N>;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("length delimited bytes, e.g.: \"4,ABCD\"")
}

fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
v.iter()
.position(|&c| c == b',')
.ok_or_else(|| de::Error::custom("expected a comma"))
.and_then(|pos| {
let len = parse_len(&v[0..pos])
.map_err(|_| de::Error::custom("expected an unsigned int"))?;
// +1 to skip the comma after the length.
let mut start = pos + 1;
let mut end = start + len;
// Check if payload is surrounded by double quotes not included in len.
let slice_len = v.len();
if slice_len >= (end + 2) && (v[start] == b'"' && v[end + 1] == b'"') {
start += 1; // Extra +1 to remove first quote (")
end += 1; // Move end by 1 to compensate for the quote.
}
Ok(LengthDelimited {
len,
bytes: Bytes::from_slice(&v[start..end])
.map_err(|_| de::Error::custom("incorrect slice size"))?,
})
})
}
}

/// Parses a slice of bytes into an unsigned integer.
/// The slice must contain only ASCII _digits_ and must not contain additional bytes.
fn parse_len(v: &[u8]) -> Result<usize, ()> {
let len_str: &str = core::str::from_utf8(v).map_err(|_| ())?;
len_str.parse().map_err(|_| ())
}
72 changes: 69 additions & 3 deletions serde_at/src/de/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use self::map::MapAccess;
use self::seq::SeqAccess;

mod enum_;
pub mod length_delimited;
mod map;
mod seq;

Expand Down Expand Up @@ -557,12 +558,19 @@ impl<'a, 'de> de::Deserializer<'de> for &'a mut Deserializer<'de> {
visitor.visit_seq(SeqAccess::new(self))
}

/// Unsupported
fn deserialize_tuple<V>(self, _len: usize, _visitor: V) -> Result<V::Value>
/// deserialize_tuple is (mis)used for parsing LengthDelimited types.
/// They can only be used as the last param as we cannot yet communicate the length
/// back to from the visitor to slice the slice.
fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
unreachable!()
visitor
.visit_bytes(self.slice[self.index..].as_ref())
.map(|v| {
self.index = self.slice.len(); // Since we know it is the last param.
v
})
}

/// Unsupported
Expand Down Expand Up @@ -738,6 +746,7 @@ where

#[cfg(test)]
mod tests {
use super::length_delimited::LengthDelimited;
use heapless::String;
use heapless_bytes::Bytes;
use serde_derive::Deserialize;
Expand Down Expand Up @@ -889,4 +898,61 @@ mod tests {
})
);
}

#[test]
fn length_delimited() {
#[derive(Clone, Debug, Deserialize)]
pub struct PayloadResponse {
pub ctx: u8, // Some other params
pub id: i8, // Some other params
pub payload: LengthDelimited<32>,
}

let res: PayloadResponse = crate::from_slice(b"1,-1,9,\"ABCD,1234\"").unwrap();
assert_eq!(res.ctx, 1);
assert_eq!(res.id, -1);
assert_eq!(res.payload.len, 9);
assert_eq!(
res.payload.bytes,
Bytes::<32>::from_slice(b"ABCD,1234").unwrap()
);
}

#[test]
fn length_delimited_no_quotes() {
#[derive(Clone, Debug, Deserialize)]
pub struct PayloadResponse {
pub ctx: u8, // Some other params
pub id: i8, // Some other params
pub payload: LengthDelimited<32>,
}

let res: PayloadResponse = crate::from_slice(b"1,-1,9,ABCD,1234").unwrap();
assert_eq!(res.ctx, 1);
assert_eq!(res.id, -1);
assert_eq!(res.payload.len, 9);
assert_eq!(
res.payload.bytes,
Bytes::<32>::from_slice(b"ABCD,1234").unwrap()
);
}

#[test]
fn length_delimited_json() {
#[derive(Clone, Debug, Deserialize)]
pub struct PayloadResponse {
pub ctx: u8, // Some other params
pub id: i8, // Some other params
pub payload: LengthDelimited<32>,
}
let res: PayloadResponse =
crate::from_slice(b"1,-2,28,\"{\"cmd\": \"blink\", \"pin\": \"2\"}\"").unwrap();
assert_eq!(res.ctx, 1);
assert_eq!(res.id, -2);
assert_eq!(res.payload.len, 28);
assert_eq!(
res.payload.bytes,
Bytes::<32>::from_slice(b"{\"cmd\": \"blink\", \"pin\": \"2\"}").unwrap()
);
}
}

0 comments on commit 3af3d58

Please sign in to comment.