Expand description
§Derive macros for traits in tls_codec
Derive macros can be used to automatically implement the
Serialize
,
SerializeBytes
,
Deserialize
,
DeserializeBytes
, and
Size
traits for structs and enums. Note that the
functions of the Serialize
and
Deserialize
traits (and thus the
corresponding derive macros) require the "std"
feature to work.
§Warning
The derive macros support deriving the tls_codec
traits for enumerations
and the resulting serialized format complies with the “variants” section of
the TLS RFC.
However support is limited to enumerations that are serialized with their
discriminant immediately followed by the variant data. If this is not
appropriate (e.g. the format requires other fields between the discriminant
and variant data), the tls_codec
traits can be implemented manually.
§Parsing unknown values
In many cases it is necessary to deserialize structs with unknown values,
e.g. when receiving unknown TLS extensions. In this case the deserialize
function returns an Error::UnknownValue
with a u64
value of the unknown
type.
use tls_codec_derive::{TlsDeserialize, TlsSerialize, TlsSize};
#[derive(TlsDeserialize, TlsSerialize, TlsSize)]
#[repr(u16)]
enum TypeWithUnknowns {
First = 1,
Second = 2,
}
#[test]
fn type_with_unknowns() {
let incoming = [0x00u8, 0x03]; // This must be parsed into TypeWithUnknowns into an unknown
let deserialized = TypeWithUnknowns::tls_deserialize_exact(incoming);
assert!(matches!(deserialized, Err(Error::UnknownValue(3))));
}
§Available attributes
Attributes can be used to control serialization and deserialization on a per-field basis.
§with
#[tls_codec(with = "prefix")]
This attribute may be applied to a struct field. It indicates that deriving
any of the tls_codec
traits for the containing struct calls the following
functions:
prefix::tls_deserialize
when derivingDeserialize
prefix::tls_serialize
when derivingSerialize
prefix::tls_serialized_len
when derivingSize
prefix
can be a path to a module, type or trait where the functions are
defined.
Their expected signatures match the corresponding methods in the traits.
use tls_codec_derive::{TlsSerialize, TlsSize};
#[derive(TlsSerialize, TlsSize)]
struct Bytes {
#[tls_codec(with = "bytes")]
values: Vec<u8>,
}
mod bytes {
use std::io::Write;
use tls_codec::{Serialize, Size, TlsByteSliceU32};
pub fn tls_serialized_len(v: &[u8]) -> usize {
TlsByteSliceU32(v).tls_serialized_len()
}
pub fn tls_serialize<W: Write>(v: &[u8], writer: &mut W) -> Result<usize, tls_codec::Error> {
TlsByteSliceU32(v).tls_serialize(writer)
}
}
§discriminant
#[tls_codec(discriminant = 123)]
#[tls_codec(discriminant = "path::to::const::or::enum::Variant")]
This attribute may be applied to an enum variant to specify the discriminant
to use when serializing it. If all variants are units (e.g. they do not have
any data), this attribute must not be used and the desired discriminants
should be assigned to the variants using standard Rust syntax (Variant = Discriminant
).
For enumerations with non-unit variants, if no variant has this attribute, the serialization discriminants will start from zero. If this attribute is used on a variant and the following variant does not have it, its discriminant will be equal to the previous variant discriminant plus 1. This behavior is referred to as “implicit discriminants”.
You can also provide paths that lead to const
definitions or enum
Variants. The important thing is that any of those path expressions must
resolve to something that can be coerced to the #[repr(enum_repr)]
of the
enum. Please note that there are checks performed at compile time to check
if the provided value fits within the bounds of the enum_repr
to avoid
misuse.
Note: When using paths once in your enum discriminants, as we do not have enough information to deduce the next implicit discriminant (the constant expressions those paths resolve is only evaluated at a later compilation stage than macros), you will be forced to use explicit discriminants for all the other Variants of your enum.
use tls_codec_derive::{TlsSerialize, TlsSize};
const CONST_DISCRIMINANT: u8 = 5;
#[repr(u8)]
enum TokenType {
Constant = 3,
Variant = 4,
}
#[derive(TlsSerialize, TlsSize)]
#[repr(u8)]
enum TokenImplicit {
#[tls_codec(discriminant = 5)]
Int(u32),
// This will have the discriminant 6 as it's implicitly determined
Bytes([u8; 16]),
}
#[derive(TlsSerialize, TlsSize)]
#[repr(u8)]
enum TokenExplicit {
#[tls_codec(discriminant = "TokenType::Constant")]
Constant(u32),
#[tls_codec(discriminant = "TokenType::Variant")]
Variant(Vec<u8>),
#[tls_codec(discriminant = "CONST_DISCRIMINANT")]
StaticConstant(u8),
}
§skip
#[tls_codec(skip)]
This attribute may be applied to a struct field to specify that it should be
skipped. Skipping means that the field at hand will neither be serialized
into TLS bytes nor deserialized from TLS bytes. For deserialization, it is
required to populate the field with a known value. Thus, when skip
is
used, the field type needs to implement the Default trait so it can be
populated with a default value.
use tls_codec_derive::{TlsSerialize, TlsDeserialize, TlsSize};
struct CustomStruct;
impl Default for CustomStruct {
fn default() -> Self {
CustomStruct {}
}
}
#[derive(TlsSerialize, TlsDeserialize, TlsSize)]
struct StructWithSkip {
a: u8,
#[tls_codec(skip)]
b: CustomStruct,
c: u8,
}
§Conditional deserialization via the conditionally_deserializable
attribute macro
In some cases, it can be useful to have two variants of a struct, where one is deserializable and one isn’t. For example, the deserializable variant of the struct could represent an unverified message, where only verification produces the verified variant. Further processing could then be restricted to the undeserializable struct variant.
A pattern like this can be created via the conditionally_deserializable
attribute macro (requires the conditional_deserialization
feature flag).
The macro adds a boolean const generic to the struct and creates two
aliases, one for the deserializable variant (with a “Deserializable
”
prefix) and one for the undeserializable one (with an “Undeserializable
”
prefix).
use tls_codec::{Serialize, Deserialize};
use tls_codec_derive::{TlsSerialize, TlsSize, conditionally_deserializable};
#[conditionally_deserializable]
#[derive(TlsSize, TlsSerialize, PartialEq, Debug)]
struct ExampleStruct {
a: u8,
b: u16,
}
let undeserializable_struct = UndeserializableExampleStruct { a: 1, b: 2 };
let serialized = undeserializable_struct.tls_serialize_detached().unwrap();
let deserializable_struct =
DeserializableExampleStruct::tls_deserialize(&mut serialized.as_slice()).unwrap();
The helper macro #[tls_codec(cd_field)]
can be used to mark a field as
conditionally deserializable, thus allowing nested conditionally
deserializable structs.
use tls_codec::{Serialize, Deserialize};
use tls_codec_derive::{TlsSerialize, TlsSize, conditionally_deserializable};
#[conditionally_deserializable]
#[derive(TlsSize, TlsSerialize, PartialEq, Debug)]
struct ExampleStruct {
a: u8,
b: u16,
}
#[conditionally_deserializable]
#[derive(TlsSize, TlsSerialize, PartialEq, Debug)]
struct NestedExampleStruct {
#[tls_codec(cd_field)]
example_struct: ExampleStruct,
}