use std::collections::BTreeMap;
use cbor2::{Cbor, Value};
#[derive(Debug, PartialEq, Cbor)]
struct CoseKey {
#[cbor(key = 1)]
kty: u8,
#[cbor(key = 3)]
alg: i8,
#[cbor(key = -1)]
crv: u8,
#[cbor(key = -2)]
x: serde_bytes::ByteBuf,
note: Option<String>,
}
fn sample() -> CoseKey {
CoseKey {
kty: 2, alg: -7, crv: 1, x: serde_bytes::ByteBuf::from(vec![0x11, 0x22, 0x33, 0x44]),
note: None,
}
}
#[test]
fn cose_key_round_trip() {
let bytes = cbor2::to_vec(&sample()).unwrap();
assert_eq!(
hex::encode(&bytes),
"a5010203262001214411223344646e6f7465f6"
);
assert_eq!(cbor2::from_slice::<CoseKey>(&bytes).unwrap(), sample());
let value = Value::serialized(&sample()).unwrap();
assert_eq!(value.deserialized::<CoseKey>().unwrap(), sample());
let textual = cbor2::cbor!({
1 => 2,
"alg" => -7,
-1 => 1,
-2 => cbor2::Value::Bytes(vec![0x11, 0x22, 0x33, 0x44]),
"note" => null,
})
.unwrap();
assert_eq!(textual.deserialized::<CoseKey>().unwrap(), sample());
}
#[test]
fn json_just_works() {
let json = serde_json::to_string(&sample()).unwrap();
assert_eq!(
json,
r#"{"kty":2,"alg":-7,"crv":1,"x":[17,34,51,68],"note":null}"#
);
assert_eq!(serde_json::from_str::<CoseKey>(&json).unwrap(), sample());
}
#[derive(Debug, PartialEq, Cbor)]
struct LifetimeNames<'a, '__de> {
#[cbor(key = 1)]
value: u8,
#[serde(skip)]
marker: core::marker::PhantomData<(&'a (), &'__de ())>,
}
#[test]
fn derive_avoids_internal_lifetime_name_collisions() {
let value = LifetimeNames {
value: 7,
marker: core::marker::PhantomData,
};
let bytes = cbor2::to_vec(&value).unwrap();
assert_eq!(hex::encode(&bytes), "a10107");
}
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 123)]
struct ProtectedHeader {
#[cbor(key = 1)]
alg: i8,
#[cbor(key = 4)]
#[serde(with = "serde_bytes")]
kid: Vec<u8>,
}
fn header() -> ProtectedHeader {
ProtectedHeader {
alg: -7,
kid: b"kid".to_vec(),
}
}
#[test]
fn tagged_structs_wrap_and_decode_transparently() {
let bytes = cbor2::to_vec(&header()).unwrap();
assert_eq!(hex::encode(&bytes), "d87ba2012604436b6964");
assert_eq!(
cbor2::diagnostic(&bytes[..]).unwrap(),
"123({1: -7, 4: h'6b6964'})"
);
assert_eq!(
cbor2::from_slice::<ProtectedHeader>(&bytes).unwrap(),
header()
);
assert_eq!(cbor2::to_canonical_vec(&header()).unwrap(), bytes);
let untagged = hex::decode("a2012604436b6964").unwrap();
assert_eq!(
cbor2::from_slice::<ProtectedHeader>(&untagged).unwrap(),
header()
);
let wrong = hex::decode("d87ca2012604436b6964").unwrap(); assert_eq!(
cbor2::from_slice::<ProtectedHeader>(&wrong).unwrap(),
header()
);
let wrapped = hex::decode("d9d9f7d87ba2012604436b6964").unwrap(); assert_eq!(
cbor2::from_slice::<ProtectedHeader>(&wrapped).unwrap(),
header()
);
let value = Value::serialized(&header()).unwrap();
assert_eq!(
value,
Value::Tag(
123,
Box::new(cbor2::cbor!({ 1 => -7, 4 => Value::Bytes(b"kid".to_vec()) }).unwrap())
)
);
assert_eq!(value.deserialized::<ProtectedHeader>().unwrap(), header());
let untagged = cbor2::cbor!({ 1 => -7, 4 => Value::Bytes(b"kid".to_vec()) }).unwrap();
assert_eq!(
untagged.deserialized::<ProtectedHeader>().unwrap(),
header()
);
let json = serde_json::to_string(&header()).unwrap();
assert_eq!(json, r#"{"alg":-7,"kid":[107,105,100]}"#);
assert_eq!(
serde_json::from_str::<ProtectedHeader>(&json).unwrap(),
header()
);
}
#[test]
fn tagged_tuple_structs_work_too() {
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 18)]
struct Sign1(
#[serde(with = "serde_bytes")] Vec<u8>,
u8,
#[serde(with = "serde_bytes")] Vec<u8>,
#[serde(with = "serde_bytes")] Vec<u8>,
);
let msg = Sign1(vec![0xa0], 0, vec![], vec![0xff]);
let bytes = cbor2::to_vec(&msg).unwrap();
assert_eq!(hex::encode(&bytes), "d28441a0004041ff");
assert_eq!(cbor2::from_slice::<Sign1>(&bytes).unwrap(), msg);
assert_eq!(
cbor2::from_slice::<Sign1>(&hex::decode("8441a0004041ff").unwrap()).unwrap(),
msg
);
}
#[test]
fn named_structs_can_use_cose_array_shape() {
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 18, array)]
struct Sign1 {
#[serde(with = "serde_bytes")]
protected: Vec<u8>,
unprotected: u8,
#[serde(with = "serde_bytes")]
payload: Vec<u8>,
#[serde(with = "serde_bytes")]
signature: Vec<u8>,
}
let msg = Sign1 {
protected: vec![0xa0],
unprotected: 0,
payload: vec![],
signature: vec![0xff],
};
assert_eq!(Sign1::KEYS, &[]);
assert_eq!(Sign1::TAG, Some(18));
const {
assert!(Sign1::ARRAY);
}
let bytes = cbor2::to_vec(&msg).unwrap();
assert_eq!(hex::encode(&bytes), "d28441a0004041ff");
assert_eq!(cbor2::from_slice::<Sign1>(&bytes).unwrap(), msg);
let json = serde_json::to_string(&msg).unwrap();
assert_eq!(
json,
r#"{"protected":[160],"unprotected":0,"payload":[],"signature":[255]}"#
);
assert_eq!(serde_json::from_str::<Sign1>(&json).unwrap(), msg);
}
#[test]
fn enums_and_generics_work_too() {
#[derive(Debug, PartialEq, Cbor)]
enum Message {
Signed {
#[cbor(key = 1)]
payload: u8,
label: bool,
},
Unit,
}
let bytes = cbor2::to_vec(&Message::Signed {
payload: 7,
label: true,
})
.unwrap();
assert_eq!(hex::encode(&bytes), "a1665369676e6564a20107656c6162656cf5");
assert_eq!(
cbor2::from_slice::<Message>(&bytes).unwrap(),
Message::Signed {
payload: 7,
label: true
}
);
assert_eq!(cbor2::to_vec(&Message::Unit).unwrap(), b"\x64Unit");
let json = serde_json::to_string(&Message::Signed {
payload: 7,
label: true,
})
.unwrap();
assert_eq!(json, r#"{"Signed":{"payload":7,"label":true}}"#);
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 7)]
struct Wrap<T> {
#[cbor(key = 1)]
inner: T,
}
let wrapped = Wrap { inner: 5u8 };
let bytes = cbor2::to_vec(&wrapped).unwrap();
assert_eq!(hex::encode(&bytes), "c7a10105"); assert_eq!(cbor2::from_slice::<Wrap<u8>>(&bytes).unwrap(), wrapped);
}
#[test]
fn serde_attributes_combine() {
#[derive(Debug, PartialEq, Cbor)]
struct Renamed {
#[cbor(key = 3)]
#[serde(rename = "alg", alias = "algorithm")]
algorithm: i8,
#[serde(default)]
note: String,
}
let value = Renamed {
algorithm: -7,
note: String::new(),
};
let bytes = cbor2::to_vec(&value).unwrap();
assert_eq!(hex::encode(&bytes), "a20326646e6f746560");
assert_eq!(cbor2::from_slice::<Renamed>(&bytes).unwrap(), value);
assert_eq!(
serde_json::to_string(&value).unwrap(),
r#"{"alg":-7,"note":""}"#
);
let parsed: Renamed = serde_json::from_str(r#"{"algorithm":-7}"#).unwrap();
assert_eq!(parsed, value);
}
#[test]
fn full_key_range() {
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 18446744073709551615)]
struct Edges {
#[cbor(key = 0)]
zero: u8,
#[cbor(key = 18446744073709551615)]
hi: u8,
#[cbor(key = -18446744073709551616)]
lo: u8,
}
let edges = Edges {
zero: 0,
hi: 1,
lo: 2,
};
let bytes = cbor2::to_vec(&edges).unwrap();
assert_eq!(
hex::encode(&bytes),
"dbffffffffffffffffa300001bffffffffffffffff013bffffffffffffffff02"
);
assert_eq!(cbor2::from_slice::<Edges>(&bytes).unwrap(), edges);
}
#[test]
fn derive_exposes_keys_and_tag() {
assert_eq!(
CoseKey::KEYS,
&[("kty", 1), ("alg", 3), ("crv", -1), ("x", -2)]
);
assert_eq!(CoseKey::TAG, None);
assert_eq!(ProtectedHeader::KEYS, &[("alg", 1), ("kid", 4)]);
assert_eq!(ProtectedHeader::TAG, Some(123));
let keys = sample().keys();
assert_eq!(keys.len(), 4);
assert_eq!(keys["kty"], 1);
assert_eq!(keys["x"], -2);
assert!(!keys.contains_key("note"));
#[derive(Cbor)]
struct Plain {
#[allow(dead_code)]
a: u8,
}
assert_eq!(Plain::KEYS, &[]);
assert_eq!(Plain::TAG, None);
assert!(Plain { a: 0 }.keys().is_empty());
}
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 61)]
struct Claims {
#[cbor(key = 1)]
#[serde(rename = "iss")]
issuer: String,
#[cbor(key = 4)]
#[serde(rename = "exp")]
expiration: u64,
}
#[test]
fn tagged_claims_decode_tagged_or_untagged() {
let claims = Claims {
issuer: "me".into(),
expiration: 9,
};
let bytes = cbor2::to_canonical_vec(&claims).unwrap();
assert_eq!(hex::encode(&bytes), "d83da201626d650409"); assert_eq!(&bytes[..2], [0xd8, 0x3d]);
assert_eq!(cbor2::from_slice::<Claims>(&bytes).unwrap(), claims);
assert_eq!(cbor2::from_slice::<Claims>(&bytes[2..]).unwrap(), claims);
assert_eq!(Claims::TAG, Some(61));
let value = Value::serialized(&claims).unwrap();
assert_eq!(value.deserialized::<Claims>().unwrap(), claims);
assert_eq!(
cbor2::cbor!({ 1 => "me", 4 => 9 })
.unwrap()
.deserialized::<Claims>()
.unwrap(),
claims
);
}
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 61)]
struct OpenClaims {
#[cbor(key = 1)]
#[serde(rename = "iss")]
issuer: String,
#[cbor(key = 4)]
#[serde(rename = "exp")]
expiration: u64,
#[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
extra: BTreeMap<String, Value>,
}
#[test]
fn tagged_claims_support_flattened_extra_fields() {
let mut extra = BTreeMap::new();
extra.insert("scope".into(), Value::Array(vec![Value::from("read")]));
extra.insert("tenant".into(), Value::from("acme"));
let claims = OpenClaims {
issuer: "me".into(),
expiration: 9,
extra,
};
let value = Value::serialized(&claims).unwrap();
assert_eq!(
value,
Value::Tag(
61,
Box::new(
cbor2::cbor!({
1 => "me",
4 => 9,
"scope" => ["read"],
"tenant" => "acme",
})
.unwrap()
)
)
);
let bytes = cbor2::to_canonical_vec(&claims).unwrap();
assert_eq!(cbor2::from_slice::<OpenClaims>(&bytes).unwrap(), claims);
assert_eq!(
cbor2::from_slice::<OpenClaims>(&bytes[2..]).unwrap(),
claims
);
assert_eq!(value.deserialized::<OpenClaims>().unwrap(), claims);
assert_eq!(
cbor2::cbor!({
1 => "me",
4 => 9,
"scope" => ["read"],
"tenant" => "acme",
})
.unwrap()
.deserialized::<OpenClaims>()
.unwrap(),
claims
);
assert_eq!(OpenClaims::KEYS, &[("iss", 1), ("exp", 4)]);
assert_eq!(OpenClaims::TAG, Some(61));
let json = serde_json::to_string(&claims).unwrap();
assert_eq!(
json,
r#"{"iss":"me","exp":9,"scope":["read"],"tenant":"acme"}"#
);
assert_eq!(serde_json::from_str::<OpenClaims>(&json).unwrap(), claims);
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Label {
Int(i64),
Text(String),
}
impl serde::Serialize for Label {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Int(value) => serializer.serialize_i64(*value),
Self::Text(value) => serializer.serialize_str(value),
}
}
}
struct LabelVisitor;
impl<'de> serde::de::Visitor<'de> for LabelVisitor {
type Value = Label;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("an integer or text COSE label")
}
fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
Ok(Label::Int(value))
}
fn visit_i128<E: serde::de::Error>(self, value: i128) -> Result<Self::Value, E> {
i64::try_from(value)
.map(Label::Int)
.map_err(|_| E::custom("COSE integer label is out of i64 range"))
}
fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
i64::try_from(value)
.map(Label::Int)
.map_err(|_| E::custom("COSE integer label is out of i64 range"))
}
fn visit_u128<E: serde::de::Error>(self, value: u128) -> Result<Self::Value, E> {
i64::try_from(value)
.map(Label::Int)
.map_err(|_| E::custom("COSE integer label is out of i64 range"))
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(Label::Text(value.into()))
}
fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
Ok(Label::Text(value))
}
}
impl<'de> serde::Deserialize<'de> for Label {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(LabelVisitor)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
struct CoseMap(BTreeMap<Label, Value>);
impl CoseMap {
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl serde::Serialize for CoseMap {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap as _;
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key, value) in &self.0 {
map.serialize_entry(key, value)?;
}
map.end()
}
}
struct CoseMapVisitor;
impl<'de> serde::de::Visitor<'de> for CoseMapVisitor {
type Value = CoseMap;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("a COSE label map")
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut acc: A) -> Result<Self::Value, A::Error> {
let mut map = BTreeMap::new();
while let Some((key, value)) = acc.next_entry::<Label, Value>()? {
map.insert(key, value);
}
Ok(CoseMap(map))
}
}
impl<'de> serde::Deserialize<'de> for CoseMap {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_map(CoseMapVisitor)
}
}
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 61)]
struct LabelClaims {
#[cbor(key = 1)]
#[serde(rename = "iss")]
issuer: String,
#[cbor(key = 4)]
#[serde(rename = "exp")]
expiration: u64,
#[serde(flatten, default, skip_serializing_if = "CoseMap::is_empty")]
extra: CoseMap,
}
#[test]
fn tagged_claims_support_flattened_cose_label_map() {
let mut extra = CoseMap::default();
extra.0.insert(Label::Int(-65537), Value::from("private"));
extra.0.insert(Label::Int(42), Value::from("answer"));
extra
.0
.insert(Label::Text("tenant".into()), Value::from("acme"));
let claims = LabelClaims {
issuer: "me".into(),
expiration: 9,
extra,
};
let untagged = cbor2::cbor!({
1 => "me",
4 => 9,
-65537 => "private",
42 => "answer",
"tenant" => "acme",
})
.unwrap();
let value = Value::serialized(&claims).unwrap();
assert_eq!(value, Value::Tag(61, Box::new(untagged.clone())));
let bytes = cbor2::to_canonical_vec(&claims).unwrap();
assert_eq!(cbor2::from_slice::<LabelClaims>(&bytes).unwrap(), claims);
assert_eq!(
cbor2::from_slice::<LabelClaims>(&bytes[2..]).unwrap(),
claims
);
assert_eq!(value.deserialized::<LabelClaims>().unwrap(), claims);
assert_eq!(untagged.deserialized::<LabelClaims>().unwrap(), claims);
}