abol_parser/
dictionary.rs

1use std::fmt;
2use thiserror::Error;
3
4/// Represents a complete RADIUS dictionary containing standard attributes,
5/// values for enumerated types, and vendor-specific definitions.
6#[derive(Default, Clone, Debug)]
7pub struct Dictionary {
8    /// List of standard (non-vendor) RADIUS attributes.
9    pub attributes: Vec<DictionaryAttribute>,
10    /// Enumerated value mappings for attributes (e.g., Service-Type values).
11    pub values: Vec<DictionaryValue>,
12    /// Vendor definitions including their unique VSAs.
13    pub vendors: Vec<DictionaryVendor>,
14}
15impl Dictionary {
16    /// Merges two dictionaries into a single combined dictionary.
17    ///
18    /// This performs strict validation to ensure there are no collisions between
19    /// attribute names, OIDs, or vendor definitions.
20    ///
21    /// # Errors
22    /// Returns `DictionaryError::Conflict` if:
23    /// * Standard attribute names or OIDs collide.
24    /// * Vendor IDs or names are inconsistent between dictionaries.
25    /// * Attributes within a specific vendor collide.
26    pub fn merge(d1: &Dictionary, d2: &Dictionary) -> Result<Dictionary, DictionaryError> {
27        for attr in &d2.attributes {
28            if d1.attributes.iter().any(|a| a.name == attr.name) {
29                return Err(DictionaryError::Conflict(format!(
30                    "duplicate attribute name: {}",
31                    attr.name
32                )));
33            }
34            if d1.attributes.iter().any(|a| a.oid == attr.oid) {
35                return Err(DictionaryError::Conflict(format!(
36                    "duplicate attribute OID: {}",
37                    attr.oid
38                )));
39            }
40        }
41
42        for vendor in &d2.vendors {
43            let existing_by_name = d1.vendors.iter().find(|v| v.name == vendor.name);
44            let existing_by_code = d1.vendors.iter().find(|v| v.code == vendor.code);
45
46            // If name exists but code is different, or code exists but name is different
47            if existing_by_name != existing_by_code {
48                return Err(DictionaryError::Conflict(format!(
49                    "conflicting vendor definition: {} ({})",
50                    vendor.name, vendor.code
51                )));
52            }
53
54            // If the vendor already exists, check for attribute collisions within that vendor
55            if let Some(existing) = existing_by_name {
56                for attr in &vendor.attributes {
57                    if existing.attributes.iter().any(|a| a.name == attr.name) {
58                        return Err(DictionaryError::Conflict(format!(
59                            "duplicate vendor attribute name: {}",
60                            attr.name
61                        )));
62                    }
63                    if existing.attributes.iter().any(|a| a.oid == attr.oid) {
64                        return Err(DictionaryError::Conflict(format!(
65                            "duplicate vendor attribute OID: {}",
66                            attr.oid
67                        )));
68                    }
69                }
70            }
71        }
72
73        let mut new_dict = d1.clone();
74
75        // Append top-level attributes and values
76        new_dict.attributes.extend(d2.attributes.clone());
77        new_dict.values.extend(d2.values.clone());
78
79        // Merge vendors
80        for v2 in &d2.vendors {
81            if let Some(v1) = new_dict.vendors.iter_mut().find(|v| v.code == v2.code) {
82                // Vendor exists: merge its attributes and values
83                v1.attributes.extend(v2.attributes.clone());
84                v1.values.extend(v2.values.clone());
85            } else {
86                new_dict.vendors.push(v2.clone());
87            }
88        }
89
90        Ok(new_dict)
91    }
92}
93#[derive(Error, Debug)]
94pub enum DictionaryError {
95    #[error("Dictionary conflict: {0}")]
96    Conflict(String),
97}
98#[derive(Debug, PartialEq, Eq, Clone)]
99pub enum AttributeType {
100    String,
101    Integer,
102    IpAddr,
103    Octets,
104    Date,
105    Vsa,
106    Ether,
107    ABinary,
108    Byte,
109    Short,
110    Signed,
111    Tlv,
112    Ipv4Prefix,
113    Ifid,
114    Ipv6Addr,
115    Ipv6Prefix,
116    InterfaceId,
117    //todo check unknown type and add all attributes
118    Unknown(String),
119}
120#[derive(Debug, PartialEq, Eq, Clone)]
121pub struct DictionaryAttribute {
122    pub name: String,
123    pub oid: Oid,
124    pub attr_type: AttributeType,
125    pub size: SizeFlag,
126    pub encrypt: Option<u8>,
127    pub has_tag: Option<bool>,
128    pub concat: Option<bool>,
129}
130/// The Object Identifier (OID) for a RADIUS attribute, consisting of
131/// an optional vendor ID and the attribute code.
132#[derive(Debug, PartialEq, Eq, Clone, Default)]
133pub struct Oid {
134    /// The SMI Private Enterprise Number (PEN). None for standard attributes.
135    pub vendor: Option<u32>,
136    /// The attribute type code (0-255 for standard, vendor-specific for VSAs).
137    pub code: u32,
138}
139
140impl fmt::Display for Oid {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match self.vendor {
143            Some(v) => write!(f, "{}-{}", v, self.code),
144            None => write!(f, "{}", self.code),
145        }
146    }
147}
148/// Flags representing size constraints on attribute values.
149#[derive(Debug, PartialEq, Eq, Clone, Default)]
150pub enum SizeFlag {
151    /// No constraint (default).
152    #[default]
153    Any,
154    /// Value must be exactly N bytes.
155    Exact(u32),
156    /// Value must be between N and M bytes (inclusive).
157    Range(u32, u32),
158}
159
160impl SizeFlag {
161    pub fn is_constrained(&self) -> bool {
162        !matches!(self, SizeFlag::Any)
163    }
164}
165/// A mapping between a string name and a numeric value for an attribute.
166#[derive(Debug, PartialEq, Eq, Clone)]
167pub struct DictionaryValue {
168    /// The name of the attribute this value belongs to.
169    pub attribute_name: String,
170    /// The name of the specific value (e.g., "Access-Request").
171    pub name: String,
172    /// The numeric representation of the value.
173    pub value: u64,
174}
175/// A RADIUS Vendor definition.
176#[derive(Debug, PartialEq, Eq, Clone)]
177pub struct DictionaryVendor {
178    /// The name of the vendor (e.g., "Cisco").
179    pub name: String,
180    /// The SMI Private Enterprise Number.
181    pub code: u32,
182    /// Attributes specific to this vendor.
183    pub attributes: Vec<DictionaryAttribute>,
184    /// Enumerated value mappings for this vendor's attributes.
185    pub values: Vec<DictionaryValue>,
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn mock_attr(name: &str, code: u32) -> DictionaryAttribute {
193        DictionaryAttribute {
194            name: name.to_string(),
195            oid: Oid { vendor: None, code },
196            attr_type: AttributeType::Integer,
197            size: SizeFlag::Any,
198            encrypt: None,
199            has_tag: None,
200            concat: None,
201        }
202    }
203
204    #[test]
205    fn test_merge_success() {
206        let mut d1 = Dictionary::default();
207        d1.attributes.push(mock_attr("User-Name", 1));
208
209        let mut d2 = Dictionary::default();
210        d2.attributes.push(mock_attr("Password", 2));
211
212        let merged = Dictionary::merge(&d1, &d2).unwrap();
213        assert_eq!(merged.attributes.len(), 2);
214    }
215
216    #[test]
217    fn test_merge_conflict_name() {
218        let mut d1 = Dictionary::default();
219        d1.attributes.push(mock_attr("User-Name", 1));
220
221        let mut d2 = Dictionary::default();
222        d2.attributes.push(mock_attr("User-Name", 2)); // Conflict on name
223
224        let result = Dictionary::merge(&d1, &d2);
225        assert!(matches!(result, Err(DictionaryError::Conflict(m)) if m.contains("name")));
226    }
227
228    #[test]
229    fn test_merge_conflict_oid() {
230        let mut d1 = Dictionary::default();
231        d1.attributes.push(mock_attr("User-Name", 1));
232
233        let mut d2 = Dictionary::default();
234        d2.attributes.push(mock_attr("Login-Name", 1)); // Conflict on OID
235
236        let result = Dictionary::merge(&d1, &d2);
237        assert!(matches!(result, Err(DictionaryError::Conflict(m)) if m.contains("OID")));
238    }
239
240    #[test]
241    fn test_vendor_merge_and_conflict() {
242        let v1 = DictionaryVendor {
243            name: "Cisco".to_string(),
244            code: 9,
245            attributes: vec![mock_attr("Cisco-AVPair", 1)],
246            values: vec![],
247        };
248
249        let mut d1 = Dictionary::default();
250        d1.vendors.push(v1);
251
252        // Case 1: Merge different attributes into same vendor
253        let v2 = DictionaryVendor {
254            name: "Cisco".to_string(),
255            code: 9,
256            attributes: vec![mock_attr("Cisco-Other", 2)],
257            values: vec![],
258        };
259        let mut d2 = Dictionary::default();
260        d2.vendors.push(v2);
261
262        let merged = Dictionary::merge(&d1, &d2).expect("Should merge vendor attributes");
263        assert_eq!(merged.vendors[0].attributes.len(), 2);
264
265        // Case 2: Conflict on vendor attribute OID
266        let v3 = DictionaryVendor {
267            name: "Cisco".to_string(),
268            code: 9,
269            attributes: vec![mock_attr("Cisco-Duplicate", 1)], // OID 1 already exists in d1's Cisco vendor
270            values: vec![],
271        };
272        let mut d3 = Dictionary::default();
273        d3.vendors.push(v3);
274
275        let result = Dictionary::merge(&d1, &d3);
276        assert!(result.is_err());
277    }
278
279    #[test]
280    fn test_vendor_mismatch_definition() {
281        let mut d1 = Dictionary::default();
282        d1.vendors.push(DictionaryVendor {
283            name: "Cisco".to_string(),
284            code: 9,
285            attributes: vec![],
286            values: vec![],
287        });
288
289        let mut d2 = Dictionary::default();
290        d2.vendors.push(DictionaryVendor {
291            name: "Cisco".to_string(),
292            code: 10, // Same name, different code
293            attributes: vec![],
294            values: vec![],
295        });
296
297        let result = Dictionary::merge(&d1, &d2);
298        assert!(matches!(result, Err(DictionaryError::Conflict(_))));
299    }
300}