Crate spinel_pack

source ·
Expand description

This Rust crate allows you to easily and safely serialize/deserialize data structures into the byte format specified in section 3 of the Spinel Protocol Documentation. This documentation assumes some familiarity with that documentation. The behavior of this crate was inspired by the spinel_datatype_pack/spinel_datatype_unpack methods from OpenThread.

Key Features

  • Ability to deserialize to borrowed types (like &str) in addition to owned types.
  • Convenient attribute macro (#[spinel_packed("...")]) for automatically implementing pack/unpack traits on datatypes, including structs with borrowed references and nested Spinel data types.
  • Convenient in-line macros (spinel_read!(...)/spinel_write!(...)) for parsing arbitrary Spinel data immediately in place without the need to define new types.
  • All macros check the format strings against the types being serialized/deserialized at compile-time, generating compiler errors whenever there is a mismatch.

Packing/Serialization

Serialization is performed via two traits:

  • TryPack for serializing to the natural Spinel byte format for the given type. The key method on the trait is try_pack().
  • TryPackAs<MarkerType> for serializing to one of the specific Spinel format types. Since this trait is generic, a given type can have multiple implementations of this trait for serializing to the byte format specified by MarkerType. The key method on the trait is try_pack_as().

The TryPack/TryPackAs traits operate on types that implement std::io::Write, which includes Vec<u8> and &mut&mut[u8].

Unpacking/Deserialization

Deserialization is performed via three traits:

  • TryUnpack for deserializing bytes into an instance of the associated type Self::Unpacked (which is usually Self), inferring the byte format from the type. This trait supports deserializing to both borrowed types (like &str) and owned types (like String), and thus has a lifetime parameter. The key method on the trait is try_unpack().
  • TryUnpackAs<MarkerType> for deserializing a spinel field that is encoded as a MarkerType into an instance of the associated type Self::Unpacked (which is usually Self). This trait supports deserializing to both borrowed types (like &str) and owned types (like String), and thus has a lifetime parameter. The key method on the trait is try_unpack_as().
  • TryOwnedUnpack, which is similar to TryUnpack except that it supports owned types only (like String). The key method on the trait is try_owned_unpack().

The TryUnpack/TryOwnedUnpack traits operate on byte slices (&[u8]) and byte slice iterators (std::slice::Iter<u8>).

Supported Primitive Types

This crate supports the following raw Spinel types:

CharNameTypeMarker Type (if different)
.VOID()
bBOOLbool
CUINT8u8
cINT8i8
SUINT16u16
sINT16i16
LUINT32u32
lINT32i32
iUINT_PACKEDu32SpinelUint
6IPv6ADDRstd::net::Ipv6Addr
EEUI64EUI64
eEUI48EUI48
DDATA&[u8]/Vec<u8>[u8]
dDATA_WLEN&[u8]/Vec<u8>SpinelDataWlen
UUTF8&str/Stringstr

The Spinel struct (t(...)) and array (A(...)) types are not directly supported and will result in a compiler error if used in a Spinel format string. However, you can emulate the behavior of spinel structs by replacing the t(...) with a d and using another Spinel datatype (like one created with the spinel_packed attribute) instead of &[u8] or Vec<u8>.

How Format Strings Work

The macro spinel_write!(...) can be used to directly unpack Spinel-encoded data into individual fields, with the encoded type being defined by a format string. This macro will parse the format string, associating each character in the string with a specific Marker Type and field argument. For each of the fields in the format, the macro will make a call into TryPackAs<MarkerType>::try_pack_as(...) for the given field’s argument. If there is no implementation of TryPackAs<MarkerType> for the type of the field’s argument, a compile-time error is generated.

Examples

Struct example:

use spinel_pack::prelude::*;

#[spinel_packed("CiiLUE")]
#[derive(Debug, Eq, PartialEq)]
pub struct SomePackedData<'a> {
    foo: u8,
    bar: u32,
    blah: u32,
    bleh: u32,
    name: &'a str,
    addr: spinel_pack::EUI64,
}

Packing into a new Vec:

let data = SomePackedData {
    foo: 10,
    bar: 20,
    blah: 30,
    bleh: 40,
    name: "This is a string",
    addr: spinel_pack::EUI64([0,0,0,0,0,0,0,0]),
};

let packed: Vec<u8> = data.try_packed()?;

Packing into an existing array:

let data = SomePackedData {
    foo: 10,
    bar: 20,
    blah: 30,
    bleh: 40,
    name: "This is a string",
    addr: spinel_pack::EUI64([0,0,0,0,0,0,0,0]),
};

let mut bytes = [0u8; 500];
let length = data.try_pack(&mut &mut bytes[..])?;

Unpacking:

let bytes: &[u8] = &[0x01, 0x02, 0x03, 0xef, 0xbe, 0xad, 0xde, 0x31, 0x32, 0x33, 0x00, 0x02,
                     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];

let data = SomePackedData::try_unpack(&mut bytes.iter())?;

assert_eq!(data.foo, 1);
assert_eq!(data.bar, 2);
assert_eq!(data.blah, 3);
assert_eq!(data.bleh, 0xdeadbeef);
assert_eq!(data.name, "123");
assert_eq!(data.addr, spinel_pack::EUI64([0x02,0xff,0xff,0xff,0xff,0xff,0xff,0xff]));

Spinel packed structs can be nested:

#[spinel_packed("idU")]
#[derive(Debug, Eq, PartialEq)]
pub struct SomeNestedData<'a> {
    foo: u32,
    test_struct_1: SomePackedData<'a>,
    name: String,
}

Each field of a struct must have an associated format type indicator:

#[spinel_packed("i")]
#[derive(Debug, Eq, PartialEq)]
pub struct SomePackedData {
    foo: u32,
    data: &'a [u8], // Compiler error, no format type
}

Likewise, each format type indicator must have a field:

#[spinel_packed("id")]
#[derive(Debug, Eq, PartialEq)]
pub struct SomePackedData {
    foo: u32,
} // Compiler error, missing field for 'd'
#use spinel_pack::prelude::*;
#[spinel_packed("id")]
#[derive(Debug, Eq, PartialEq)]
pub struct SomePackedData; // Compiler error, no fields at all

Using spinel_write!():

let mut target: Vec<u8> = vec![];

spinel_write!(&mut target, "isc", 1, 2, 3)
    .expect("spinel_write failed");

assert_eq!(target, vec![1u8,2u8,0u8,3u8]);

Using spinel_read!():

// The data to parse.
let bytes: &[u8] = &[
    0x01, 0x02, 0x83, 0x03, 0xef, 0xbe, 0xad, 0xde,
    0x31, 0x32, 0x33, 0x00, 0x02, 0xf1, 0xf2, 0xf3,
    0xf4, 0xf5, 0xf6, 0xf7,
];

// The variables that we will place
// the parsed values into. Note that
// currently these need to be initialized
// before they can be used with `spinel_read`.
let mut foo: u8 = 0;
let mut bar: u32 = 0;
let mut blah: u32 = 0;
let mut bleh: u32 = 0;
let mut name: String = Default::default();
let mut addr: spinel_pack::EUI64 = Default::default();

// Parse the data.
spinel_read!(&mut bytes.iter(), "CiiLUE", foo, bar, blah, bleh, name, addr)
    .expect("spinel_read failed");

// Verify that the variables match
// the values we expect.
assert_eq!(foo, 1);
assert_eq!(bar, 2);
assert_eq!(blah, 387);
assert_eq!(bleh, 0xdeadbeef);
assert_eq!(name, "123");
assert_eq!(addr, spinel_pack::EUI64([0x02, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7]));

Modules

Prelude module intended for blanket inclusion to make the crate easier to use.

Macros

Provides an automatic implementation of TryUnpack when wrapped around an implementation of TryOwnedUnpack.
In-line proc macro for parsing spinel-formatted data fields from a byte slice iterator.
In-line proc macro for writing spinel-formatted data fields to a type implementing std::io::Write.
In-line proc macro for determining the written length of spinel-formatted data fields.

Structs

Data type representing a EUI48 address.
Data type representing a EUI64 address.

Enums

Marker type used to specify data fields that are prepended with its length.
Marker type used to specify integers encoded with Spinel’s variable-length unsigned integer encoding.
Error type for unpacking operations.

Traits

Marker trait for types which always serialize to the same length.
Trait for unpacking only into owned types, like Vec<u8> or String.
Trait implemented by data types that support being serialized to a spinel-based byte encoding.
Trait implemented by data types that support being serialized to a specific spinel-based byte encoding, based on the marker type.
Trait for unpacking a spinel-encoded buffer to a specific type.
Trait for unpacking a spinel-encoded buffer to a specific type when the field type is known.

Attribute Macros

Attribute macro which takes a Spinel format string as an argument and automatically defines the TryPack/TryUnpack traits for the given struct.