use matter_codec::{Element, Tag, TlvReader, TlvWriter, Value};
use crate::error::{Error, Result};
use crate::tlv_tags as tags;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DnAttribute {
CommonName(String),
Surname(String),
SerialNumber(String),
CountryName(String),
LocalityName(String),
StateOrProvinceName(String),
OrganizationName(String),
OrganizationalUnitName(String),
Title(String),
Name(String),
GivenName(String),
Initials(String),
GenerationQualifier(String),
DnQualifier(String),
Pseudonym(String),
DomainComponent(String),
NodeId(u64),
IcacId(u64),
RcacId(u64),
FabricId(u64),
CaseAuthenticatedTag(u32),
VendorId(u16),
ProductId(u16),
Other {
tag: u8,
value: DnAttributeValue,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DnAttributeValue {
Utf8(String),
Uint(u64),
Bytes(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DistinguishedName(Vec<DnAttribute>);
impl DistinguishedName {
#[must_use]
pub fn new(attrs: Vec<DnAttribute>) -> Self {
Self(attrs)
}
pub fn iter(&self) -> core::slice::Iter<'_, DnAttribute> {
self.0.iter()
}
#[must_use]
pub fn node_id(&self) -> Option<u64> {
self.0.iter().find_map(|a| match a {
DnAttribute::NodeId(v) => Some(*v),
_ => None,
})
}
#[must_use]
pub fn fabric_id(&self) -> Option<u64> {
self.0.iter().find_map(|a| match a {
DnAttribute::FabricId(v) => Some(*v),
_ => None,
})
}
#[must_use]
pub fn rcac_id(&self) -> Option<u64> {
self.0.iter().find_map(|a| match a {
DnAttribute::RcacId(v) => Some(*v),
_ => None,
})
}
#[must_use]
pub fn icac_id(&self) -> Option<u64> {
self.0.iter().find_map(|a| match a {
DnAttribute::IcacId(v) => Some(*v),
_ => None,
})
}
#[must_use]
pub fn common_name(&self) -> Option<&str> {
self.0.iter().find_map(|a| match a {
DnAttribute::CommonName(v) => Some(v.as_str()),
_ => None,
})
}
#[allow(dead_code)]
pub(crate) fn read(reader: &mut TlvReader<'_>) -> Result<Self> {
match reader.next()? {
Some(Element::ContainerStart {
kind: matter_codec::ContainerKind::List,
..
}) => {}
_ => return Err(Error::WrongFieldType(0)),
}
Self::read_from_open_list(reader)
}
pub(crate) fn read_from_open_list(reader: &mut TlvReader<'_>) -> Result<Self> {
let mut attrs = Vec::new();
loop {
match reader.next()? {
None => return Err(matter_codec::Error::UnclosedContainer.into()),
Some(Element::ContainerEnd) => break,
Some(Element::Scalar { tag, value }) => {
let Tag::Context(tag_num) = tag else {
return Err(Error::InvalidDnAttribute(0));
};
attrs.push(decode_attribute(tag_num, value)?);
}
Some(Element::ContainerStart { .. }) => {
return Err(Error::WrongFieldType(0));
}
Some(_) => return Err(Error::WrongFieldType(0)),
}
}
Ok(Self(attrs))
}
pub(crate) fn write(&self, writer: &mut TlvWriter<'_>, outer_tag: Tag) -> Result<()> {
writer.start_list(outer_tag)?;
for attr in &self.0 {
encode_attribute(writer, attr)?;
}
writer.end_container()?;
Ok(())
}
}
impl<'a> IntoIterator for &'a DistinguishedName {
type Item = &'a DnAttribute;
type IntoIter = core::slice::Iter<'a, DnAttribute>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
pub(crate) fn decode_attribute(tag: u8, value: Value) -> Result<DnAttribute> {
use DnAttribute as A;
match (tag, value) {
(tags::DN_COMMON_NAME, Value::Utf8(s)) => Ok(A::CommonName(s)),
(tags::DN_SURNAME, Value::Utf8(s)) => Ok(A::Surname(s)),
(tags::DN_SERIAL_NUMBER, Value::Utf8(s)) => Ok(A::SerialNumber(s)),
(tags::DN_COUNTRY_NAME, Value::Utf8(s)) => Ok(A::CountryName(s)),
(tags::DN_LOCALITY_NAME, Value::Utf8(s)) => Ok(A::LocalityName(s)),
(tags::DN_STATE_OR_PROVINCE, Value::Utf8(s)) => Ok(A::StateOrProvinceName(s)),
(tags::DN_ORGANIZATION_NAME, Value::Utf8(s)) => Ok(A::OrganizationName(s)),
(tags::DN_ORG_UNIT_NAME, Value::Utf8(s)) => Ok(A::OrganizationalUnitName(s)),
(tags::DN_TITLE, Value::Utf8(s)) => Ok(A::Title(s)),
(tags::DN_NAME, Value::Utf8(s)) => Ok(A::Name(s)),
(tags::DN_GIVEN_NAME, Value::Utf8(s)) => Ok(A::GivenName(s)),
(tags::DN_INITIALS, Value::Utf8(s)) => Ok(A::Initials(s)),
(tags::DN_GENERATION_QUALIFIER, Value::Utf8(s)) => Ok(A::GenerationQualifier(s)),
(tags::DN_DN_QUALIFIER, Value::Utf8(s)) => Ok(A::DnQualifier(s)),
(tags::DN_PSEUDONYM, Value::Utf8(s)) => Ok(A::Pseudonym(s)),
(tags::DN_DOMAIN_COMPONENT, Value::Utf8(s)) => Ok(A::DomainComponent(s)),
(tags::DN_MATTER_NODE_ID, Value::Uint(v)) => Ok(A::NodeId(v)),
(tags::DN_MATTER_ICAC_ID, Value::Uint(v)) => Ok(A::IcacId(v)),
(tags::DN_MATTER_RCAC_ID, Value::Uint(v)) => Ok(A::RcacId(v)),
(tags::DN_MATTER_FABRIC_ID, Value::Uint(v)) => Ok(A::FabricId(v)),
(tags::DN_MATTER_NOC_CAT, Value::Uint(v)) => {
let v32 = u32::try_from(v).map_err(|_| Error::FieldValueOutOfRange { tag })?;
Ok(A::CaseAuthenticatedTag(v32))
}
(n, Value::Utf8(s)) if is_untyped_dn_tag(n) => Ok(A::Other {
tag: n,
value: DnAttributeValue::Utf8(s),
}),
(n, Value::Uint(v)) if is_untyped_dn_tag(n) => Ok(A::Other {
tag: n,
value: DnAttributeValue::Uint(v),
}),
(n, Value::Bytes(b)) if is_untyped_dn_tag(n) => Ok(A::Other {
tag: n,
value: DnAttributeValue::Bytes(b),
}),
(n, _) if !is_dn_tag(n) => Err(Error::InvalidDnAttribute(n)),
(n, _) => Err(Error::InvalidDnAttributeType(n)),
}
}
fn encode_attribute(writer: &mut TlvWriter<'_>, attr: &DnAttribute) -> Result<()> {
use DnAttribute as A;
match attr {
A::CommonName(s) => writer.put_utf8(Tag::Context(tags::DN_COMMON_NAME), s)?,
A::Surname(s) => writer.put_utf8(Tag::Context(tags::DN_SURNAME), s)?,
A::SerialNumber(s) => writer.put_utf8(Tag::Context(tags::DN_SERIAL_NUMBER), s)?,
A::CountryName(s) => writer.put_utf8(Tag::Context(tags::DN_COUNTRY_NAME), s)?,
A::LocalityName(s) => writer.put_utf8(Tag::Context(tags::DN_LOCALITY_NAME), s)?,
A::StateOrProvinceName(s) => {
writer.put_utf8(Tag::Context(tags::DN_STATE_OR_PROVINCE), s)?;
}
A::OrganizationName(s) => {
writer.put_utf8(Tag::Context(tags::DN_ORGANIZATION_NAME), s)?;
}
A::OrganizationalUnitName(s) => {
writer.put_utf8(Tag::Context(tags::DN_ORG_UNIT_NAME), s)?;
}
A::Title(s) => writer.put_utf8(Tag::Context(tags::DN_TITLE), s)?,
A::Name(s) => writer.put_utf8(Tag::Context(tags::DN_NAME), s)?,
A::GivenName(s) => writer.put_utf8(Tag::Context(tags::DN_GIVEN_NAME), s)?,
A::Initials(s) => writer.put_utf8(Tag::Context(tags::DN_INITIALS), s)?,
A::GenerationQualifier(s) => {
writer.put_utf8(Tag::Context(tags::DN_GENERATION_QUALIFIER), s)?;
}
A::DnQualifier(s) => writer.put_utf8(Tag::Context(tags::DN_DN_QUALIFIER), s)?,
A::Pseudonym(s) => writer.put_utf8(Tag::Context(tags::DN_PSEUDONYM), s)?,
A::DomainComponent(s) => {
writer.put_utf8(Tag::Context(tags::DN_DOMAIN_COMPONENT), s)?;
}
A::NodeId(v) => writer.put_uint(Tag::Context(tags::DN_MATTER_NODE_ID), *v)?,
A::IcacId(v) => writer.put_uint(Tag::Context(tags::DN_MATTER_ICAC_ID), *v)?,
A::RcacId(v) => writer.put_uint(Tag::Context(tags::DN_MATTER_RCAC_ID), *v)?,
A::FabricId(v) => writer.put_uint(Tag::Context(tags::DN_MATTER_FABRIC_ID), *v)?,
A::CaseAuthenticatedTag(v) => {
writer.put_uint(Tag::Context(tags::DN_MATTER_NOC_CAT), u64::from(*v))?;
}
A::VendorId(_) => return Err(Error::DnAttributeNotTlvEncodable("VendorId")),
A::ProductId(_) => return Err(Error::DnAttributeNotTlvEncodable("ProductId")),
A::Other { tag, value } => match value {
DnAttributeValue::Utf8(s) => writer.put_utf8(Tag::Context(*tag), s)?,
DnAttributeValue::Uint(v) => writer.put_uint(Tag::Context(*tag), *v)?,
DnAttributeValue::Bytes(b) => writer.put_bytes(Tag::Context(*tag), b)?,
},
}
Ok(())
}
const fn is_dn_tag(tag: u8) -> bool {
matches!(tag, 1..=26)
}
const fn is_untyped_dn_tag(tag: u8) -> bool {
matches!(tag, 18 | 23..=26)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
fn write_dn(dn: &DistinguishedName) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
dn.write(&mut w, Tag::Anonymous).unwrap();
buf
}
fn read_dn(bytes: &[u8]) -> DistinguishedName {
let mut r = TlvReader::new(bytes);
DistinguishedName::read(&mut r).unwrap()
}
#[test]
fn round_trip_common_name() {
let dn = DistinguishedName::new(vec![DnAttribute::CommonName("CN".into())]);
let bytes = write_dn(&dn);
assert_eq!(read_dn(&bytes), dn);
}
#[test]
fn round_trip_matter_node_id() {
let dn = DistinguishedName::new(vec![DnAttribute::NodeId(0xDEAD_BEEF_CAFE_BABE)]);
let bytes = write_dn(&dn);
assert_eq!(read_dn(&bytes), dn);
}
#[test]
fn round_trip_multiple_attributes_preserves_order() {
let dn = DistinguishedName::new(vec![
DnAttribute::FabricId(1),
DnAttribute::NodeId(2),
DnAttribute::CommonName("device".into()),
]);
let bytes = write_dn(&dn);
let parsed = read_dn(&bytes);
assert_eq!(parsed, dn);
assert!(matches!(
parsed.iter().next(),
Some(DnAttribute::FabricId(1))
));
}
#[test]
fn round_trip_other_attribute_for_tag_18() {
let dn = DistinguishedName::new(vec![DnAttribute::Other {
tag: 18,
value: DnAttributeValue::Uint(42),
}]);
let bytes = write_dn(&dn);
assert_eq!(read_dn(&bytes), dn);
}
#[test]
fn read_rejects_unknown_dn_tag() {
let mut buf = Vec::new();
{
let mut w = TlvWriter::new(&mut buf);
w.start_list(Tag::Anonymous).unwrap();
w.put_utf8(Tag::Context(100), "bogus").unwrap();
w.end_container().unwrap();
}
let mut r = TlvReader::new(&buf);
assert!(matches!(
DistinguishedName::read(&mut r),
Err(Error::InvalidDnAttribute(100))
));
}
#[test]
fn read_rejects_wrong_type_for_known_tag() {
let mut buf = Vec::new();
{
let mut w = TlvWriter::new(&mut buf);
w.start_list(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(tags::DN_COMMON_NAME), 42).unwrap();
w.end_container().unwrap();
}
let mut r = TlvReader::new(&buf);
assert!(matches!(
DistinguishedName::read(&mut r),
Err(Error::InvalidDnAttributeType(_))
));
}
#[test]
fn typed_accessors_return_expected_values() {
let dn = DistinguishedName::new(vec![
DnAttribute::FabricId(7),
DnAttribute::NodeId(42),
DnAttribute::CommonName("device-007".into()),
]);
assert_eq!(dn.node_id(), Some(42));
assert_eq!(dn.fabric_id(), Some(7));
assert_eq!(dn.common_name(), Some("device-007"));
assert_eq!(dn.rcac_id(), None);
}
}