netgauze_ipfix_code_generator/
lib.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::{
17    generator::*,
18    xml_parsers::{
19        ipfix::{parse_iana_common_values, parse_information_elements, ID_IE},
20        xml_common::find_node_by_id,
21    },
22};
23use std::{collections::HashMap, ffi::OsString, fs, path::Path};
24use thiserror::Error;
25use xml_parsers::sub_registries::parse_subregistry;
26
27mod generator;
28mod generator_aggregation;
29mod generator_sub_registries;
30
31pub mod xml_parsers {
32    pub mod ipfix;
33    pub mod sub_registries;
34    pub mod xml_common;
35}
36
37const APP_USER_AGENT: &str = "curl/7.79.1";
38const GENERATED_VENDOR_MAIN_SUFFIX: &str = "generated.rs";
39const GENERATED_VENDOR_DESER_SUFFIX: &str = "deser_generated.rs";
40const GENERATED_VENDOR_SER_SUFFIX: &str = "ser_generated.rs";
41
42/// Represent Information Element as read form a registry
43#[derive(Debug, Clone)]
44pub struct InformationElement {
45    pub pen: u32,
46    pub name: String,
47    pub data_type: String,
48    pub group: Option<String>,
49    pub data_type_semantics: Option<String>,
50    pub element_id: u16,
51    pub applicability: Option<String>,
52    pub status: String,
53    pub description: String,
54    pub revision: u32,
55    pub date: String,
56    pub references: Option<String>,
57    pub xrefs: Vec<Xref>,
58    pub units: Option<String>,
59    pub range: Option<String>,
60    pub subregistry: Option<Vec<InformationElementSubRegistry>>,
61}
62
63/// There could be different types of subregistries that require a different
64/// parsing and/or code generator.
65#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
66pub enum SubRegistryType {
67    /// Simple sub-registries with Value and (Name and/or Description)
68    /// plus optional comment, parameters, xrefs, such as:
69    /// [flowEndReason (Value 136)](https://www.iana.org/assignments/ipfix/ipfix.xhtml#ipfix-flow-end-reason)
70    ValueNameDescRegistry,
71    /// Sub-registries with nested registries for reason code, such as:
72    /// [Forwarding Status (Value 89)](https://www.iana.org/assignments/ipfix/ipfix.xml#forwarding-status)
73    ReasonCodeNestedRegistry,
74}
75
76/// Abstracts Information Element sub-registries types
77#[derive(Debug, Clone)]
78pub enum InformationElementSubRegistry {
79    ValueNameDescRegistry(ValueNameDescRegistry),
80    ReasonCodeNestedRegistry(ReasonCodeNestedRegistry),
81}
82
83/// Describes simple sub-registries with Value and (Name and/or Description)
84/// plus optional comment, parameters, xrefs, such as:
85/// [flowEndReason (Value 136)](https://www.iana.org/assignments/ipfix/ipfix.xhtml#ipfix-flow-end-reason)
86#[derive(Debug, Clone, Default)]
87pub struct ValueNameDescRegistry {
88    pub value: u8,
89    pub name: String,
90    pub description: String,
91    pub comments: Option<String>,
92    pub parameters: Option<String>,
93    pub xrefs: Vec<Xref>,
94}
95
96/// Describes sub-registries with nested registries for reason code, such as:
97/// [Forwarding Status (Value 89)](https://www.iana.org/assignments/ipfix/ipfix.xml#forwarding-status)
98#[derive(Debug, Clone, Default)]
99pub struct ReasonCodeNestedRegistry {
100    pub value: u8,
101    pub name: String,
102    pub description: String,
103    pub comments: Option<String>,
104    pub parameters: Option<String>,
105    pub xrefs: Vec<Xref>,
106    pub reason_code_reg: Vec<InformationElementSubRegistry>,
107}
108
109/// Describes simple registries such as
110/// [IPFIX Information Element Data Types](https://www.iana.org/assignments/ipfix/ipfix.xml#ipfix-information-element-data-types)
111/// And [IPFIX Information Element Semantics](https://www.iana.org/assignments/ipfix/ipfix.xhtml#ipfix-information-element-semantics)
112#[derive(Debug)]
113pub struct SimpleRegistry {
114    pub value: u8,
115    pub description: String,
116    pub comments: Option<String>,
117    pub xref: Vec<Xref>,
118}
119
120/// Describes `<xref>` tag to link to a resource
121#[derive(Debug, Clone)]
122pub struct Xref {
123    pub ty: String,
124    pub data: String,
125}
126
127/// From where to pull the IPFIX definitions
128#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
129pub enum RegistrySource {
130    /// The registry data is directly encoded here
131    String(String),
132
133    /// Pull from an HTTP URL
134    Http(String),
135
136    /// Pull from a file accessible on the local filesystem.
137    File(String),
138}
139
140/// IPFIX can be defined in multiple ways
141#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
142pub enum RegistryType {
143    /// Use the IANA format as used in [IANA Flow IE](https://www.iana.org/assignments/ipfix/ipfix.xml)
144    /// and defined by the schema [IANA Schema](https://www.iana.org/assignments/ipfix/ipfix.rng)
145    IanaXML,
146}
147
148/// Configuration for a single IPFIX Flow IE entities definition
149/// Could be the main IANA registry or a vendor specific source
150#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
151pub struct SourceConfig {
152    source: RegistrySource,
153    registry_type: RegistryType,
154    /// Private Enterprise Number
155    pen: u32,
156    /// rust sub-module name under which the IPFIX information will be generated
157    mod_name: String,
158    /// Name use for various top-level enums (use rust CamelCase convention)
159    name: String,
160    /// External IANA subregistries for the IEs
161    ext_subregs_source: Option<Vec<ExternalSubRegistrySource>>,
162}
163
164impl SourceConfig {
165    pub const fn new(
166        source: RegistrySource,
167        registry_type: RegistryType,
168        pen: u32,
169        mod_name: String,
170        name: String,
171        ext_subregs_source: Option<Vec<ExternalSubRegistrySource>>,
172    ) -> Self {
173        Self {
174            source,
175            registry_type,
176            pen,
177            mod_name,
178            name,
179            ext_subregs_source,
180        }
181    }
182
183    pub const fn source(&self) -> &RegistrySource {
184        &self.source
185    }
186
187    pub const fn registry_type(&self) -> &RegistryType {
188        &self.registry_type
189    }
190
191    pub const fn pen(&self) -> u32 {
192        self.pen
193    }
194
195    pub const fn mod_name(&self) -> &String {
196        &self.mod_name
197    }
198
199    pub const fn name(&self) -> &String {
200        &self.name
201    }
202
203    pub const fn ext_subregs_source(&self) -> &Option<Vec<ExternalSubRegistrySource>> {
204        &self.ext_subregs_source
205    }
206}
207
208/// From where to pull the external sub-registries
209#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
210pub struct ExternalSubRegistrySource {
211    source: RegistrySource,
212    // Subregistry type to be used for parsing
213    registry_type: SubRegistryType,
214    // ID which identifies the registry in the XML document
215    registry_id: String,
216    // Information Element ID to which the SubRegistry is referencing
217    ie_id: u16,
218}
219
220impl ExternalSubRegistrySource {
221    pub const fn new(
222        source: RegistrySource,
223        registry_type: SubRegistryType,
224        registry_id: String,
225        ie_id: u16,
226    ) -> Self {
227        Self {
228            source,
229            registry_type,
230            registry_id,
231            ie_id,
232        }
233    }
234
235    pub const fn registry_type(&self) -> &SubRegistryType {
236        &self.registry_type
237    }
238}
239
240/// Configuration to generate IPFIX/Netflow entities
241#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
242pub struct Config {
243    iana: SourceConfig,
244    vendors: Vec<SourceConfig>,
245}
246
247impl Config {
248    pub const fn new(iana: SourceConfig, vendors: Vec<SourceConfig>) -> Self {
249        Self { iana, vendors }
250    }
251
252    pub const fn iana(&self) -> &SourceConfig {
253        &self.iana
254    }
255
256    pub const fn vendors(&self) -> &Vec<SourceConfig> {
257        &self.vendors
258    }
259}
260
261#[derive(Error, Debug)]
262pub enum GetStringSourceError {
263    #[error("http request error")]
264    HttpError(#[from] reqwest::Error),
265    #[error("reading data from filesystem error")]
266    StdIoError(#[from] std::io::Error),
267}
268
269/// Get the data from an XML source, and return the root node
270pub fn get_string_source(source: &RegistrySource) -> Result<String, GetStringSourceError> {
271    let str = match source {
272        RegistrySource::String(xml_string) => xml_string.clone(),
273        RegistrySource::Http(url) => {
274            let client = reqwest::blocking::ClientBuilder::new()
275                .user_agent(APP_USER_AGENT)
276                .build()?;
277            let resp = client.get(url).send()?;
278            resp.text()?
279        }
280        RegistrySource::File(path) => fs::read_to_string(path)?,
281    };
282    Ok(str)
283}
284
285#[derive(Error, Debug)]
286pub enum GenerateIanaConfigError {
287    #[error("writing generated code to filesystem error")]
288    StdIoError(#[from] std::io::Error),
289
290    #[error("error getting registry data from the source")]
291    SourceError(#[from] GetStringSourceError),
292
293    #[error("error parsing xml data from the given source")]
294    XmlParsingError(#[from] roxmltree::Error),
295
296    #[error("registry type is not supported")]
297    UnsupportedRegistryType(RegistryType),
298}
299
300/// Specifically generate the IANA configs, unlike vendor specific registries,
301/// IANA generate more types related to the IPFIX protocol itself
302fn generate_vendor_ie(
303    out_dir: &OsString,
304    config: &SourceConfig,
305) -> Result<(), GenerateIanaConfigError> {
306    if config.registry_type != RegistryType::IanaXML {
307        return Err(GenerateIanaConfigError::UnsupportedRegistryType(
308            config.registry_type.clone(),
309        ));
310    }
311    let ipfix_xml_string = get_string_source(&config.source)?;
312    let ipfix_xml_doc = roxmltree::Document::parse(ipfix_xml_string.as_str())?;
313    let ipfix_root = ipfix_xml_doc.root();
314
315    // Parse any external sub-registries provided:
316    let mut ext_subregs: HashMap<u16, Vec<InformationElementSubRegistry>> = HashMap::new();
317    if let Some(ext_subregs_source) = &config.ext_subregs_source {
318        for subreg in ext_subregs_source {
319            let subreg_xml_string = get_string_source(&subreg.source)?;
320            let subreg_xml_doc = roxmltree::Document::parse(subreg_xml_string.as_str())?;
321            let subreg_root = subreg_xml_doc.root();
322
323            let subreg_node = find_node_by_id(&subreg_root, &subreg.registry_id).unwrap();
324            ext_subregs.insert(
325                subreg.ie_id,
326                parse_subregistry(&subreg_node, subreg.registry_type).1,
327            );
328        }
329    }
330
331    let ipfix_ie_node = find_node_by_id(&ipfix_root, ID_IE).unwrap();
332    let ie_parsed = parse_information_elements(&ipfix_ie_node, config.pen, ext_subregs);
333
334    let ie_generated = generate_information_element_ids(&ie_parsed);
335
336    let deser_generated = generate_pkg_ie_deserializers(config.mod_name.as_str(), &ie_parsed);
337    let ser_generated = generate_pkg_ie_serializers(config.mod_name.as_str(), &ie_parsed);
338
339    let mut output = String::new();
340    output.push_str(ie_generated.as_str());
341    output.push_str("\n\n");
342
343    output.push_str(generate_ie_values(&ie_parsed, Some(config.name().clone())).as_str());
344    output.push_str(generate_fields_enum(&ie_parsed).as_str());
345
346    output.push_str(generate_flat_ie_struct(&ie_parsed, &vec![]).as_str());
347
348    let dest_path = Path::new(&out_dir).join(format!(
349        "{}_{}",
350        config.mod_name, GENERATED_VENDOR_MAIN_SUFFIX
351    ));
352    fs::write(dest_path, output)?;
353
354    let deser_dest_path = Path::new(&out_dir).join(format!(
355        "{}_{}",
356        config.mod_name, GENERATED_VENDOR_DESER_SUFFIX
357    ));
358    fs::write(deser_dest_path, deser_generated)?;
359
360    let ser_dest_path = Path::new(&out_dir).join(format!(
361        "{}_{}",
362        config.mod_name, GENERATED_VENDOR_SER_SUFFIX
363    ));
364    fs::write(ser_dest_path, ser_generated)?;
365
366    Ok(())
367}
368
369#[derive(Error, Debug)]
370pub enum GenerateError {
371    #[error("writing generated code to filesystem error")]
372    StdIoError(#[from] std::io::Error),
373
374    #[error("error in generating IANA configs")]
375    GenerateIanaConfigError(#[from] GenerateIanaConfigError),
376
377    #[error("error getting registry data from the source")]
378    SourceError(#[from] GetStringSourceError),
379
380    #[error("error parsing xml data from the given source")]
381    XmlParsingError(#[from] roxmltree::Error),
382
383    #[error("registry type is not supported")]
384    UnsupportedRegistryType(RegistryType),
385}
386
387pub fn generate(out_dir: &OsString, config: &Config) -> Result<(), GenerateError> {
388    let mut ie_output = String::new();
389    ie_output.push_str(generate_common_types().as_str());
390    ie_output.push_str(generate_ie_status().as_str());
391
392    // Start parsing the IANA registry
393    let ipfix_xml_string = get_string_source(&config.iana.source)?;
394    let ipfix_xml_doc = roxmltree::Document::parse(ipfix_xml_string.as_str())?;
395    let iana_ipfix_root = ipfix_xml_doc.root();
396
397    let (_data_types_parsed, semantics_parsed, units_parsed) =
398        parse_iana_common_values(&iana_ipfix_root);
399    let semantics_generated = generate_ie_semantics(&semantics_parsed);
400    let units_generated = generate_ie_units(&units_parsed);
401    ie_output.push_str(semantics_generated.as_str());
402    ie_output.push_str(units_generated.as_str());
403
404    // Parse any external sub-registries provided:
405    let mut ext_subregs: HashMap<u16, Vec<InformationElementSubRegistry>> = HashMap::new();
406    if let Some(ext_subregs_source) = &config.iana.ext_subregs_source {
407        for subreg in ext_subregs_source {
408            let subreg_xml_string = get_string_source(&subreg.source)?;
409            let subreg_xml_doc = roxmltree::Document::parse(subreg_xml_string.as_str())?;
410            let subreg_root = subreg_xml_doc.root();
411
412            let subreg_node = find_node_by_id(&subreg_root, &subreg.registry_id).unwrap();
413            ext_subregs.insert(
414                subreg.ie_id,
415                parse_subregistry(&subreg_node, subreg.registry_type).1,
416            );
417        }
418    }
419
420    let iana_ipfix_ie_node = find_node_by_id(&iana_ipfix_root, ID_IE).unwrap();
421    let iana_ie_parsed = parse_information_elements(&iana_ipfix_ie_node, 0, ext_subregs);
422
423    let mut vendors = vec![];
424    for vendor in &config.vendors {
425        vendors.push((vendor.name.clone(), vendor.mod_name.clone(), vendor.pen));
426        generate_vendor_ie(out_dir, vendor)?;
427    }
428
429    // Generate IANA IE and reference to vendor specific IEs
430    ie_output.push_str(generate_ie_ids(&iana_ie_parsed, &vendors).as_str());
431    ie_output.push_str(generate_ie_values(&iana_ie_parsed, None).as_str());
432
433    let mut ie_deser = String::new();
434    let mut ie_ser = String::new();
435    ie_deser.push_str("use crate::ie::*;\n\n");
436    ie_ser.push_str("use crate::ie::*;\n\n");
437
438    for vendor in &config.vendors {
439        ie_output.push_str(
440            format!(
441                "pub mod {} {{include!(concat!(env!(\"OUT_DIR\"), \"/{}_{}\"));}}\n\n",
442                vendor.mod_name(),
443                vendor.mod_name(),
444                GENERATED_VENDOR_MAIN_SUFFIX
445            )
446            .as_str(),
447        );
448        ie_deser.push_str(
449            format!(
450                "pub mod {} {{include!(concat!(env!(\"OUT_DIR\"), \"/{}_{}\"));}}\n\n",
451                vendor.mod_name(),
452                vendor.mod_name(),
453                GENERATED_VENDOR_DESER_SUFFIX
454            )
455            .as_str(),
456        );
457        ie_ser.push_str(
458            format!(
459                "pub mod {} {{include!(concat!(env!(\"OUT_DIR\"), \"/{}_{}\"));}}\n\n",
460                vendor.mod_name(),
461                vendor.mod_name(),
462                GENERATED_VENDOR_SER_SUFFIX
463            )
464            .as_str(),
465        );
466    }
467
468    ie_output.push_str(generate_flat_ie_struct(&iana_ie_parsed, &vendors).as_str());
469    ie_deser.push_str(generate_ie_deser_main(&iana_ie_parsed, &vendors).as_str());
470    ie_ser.push_str(generate_ie_ser_main(&iana_ie_parsed, &vendors).as_str());
471
472    let ie_dest_path = Path::new(&out_dir).join("ie_generated.rs");
473    fs::write(ie_dest_path, ie_output)?;
474
475    let ie_deser_dest_path = Path::new(&out_dir).join("ie_deser_generated.rs");
476    fs::write(ie_deser_dest_path, ie_deser)?;
477
478    let ie_ser_dest_path = Path::new(&out_dir).join("ie_ser_generated.rs");
479    fs::write(ie_ser_dest_path, ie_ser)?;
480    Ok(())
481}