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