netgauze_ipfix_code_generator/xml_parsers/
sub_registries.rs

1// Copyright (C) 2022-present The NetGauze Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12// implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::xml_parsers::xml_common::*;
17use crate::{
18    InformationElementSubRegistry, ReasonCodeNestedRegistry, SubRegistryType,
19    ValueNameDescRegistry, Xref,
20};
21
22use regex::Regex;
23use roxmltree::Node;
24use tracing::info;
25
26const MAX_WORDS_NAME: usize = 10;
27const MAX_CHARS_DISPLAY_NAME: usize = 50;
28
29/// Subregistry Trait with getter functions for common values
30pub trait SubRegistry {
31    fn name(&self) -> &str;
32    fn display_name(&self) -> &str;
33    fn description(&self) -> &str;
34    fn comments(&self) -> &Option<String>;
35    fn parameters(&self) -> &Option<String>;
36    fn xrefs(&self) -> &Vec<Xref>;
37}
38
39impl SubRegistry for ValueNameDescRegistry {
40    fn name(&self) -> &str {
41        &self.name
42    }
43
44    fn display_name(&self) -> &str {
45        &self.display_name
46    }
47
48    fn description(&self) -> &str {
49        &self.description
50    }
51
52    fn comments(&self) -> &Option<String> {
53        &self.comments
54    }
55
56    fn parameters(&self) -> &Option<String> {
57        &self.parameters
58    }
59
60    fn xrefs(&self) -> &Vec<Xref> {
61        &self.xrefs
62    }
63}
64
65impl SubRegistry for ReasonCodeNestedRegistry {
66    fn name(&self) -> &str {
67        &self.name
68    }
69
70    fn display_name(&self) -> &str {
71        &self.display_name
72    }
73
74    fn description(&self) -> &str {
75        &self.description
76    }
77
78    fn comments(&self) -> &Option<String> {
79        &self.comments
80    }
81
82    fn parameters(&self) -> &Option<String> {
83        &self.parameters
84    }
85
86    fn xrefs(&self) -> &Vec<Xref> {
87        &self.xrefs
88    }
89}
90
91/// Wrapper to call the appropriate sub-registry parsing function based on the
92/// registry_type. Returns a `Vec<InformationElementSubRegistry>`
93pub fn parse_subregistry(
94    node: &Node<'_, '_>,
95    registry_type: SubRegistryType,
96) -> (u16, Vec<InformationElementSubRegistry>) {
97    match registry_type {
98        SubRegistryType::ValueNameDescRegistry => {
99            let (ie_id, reg) = parse_val_name_desc_u8_registry(node);
100            let ie_subreg: Vec<InformationElementSubRegistry> = reg
101                .into_iter()
102                .map(InformationElementSubRegistry::ValueNameDescRegistry)
103                .collect();
104            (ie_id, ie_subreg)
105        }
106        SubRegistryType::ReasonCodeNestedRegistry => {
107            let (ie_id, reg) = parse_reason_code_nested_u8_registry_2bit(node);
108            let ie_subreg: Vec<InformationElementSubRegistry> = reg
109                .into_iter()
110                .map(InformationElementSubRegistry::ReasonCodeNestedRegistry)
111                .collect();
112            (ie_id, ie_subreg)
113        }
114    }
115}
116
117/// Parse generic sub-registries with value (or id), name and/or description,
118/// and optionally a comment, and parameter set. Examples:
119/// - [flowEndReason (Value 136)](https://www.iana.org/assignments/ipfix/ipfix.xhtml#ipfix-flow-end-reason)
120/// - [flowSelectorAlgorithm (Value 390)](https://www.iana.org/assignments/ipfix/ipfix.xhtml#ipfix-flowselectoralgorithm)
121pub fn parse_val_name_desc_u8_registry(node: &Node<'_, '_>) -> (u16, Vec<ValueNameDescRegistry>) {
122    let mut ret = Vec::new();
123
124    let children = node
125        .children()
126        .filter(|x| x.tag_name() == (IANA_NAMESPACE, "record").into())
127        .collect::<Vec<_>>();
128
129    let title = get_string_child(node, (IANA_NAMESPACE, "title").into()).unwrap_or_default();
130
131    let ie_id_regex = Regex::new(r"Value (\d+)").unwrap();
132    let ie_id = ie_id_regex
133        .captures(&title)
134        .and_then(|captures| captures.get(1))
135        .and_then(|capture| capture.as_str().parse().ok())
136        .unwrap_or(0);
137
138    for child in &children {
139        let value = get_string_child(child, (IANA_NAMESPACE, "value").into()).map(|x| {
140            if let Some(hex_value) = x.strip_prefix("0x") {
141                u8::from_str_radix(hex_value, 16)
142            } else if let Some(bin_value) = x.strip_prefix("0b") {
143                u8::from_str_radix(bin_value, 2)
144            } else if let Some(bin_value) = x.strip_suffix('b') {
145                u8::from_str_radix(bin_value, 2)
146            } else {
147                x.parse::<u8>()
148            }
149        });
150
151        // If value column is not present, fallback to id (e.g. psamp-parameters
152        // registry)
153        let value = match value {
154            Some(_) => value,
155            None => get_string_child(child, (IANA_NAMESPACE, "id").into()).map(|x| {
156                if let Some(hex_value) = x.strip_prefix("0x") {
157                    u8::from_str_radix(hex_value, 16)
158                } else if let Some(bin_value) = x.strip_prefix("0b") {
159                    u8::from_str_radix(bin_value, 2)
160                } else if let Some(bin_value) = x.strip_suffix('b') {
161                    u8::from_str_radix(bin_value, 2)
162                } else {
163                    x.parse::<u8>()
164                }
165            }),
166        };
167
168        let name_parsed = get_string_child(child, (IANA_NAMESPACE, "name").into());
169
170        // TODO: also consider unassigned and experimentation values
171        if Some(true)
172            == name_parsed
173                .as_ref()
174                .map(|x| x.as_str() == UNASSIGNED || x.contains(EXPERIMENTATION))
175        {
176            continue;
177        }
178
179        let description_parsed = parse_simple_description_string(child);
180        if Some(true)
181            == description_parsed
182                .as_ref()
183                .map(|x| x.as_str() == UNASSIGNED || x.contains(EXPERIMENTATION))
184        {
185            continue;
186        }
187
188        // Populate name, display_name, and description
189        // - name is always a usable enum variant type name (if not there in the
190        //   registry, take it from the description)
191        // - display_name matches the IANA registry name (apart from when name is
192        //   populated from description field)
193        let mut name: String;
194        let mut display_name: String;
195        let description: String;
196        if let Some(Ok(value)) = value {
197            if value == u8::MAX {
198                // TODO: also consider unassigned and experimentation values
199                continue;
200            }
201
202            if let Some(name_parsed) = name_parsed {
203                display_name = name_parsed.clone();
204
205                (_, name) = xml_string_to_enum_type(&name_parsed);
206                if let Some(desc_parsed) = description_parsed {
207                    description = desc_parsed;
208                } else {
209                    description = name_parsed;
210                }
211            } else if let Some(mut desc_parsed) = description_parsed {
212                description = desc_parsed.clone();
213
214                let desc_words_amount: usize;
215                (desc_words_amount, desc_parsed) = xml_string_to_enum_type(&desc_parsed);
216
217                if desc_words_amount < MAX_WORDS_NAME {
218                    display_name = desc_parsed.clone();
219                    name = desc_parsed;
220                } else {
221                    display_name = format!("Value{value}");
222                    name = format!("Value{value}");
223                }
224
225                if description.len() < MAX_CHARS_DISPLAY_NAME {
226                    display_name = description.clone();
227                }
228            } else {
229                info!("Skipping sub-registry: missing both name and description!");
230                continue;
231            }
232
233            // Handle duplicates
234            if name == *RESERVED || name == *PRIVATE {
235                name = format!("{name}{value}");
236            }
237
238            let comments = get_string_child(child, (IANA_NAMESPACE, "comments").into());
239            let parameters = get_string_child(child, (IANA_NAMESPACE, "parameters").into());
240            let xrefs = parse_xref(child);
241
242            ret.push(ValueNameDescRegistry {
243                value,
244                name,
245                display_name,
246                description,
247                comments,
248                parameters,
249                xrefs,
250            });
251        }
252    }
253
254    (ie_id, ret)
255}
256
257/// Parse sub-registries with nested (first 2bit = status) registries for reason
258/// code, such as: [Forwarding Status (Value 89)](https://www.iana.org/assignments/ipfix/ipfix.xml#forwarding-status)
259pub fn parse_reason_code_nested_u8_registry_2bit(
260    node: &Node<'_, '_>,
261) -> (u16, Vec<ReasonCodeNestedRegistry>) {
262    let (ie_id, subreg) = parse_val_name_desc_u8_registry(node);
263
264    let ret: Vec<ReasonCodeNestedRegistry> = subreg
265        .iter()
266        .map(|subreg| {
267            let val_bin_str = format!("{:02b}", subreg.value);
268            let reason_code_pattern = format!(r".*-{val_bin_str}b");
269            let reason_code_reg_pattern = Regex::new(&reason_code_pattern).unwrap();
270            let reason_code_node = find_node_by_regex(node, &reason_code_reg_pattern).unwrap();
271            ReasonCodeNestedRegistry {
272                value: subreg.value << 6,
273                name: SubRegistry::name(subreg).to_string(),
274                display_name: SubRegistry::display_name(subreg).to_string(),
275                description: SubRegistry::description(subreg).to_string(),
276                comments: SubRegistry::comments(subreg).to_owned(),
277                parameters: SubRegistry::parameters(subreg).to_owned(),
278                xrefs: SubRegistry::xrefs(subreg).to_owned(),
279                reason_code_reg: {
280                    let (_, reg) = parse_val_name_desc_u8_registry(&reason_code_node);
281                    let reg: Vec<InformationElementSubRegistry> = reg
282                        .into_iter()
283                        .map(InformationElementSubRegistry::ValueNameDescRegistry)
284                        .collect();
285                    reg
286                },
287            }
288        })
289        .collect();
290
291    (ie_id, ret)
292}