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
Prelude module intended for blanket inclusion to make the crate easier to use.

Macros§

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

Structs§

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

Enums§

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

Traits§

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

Attribute Macros§

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