use rustlavel_core::{Error, Result};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub enum Cbor {
Unsigned(u64),
Negative(i64),
Bytes(Vec<u8>),
Text(String),
Array(Vec<Cbor>),
Map(BTreeMap<CborKey, Cbor>),
Bool(bool),
Null,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum CborKey {
Int(i64),
Text(String),
}
impl Cbor {
pub fn parse(bytes: &[u8]) -> Result<Cbor> {
let mut decoder = Decoder { bytes, position: 0, depth: 0 };
let value = decoder.value()?;
if decoder.position != bytes.len() {
return Err(Error::msg(format!(
"CBOR had {} trailing bytes after a complete value",
bytes.len() - decoder.position
)));
}
Ok(value)
}
pub fn parse_prefix(bytes: &[u8]) -> Result<(Cbor, usize)> {
let mut decoder = Decoder { bytes, position: 0, depth: 0 };
let value = decoder.value()?;
Ok((value, decoder.position))
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Cbor::Bytes(bytes) => Some(bytes),
_ => None,
}
}
pub fn as_text(&self) -> Option<&str> {
match self {
Cbor::Text(text) => Some(text),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Cbor::Unsigned(value) => i64::try_from(*value).ok(),
Cbor::Negative(value) => Some(*value),
_ => None,
}
}
pub fn get(&self, key: &str) -> Option<&Cbor> {
match self {
Cbor::Map(map) => map.get(&CborKey::Text(key.to_string())),
_ => None,
}
}
pub fn get_int(&self, key: i64) -> Option<&Cbor> {
match self {
Cbor::Map(map) => map.get(&CborKey::Int(key)),
_ => None,
}
}
}
const MAX_DEPTH: usize = 16;
struct Decoder<'a> {
bytes: &'a [u8],
position: usize,
depth: usize,
}
impl Decoder<'_> {
fn value(&mut self) -> Result<Cbor> {
if self.depth > MAX_DEPTH {
return Err(Error::msg(format!("CBOR nested deeper than {MAX_DEPTH} levels")));
}
let initial = self.byte()?;
let major = initial >> 5;
let extra = initial & 0x1f;
match major {
0 => Ok(Cbor::Unsigned(self.argument(extra)?)),
1 => {
let magnitude = self.argument(extra)?;
i64::try_from(magnitude)
.map(|n| Cbor::Negative(-1 - n))
.map_err(|_| Error::msg("CBOR negative integer is too large for i64"))
}
2 => Ok(Cbor::Bytes(self.take(extra)?.to_vec())),
3 => {
let raw = self.take(extra)?;
String::from_utf8(raw.to_vec())
.map(Cbor::Text)
.map_err(|_| Error::msg("CBOR text string is not valid UTF-8"))
}
4 => {
let count = self.count(extra)?;
let mut items = Vec::with_capacity(count.min(1024));
for _ in 0..count {
self.depth += 1;
items.push(self.value()?);
self.depth -= 1;
}
Ok(Cbor::Array(items))
}
5 => {
let count = self.count(extra)?;
let mut map = BTreeMap::new();
for _ in 0..count {
self.depth += 1;
let key = self.key()?;
let value = self.value()?;
self.depth -= 1;
if map.insert(key, value).is_some() {
return Err(Error::msg("CBOR map has a duplicate key"));
}
}
Ok(Cbor::Map(map))
}
7 => match extra {
20 => Ok(Cbor::Bool(false)),
21 => Ok(Cbor::Bool(true)),
22 => Ok(Cbor::Null),
23 => Ok(Cbor::Null),
other => Err(Error::msg(format!(
"CBOR simple value {other} is not one an authenticator may send"
))),
},
other => Err(Error::msg(format!(
"CBOR major type {other} is not accepted here — tags and floats have no place \
in an authenticator message"
))),
}
}
fn key(&mut self) -> Result<CborKey> {
match self.value()? {
Cbor::Unsigned(value) => i64::try_from(value)
.map(CborKey::Int)
.map_err(|_| Error::msg("CBOR map key is too large")),
Cbor::Negative(value) => Ok(CborKey::Int(value)),
Cbor::Text(text) => Ok(CborKey::Text(text)),
other => Err(Error::msg(format!("CBOR map key must be an integer or text, got {other:?}"))),
}
}
fn argument(&mut self, extra: u8) -> Result<u64> {
match extra {
0..=23 => Ok(extra as u64),
24 => Ok(self.byte()? as u64),
25 => Ok(u16::from_be_bytes(self.fixed::<2>()?) as u64),
26 => Ok(u32::from_be_bytes(self.fixed::<4>()?) as u64),
27 => Ok(u64::from_be_bytes(self.fixed::<8>()?)),
31 => Err(Error::msg(
"CBOR indefinite-length items are not canonical and are refused here",
)),
other => Err(Error::msg(format!("CBOR additional information {other} is reserved"))),
}
}
fn count(&mut self, extra: u8) -> Result<usize> {
let claimed = self.argument(extra)?;
let remaining = self.bytes.len() - self.position;
if claimed > remaining as u64 {
return Err(Error::msg(format!(
"CBOR header claims {claimed} items but only {remaining} bytes remain"
)));
}
Ok(claimed as usize)
}
fn take(&mut self, extra: u8) -> Result<&[u8]> {
let length = self.argument(extra)? as usize;
let end = self
.position
.checked_add(length)
.ok_or_else(|| Error::msg("CBOR length overflows"))?;
if end > self.bytes.len() {
return Err(Error::msg(format!(
"CBOR string of {length} bytes runs past the end of the message"
)));
}
let slice = &self.bytes[self.position..end];
self.position = end;
Ok(slice)
}
fn byte(&mut self) -> Result<u8> {
let byte = *self
.bytes
.get(self.position)
.ok_or_else(|| Error::msg("CBOR ended in the middle of a value"))?;
self.position += 1;
Ok(byte)
}
fn fixed<const N: usize>(&mut self) -> Result<[u8; N]> {
let end = self.position + N;
if end > self.bytes.len() {
return Err(Error::msg("CBOR ended in the middle of a number"));
}
let mut out = [0u8; N];
out.copy_from_slice(&self.bytes[self.position..end]);
self.position = end;
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_the_integer_encodings() {
assert_eq!(Cbor::parse(&[0x00]).unwrap(), Cbor::Unsigned(0));
assert_eq!(Cbor::parse(&[0x17]).unwrap(), Cbor::Unsigned(23));
assert_eq!(Cbor::parse(&[0x18, 0x18]).unwrap(), Cbor::Unsigned(24));
assert_eq!(Cbor::parse(&[0x19, 0x01, 0x00]).unwrap(), Cbor::Unsigned(256));
assert_eq!(Cbor::parse(&[0x1a, 0, 1, 0, 0]).unwrap(), Cbor::Unsigned(65536));
assert_eq!(Cbor::parse(&[0x26]).unwrap(), Cbor::Negative(-7));
assert_eq!(Cbor::parse(&[0x20]).unwrap(), Cbor::Negative(-1));
}
#[test]
fn reads_strings_arrays_and_maps() {
assert_eq!(Cbor::parse(&[0x43, 1, 2, 3]).unwrap(), Cbor::Bytes(vec![1, 2, 3]));
assert_eq!(Cbor::parse(b"\x63abc").unwrap(), Cbor::Text("abc".into()));
assert_eq!(
Cbor::parse(&[0x82, 0x01, 0x02]).unwrap(),
Cbor::Array(vec![Cbor::Unsigned(1), Cbor::Unsigned(2)])
);
let map = Cbor::parse(b"\xa1\x63fmt\x64none").unwrap();
assert_eq!(map.get("fmt").and_then(Cbor::as_text), Some("none"));
}
#[test]
fn a_cose_key_reads_by_integer_label() {
let key = Cbor::parse(&[0xa2, 0x01, 0x02, 0x03, 0x26]).unwrap();
assert_eq!(key.get_int(1).and_then(Cbor::as_i64), Some(2));
assert_eq!(key.get_int(3).and_then(Cbor::as_i64), Some(-7));
}
#[test]
fn trailing_bytes_are_refused() {
let error = Cbor::parse(&[0x00, 0x00]).unwrap_err().to_string();
assert!(error.contains("trailing"), "got {error}");
let (value, consumed) = Cbor::parse_prefix(&[0x00, 0xff]).unwrap();
assert_eq!(value, Cbor::Unsigned(0));
assert_eq!(consumed, 1);
}
#[test]
fn a_length_longer_than_the_message_is_refused_before_allocating() {
let error = Cbor::parse(&[0x5a, 0xff, 0xff, 0xff, 0xff, 0x00]).unwrap_err().to_string();
assert!(error.contains("runs past the end"), "got {error}");
let error = Cbor::parse(&[0x9a, 0xff, 0xff, 0xff, 0xff]).unwrap_err().to_string();
assert!(error.contains("only"), "got {error}");
}
#[test]
fn indefinite_lengths_are_refused_rather_than_parsed() {
let error = Cbor::parse(&[0x5f, 0x41, 0x01, 0xff]).unwrap_err().to_string();
assert!(error.contains("indefinite"), "got {error}");
}
#[test]
fn tags_and_floats_are_refused() {
assert!(Cbor::parse(&[0xc0, 0x00]).is_err(), "tag");
assert!(Cbor::parse(&[0xfa, 0, 0, 0, 0]).is_err(), "float");
}
#[test]
fn a_duplicate_map_key_is_an_error_not_a_silent_choice() {
let error = Cbor::parse(&[0xa2, 0x01, 0x01, 0x01, 0x02]).unwrap_err().to_string();
assert!(error.contains("duplicate"), "got {error}");
}
#[test]
fn deep_nesting_is_bounded() {
let deep: Vec<u8> = std::iter::repeat_n(0x81, MAX_DEPTH + 2).chain([0x00]).collect();
let error = Cbor::parse(&deep).unwrap_err().to_string();
assert!(error.contains("nested deeper"), "got {error}");
}
#[test]
fn truncation_anywhere_is_an_error_and_never_a_panic() {
let complete: &[u8] = &[0xa2, 0x01, 0x02, 0x03, 0x26];
for length in 0..complete.len() {
assert!(Cbor::parse(&complete[..length]).is_err(), "prefix of {length} bytes");
}
assert!(Cbor::parse(complete).is_ok());
}
#[test]
fn text_that_is_not_utf8_is_refused() {
assert!(Cbor::parse(&[0x62, 0xff, 0xfe]).is_err());
}
}