| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // TODO(crbug.com/262737383): When Crubit supports generating C++ pattern |
| // matching and accessor logic for non-repr(C) Rust ADT enums (like `Value` and |
| // `MapKey`), remove all manual inspection (`kind()`) and payload extraction |
| // (`as_int()`, `as_string()`, `as_array()`, etc.) methods below, as well as the |
| // `MapKeyKind` and `ValueKind` proxy enums. |
| use alloc::vec::Vec; |
| use core::cmp::Ordering; |
| |
| use crate::constants::*; |
| use crate::writer; |
| |
| #[repr(C)] |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub enum ValueKind { |
| Int = 0, |
| Bytestring = 1, |
| String = 2, |
| Array = 3, |
| Map = 4, |
| Boolean = 5, |
| // Obsolete: Float = 6, |
| Null = 7, |
| Undefined = 8, |
| InvalidUtf8 = 9, |
| } |
| |
| /// Value represents a CBOR structure. |
| /// |
| /// Integers are mapped to `i64` despite CBOR having 65-bit integers. CBOR |
| /// integers outside the range of an `i64` result in an error during parsing. |
| /// Byte strings are returned as `Bytes`s in order to avoid copies. |
| /// |
| /// `Default` is derived because Crubit requires it to generate a C++ move |
| /// constructor; `Null` is the natural empty value. |
| #[derive(Debug, PartialEq, Clone, Default)] |
| pub enum Value<'a> { |
| Int(i64), |
| Bytestring(&'a [u8]), |
| String(&'a str), |
| Array(Vec<Value<'a>>), |
| Map(Map<'a>), |
| Boolean(bool), |
| #[default] |
| Null, |
| Undefined, |
| InvalidUtf8(&'a [u8]), |
| } |
| |
| impl<'a> Value<'a> { |
| // to_bytes serialises `self` to CBOR and returns the result. |
| pub fn to_bytes(&self) -> Vec<u8> { |
| writer::write(self) |
| } |
| |
| // append_bytes appends a serialisation of `self` to `out`. |
| pub fn append_bytes(&self, out: &mut Vec<u8>) { |
| writer::append_value(self, out); |
| } |
| |
| pub fn kind(&self) -> ValueKind { |
| match self { |
| Self::Int(_) => ValueKind::Int, |
| Self::Bytestring(_) => ValueKind::Bytestring, |
| Self::String(_) => ValueKind::String, |
| Self::Array(_) => ValueKind::Array, |
| Self::Map(_) => ValueKind::Map, |
| Self::Boolean(_) => ValueKind::Boolean, |
| Self::Null => ValueKind::Null, |
| Self::Undefined => ValueKind::Undefined, |
| Self::InvalidUtf8(_) => ValueKind::InvalidUtf8, |
| } |
| } |
| |
| pub fn as_int(&self) -> Option<i64> { |
| match self { |
| Self::Int(v) => Some(*v), |
| _ => None, |
| } |
| } |
| |
| pub fn as_bool(&self) -> Option<bool> { |
| match self { |
| Self::Boolean(v) => Some(*v), |
| _ => None, |
| } |
| } |
| |
| pub fn as_bytestring(&self) -> Option<&'a [u8]> { |
| match self { |
| Self::Bytestring(v) => Some(v), |
| _ => None, |
| } |
| } |
| |
| pub fn as_string(&self) -> Option<&'a str> { |
| match self { |
| Self::String(s) => Some(s), |
| _ => None, |
| } |
| } |
| |
| pub fn as_invalid_utf8(&self) -> Option<&'a [u8]> { |
| match self { |
| Self::InvalidUtf8(v) => Some(v), |
| _ => None, |
| } |
| } |
| |
| pub fn as_array(&self) -> Option<&[Value<'a>]> { |
| match self { |
| Self::Array(v) => Some(v), |
| _ => None, |
| } |
| } |
| |
| pub fn map_entries(&self) -> Option<&[MapEntry<'a>]> { |
| match self { |
| Self::Map(m) => Some(m), |
| _ => None, |
| } |
| } |
| } |
| |
| impl<'a> From<MapKey<'a>> for Value<'a> { |
| fn from(key: MapKey<'a>) -> Self { |
| match key { |
| MapKey::Int(val) => Self::Int(val), |
| MapKey::Bytestring(bytes) => Self::Bytestring(bytes), |
| MapKey::String(text) => Self::String(text), |
| MapKey::InvalidUtf8(bytes) => Self::InvalidUtf8(bytes), |
| } |
| } |
| } |
| |
| #[repr(C)] |
| #[derive(Debug, PartialEq, Clone, Default)] |
| pub struct MapEntry<'a> { |
| pub key: MapKey<'a>, |
| pub value: Value<'a>, |
| } |
| |
| impl<'a> From<(MapKey<'a>, Value<'a>)> for MapEntry<'a> { |
| fn from((key, value): (MapKey<'a>, Value<'a>)) -> Self { |
| Self { key, value } |
| } |
| } |
| |
| // TODO(crbug.com/539701789): Remove these helpers once Crubit's |
| // `rs_std::Vec<T>` bindings expose `with_capacity()` and `push()`. Crubit |
| // instantiates the lifetime as `'static`, so the C++ caller must drop the `Vec` |
| // before the buffers it borrows. |
| |
| pub fn vec_with_capacity_values(capacity: usize) -> Vec<Value<'static>> { |
| Vec::with_capacity(capacity) |
| } |
| |
| pub fn vec_push_value(vec: &mut Vec<Value<'static>>, value: Value<'static>) { |
| vec.push(value); |
| } |
| |
| pub fn vec_with_capacity_entries(capacity: usize) -> Vec<MapEntry<'static>> { |
| Vec::with_capacity(capacity) |
| } |
| |
| pub fn vec_push_entry(vec: &mut Vec<MapEntry<'static>>, entry: MapEntry<'static>) { |
| vec.push(entry); |
| } |
| |
| /// A wrapper around `Vec<MapEntry<'a>>` that represents a collection whose |
| /// elements are guaranteed to be sorted by key and unique. |
| #[derive(Debug, PartialEq, Clone, Default)] |
| pub struct Map<'a>(Vec<MapEntry<'a>>); |
| |
| impl<'a> Map<'a> { |
| pub fn new() -> Self { |
| Self(Vec::new()) |
| } |
| |
| /// Creates a new `Map` from a `Vec` without checking if the elements |
| /// are sorted or unique in release builds. |
| /// |
| /// Caller must ensure that `vec` is sorted by key and unique. |
| pub fn from_sorted_vec_unchecked(vec: Vec<MapEntry<'a>>) -> Self { |
| debug_assert!( |
| vec.is_sorted_by(|a, b| a.key < b.key), |
| "CBOR map entries must be sorted by key and unique" |
| ); |
| Self(vec) |
| } |
| |
| /// Looks up a value by its `MapKey` using binary search. |
| pub fn get(&self, key: &MapKey<'_>) -> Option<&Value<'a>> { |
| let index = self.0.binary_search_by_key(&key, |entry| &entry.key).ok()?; |
| Some(&self.0[index].value) |
| } |
| } |
| |
| impl<'a> From<Vec<MapEntry<'a>>> for Map<'a> { |
| fn from(mut vec: Vec<MapEntry<'a>>) -> Self { |
| vec.sort_by(|a, b| a.key.cmp(&b.key)); |
| Self(vec) |
| } |
| } |
| |
| impl<'a> core::ops::Deref for Map<'a> { |
| type Target = [MapEntry<'a>]; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.0 |
| } |
| } |
| |
| impl<'a> IntoIterator for Map<'a> { |
| type Item = MapEntry<'a>; |
| type IntoIter = alloc::vec::IntoIter<MapEntry<'a>>; |
| |
| fn into_iter(self) -> Self::IntoIter { |
| self.0.into_iter() |
| } |
| } |
| |
| impl<'a, 'b> IntoIterator for &'b Map<'a> { |
| type Item = &'b MapEntry<'a>; |
| type IntoIter = core::slice::Iter<'b, MapEntry<'a>>; |
| |
| fn into_iter(self) -> Self::IntoIter { |
| self.0.iter() |
| } |
| } |
| |
| #[repr(C)] |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
| pub enum MapKeyKind { |
| Int = 0, |
| Bytestring = 1, |
| String = 2, |
| InvalidUtf8 = 3, |
| } |
| |
| /// A MapKey is the type of values that can key a CBOR map. |
| #[derive(Debug, Clone, Copy)] |
| pub enum MapKey<'a> { |
| // A separate `MapKey` type is used because we want to exclude things like |
| // maps keyed by arrays or other maps. Such structures never appear in |
| // CTAP and so we don't need to support them. |
| // |
| // We expect that a map will always have keys of the same type, which |
| // suggests that `Value::Map` could be split into `Value::IntKeyedMap` etc. |
| // However, that falls down when a map is empty because the parser can't |
| // know what the key type should be, yet calling code will want to expect |
| // the right type of map. Thus we end up supporting heterogeneous maps. |
| Int(i64), |
| Bytestring(&'a [u8]), |
| String(&'a str), |
| /// Test-only `Value::Type::INVALID_UTF8` key from C++; encoded as a text |
| /// string. |
| InvalidUtf8(&'a [u8]), |
| } |
| |
| /// Required by Crubit to generate a C++ move constructor. |
| impl Default for MapKey<'_> { |
| fn default() -> Self { |
| Self::Int(0) |
| } |
| } |
| |
| impl<'a> MapKey<'a> { |
| pub(crate) fn type_arg_and_payload(&self) -> (u8, u64, Option<&'a [u8]>) { |
| match self { |
| Self::Int(v) if *v >= 0 => (MAJOR_TYPE_UNSIGNED_INT, *v as u64, None), |
| Self::Int(v) => (MAJOR_TYPE_NEGATIVE_INT, !*v as u64, None), |
| Self::Bytestring(b) => (MAJOR_TYPE_BYTE_STRING, b.len() as u64, Some(b)), |
| Self::String(s) => (MAJOR_TYPE_TEXT_STRING, s.len() as u64, Some(s.as_bytes())), |
| Self::InvalidUtf8(b) => (MAJOR_TYPE_TEXT_STRING, b.len() as u64, Some(b)), |
| } |
| } |
| |
| pub fn kind(&self) -> MapKeyKind { |
| match self { |
| Self::Int(_) => MapKeyKind::Int, |
| Self::Bytestring(_) => MapKeyKind::Bytestring, |
| Self::String(_) => MapKeyKind::String, |
| Self::InvalidUtf8(_) => MapKeyKind::InvalidUtf8, |
| } |
| } |
| |
| pub fn as_int(&self) -> Option<i64> { |
| match self { |
| Self::Int(v) => Some(*v), |
| _ => None, |
| } |
| } |
| |
| pub fn as_bytestring(&self) -> Option<&'a [u8]> { |
| match self { |
| Self::Bytestring(v) => Some(v), |
| _ => None, |
| } |
| } |
| |
| pub fn as_string(&self) -> Option<&'a str> { |
| match self { |
| Self::String(s) => Some(s), |
| _ => None, |
| } |
| } |
| |
| pub fn as_invalid_utf8(&self) -> Option<&'a [u8]> { |
| match self { |
| Self::InvalidUtf8(v) => Some(v), |
| _ => None, |
| } |
| } |
| } |
| |
| impl<'a> TryFrom<Value<'a>> for MapKey<'a> { |
| type Error = Value<'a>; |
| |
| fn try_from(value: Value<'a>) -> Result<Self, Self::Error> { |
| match value { |
| Value::Int(val) => Ok(Self::Int(val)), |
| Value::Bytestring(bytes) => Ok(Self::Bytestring(bytes)), |
| Value::String(text) => Ok(Self::String(text)), |
| _ => Err(value), |
| } |
| } |
| } |
| |
| impl PartialEq for MapKey<'_> { |
| fn eq(&self, other: &Self) -> bool { |
| self.type_arg_and_payload() == other.type_arg_and_payload() |
| } |
| } |
| |
| impl Eq for MapKey<'_> {} |
| |
| impl PartialOrd for MapKey<'_> { |
| fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
| Some(self.cmp(other)) |
| } |
| } |
| |
| impl Ord for MapKey<'_> { |
| fn cmp(&self, other: &Self) -> Ordering { |
| // CTAP2 canonical CBOR orders map keys by major type, length, then |
| // bytes. |
| self.type_arg_and_payload().cmp(&other.type_arg_and_payload()) |
| } |
| } |