use std::fmt;
use thiserror::Error;
#[derive(Default, Clone, Debug)]
pub struct Dictionary {
pub attributes: Vec<DictionaryAttribute>,
pub values: Vec<DictionaryValue>,
pub vendors: Vec<DictionaryVendor>,
}
impl Dictionary {
pub fn merge(d1: &Dictionary, d2: &Dictionary) -> Result<Dictionary, DictionaryError> {
for attr in &d2.attributes {
if d1.attributes.iter().any(|a| a.name == attr.name) {
return Err(DictionaryError::Conflict(format!(
"duplicate attribute name: {}",
attr.name
)));
}
if d1.attributes.iter().any(|a| a.oid == attr.oid) {
return Err(DictionaryError::Conflict(format!(
"duplicate attribute OID: {}",
attr.oid
)));
}
}
for vendor in &d2.vendors {
let existing_by_name = d1.vendors.iter().find(|v| v.name == vendor.name);
let existing_by_code = d1.vendors.iter().find(|v| v.code == vendor.code);
if existing_by_name != existing_by_code {
return Err(DictionaryError::Conflict(format!(
"conflicting vendor definition: {} ({})",
vendor.name, vendor.code
)));
}
if let Some(existing) = existing_by_name {
for attr in &vendor.attributes {
if existing.attributes.iter().any(|a| a.name == attr.name) {
return Err(DictionaryError::Conflict(format!(
"duplicate vendor attribute name: {}",
attr.name
)));
}
if existing.attributes.iter().any(|a| a.oid == attr.oid) {
return Err(DictionaryError::Conflict(format!(
"duplicate vendor attribute OID: {}",
attr.oid
)));
}
}
}
}
let mut new_dict = d1.clone();
new_dict.attributes.extend(d2.attributes.clone());
new_dict.values.extend(d2.values.clone());
for v2 in &d2.vendors {
if let Some(v1) = new_dict.vendors.iter_mut().find(|v| v.code == v2.code) {
v1.attributes.extend(v2.attributes.clone());
v1.values.extend(v2.values.clone());
} else {
new_dict.vendors.push(v2.clone());
}
}
Ok(new_dict)
}
}
#[derive(Error, Debug)]
pub enum DictionaryError {
#[error("Dictionary conflict: {0}")]
Conflict(String),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum AttributeType {
String,
Integer,
IpAddr,
Octets,
Date,
Vsa,
Ether,
ABinary,
Byte,
Short,
Signed,
Tlv,
Ipv4Prefix,
Ifid,
Ipv6Addr,
Ipv6Prefix,
InterfaceId,
Unknown(String),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct DictionaryAttribute {
pub name: String,
pub oid: Oid,
pub attr_type: AttributeType,
pub size: SizeFlag,
pub encrypt: Option<u8>,
pub has_tag: Option<bool>,
pub concat: Option<bool>,
}
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct Oid {
pub vendor: Option<u32>,
pub code: u32,
}
impl fmt::Display for Oid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.vendor {
Some(v) => write!(f, "{}-{}", v, self.code),
None => write!(f, "{}", self.code),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub enum SizeFlag {
#[default]
Any,
Exact(u32),
Range(u32, u32),
}
impl SizeFlag {
pub fn is_constrained(&self) -> bool {
!matches!(self, SizeFlag::Any)
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct DictionaryValue {
pub attribute_name: String,
pub name: String,
pub value: u64,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct DictionaryVendor {
pub name: String,
pub code: u32,
pub attributes: Vec<DictionaryAttribute>,
pub values: Vec<DictionaryValue>,
}
#[cfg(test)]
mod tests {
use super::*;
fn mock_attr(name: &str, code: u32) -> DictionaryAttribute {
DictionaryAttribute {
name: name.to_string(),
oid: Oid { vendor: None, code },
attr_type: AttributeType::Integer,
size: SizeFlag::Any,
encrypt: None,
has_tag: None,
concat: None,
}
}
#[test]
fn test_merge_success() {
let mut d1 = Dictionary::default();
d1.attributes.push(mock_attr("User-Name", 1));
let mut d2 = Dictionary::default();
d2.attributes.push(mock_attr("Password", 2));
let merged = Dictionary::merge(&d1, &d2).unwrap();
assert_eq!(merged.attributes.len(), 2);
}
#[test]
fn test_merge_conflict_name() {
let mut d1 = Dictionary::default();
d1.attributes.push(mock_attr("User-Name", 1));
let mut d2 = Dictionary::default();
d2.attributes.push(mock_attr("User-Name", 2));
let result = Dictionary::merge(&d1, &d2);
assert!(matches!(result, Err(DictionaryError::Conflict(m)) if m.contains("name")));
}
#[test]
fn test_merge_conflict_oid() {
let mut d1 = Dictionary::default();
d1.attributes.push(mock_attr("User-Name", 1));
let mut d2 = Dictionary::default();
d2.attributes.push(mock_attr("Login-Name", 1));
let result = Dictionary::merge(&d1, &d2);
assert!(matches!(result, Err(DictionaryError::Conflict(m)) if m.contains("OID")));
}
#[test]
fn test_vendor_merge_and_conflict() {
let v1 = DictionaryVendor {
name: "Cisco".to_string(),
code: 9,
attributes: vec![mock_attr("Cisco-AVPair", 1)],
values: vec![],
};
let mut d1 = Dictionary::default();
d1.vendors.push(v1);
let v2 = DictionaryVendor {
name: "Cisco".to_string(),
code: 9,
attributes: vec![mock_attr("Cisco-Other", 2)],
values: vec![],
};
let mut d2 = Dictionary::default();
d2.vendors.push(v2);
let merged = Dictionary::merge(&d1, &d2).expect("Should merge vendor attributes");
assert_eq!(merged.vendors[0].attributes.len(), 2);
let v3 = DictionaryVendor {
name: "Cisco".to_string(),
code: 9,
attributes: vec![mock_attr("Cisco-Duplicate", 1)], values: vec![],
};
let mut d3 = Dictionary::default();
d3.vendors.push(v3);
let result = Dictionary::merge(&d1, &d3);
assert!(result.is_err());
}
#[test]
fn test_vendor_mismatch_definition() {
let mut d1 = Dictionary::default();
d1.vendors.push(DictionaryVendor {
name: "Cisco".to_string(),
code: 9,
attributes: vec![],
values: vec![],
});
let mut d2 = Dictionary::default();
d2.vendors.push(DictionaryVendor {
name: "Cisco".to_string(),
code: 10, attributes: vec![],
values: vec![],
});
let result = Dictionary::merge(&d1, &d2);
assert!(matches!(result, Err(DictionaryError::Conflict(_))));
}
}