Skip to main content

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