From 88f9a9f54769c51c446abe214ea617f417933c2e Mon Sep 17 00:00:00 2001 From: mikemiles-dev Date: Sun, 15 Mar 2026 15:58:46 -0500 Subject: [PATCH 1/4] fix: deep code review pass 33 - 6 issues fixed - V9 lookup test coverage extended from 1..=282 to 1..=323 (41 untested field IDs) - Enterprise bit extraction uses bitmask (& 0x7FFF) instead of overflowing_sub per RFC 7011 - Data::with_template_field_lengths validates field count matches record length - TcpControlBits serialization uses explicit mask (val & 0xFF) instead of silent truncation - RELEASES.md: V9 field count corrected from 46 to 48 - RELEASES.md: added missing InvalidPendingTotalBytes ConfigError variant --- RELEASES.md | 4 +- src/variable_versions/field_value.rs | 2 +- src/variable_versions/ipfix/types.rs | 12 +++++- src/variable_versions/v9/lookup.rs | 4 +- ...ests__it_tests_field_data_type_lookup.snap | 41 +++++++++++++++++++ ...9_lookup_tests__it_tests_field_lookup.snap | 41 +++++++++++++++++++ 6 files changed, 98 insertions(+), 6 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 2fb824e..ed460ac 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -95,7 +95,7 @@ - `NetflowPacketError` and `NetflowParseError` type aliases — use `NetflowError` directly * **New enum variants (exhaustive match impact)** - - `ConfigError` gains `InvalidAllowedVersion(u16)`, `InvalidFieldCount(usize)`, `InvalidTemplateTotalSize(usize)`, `InvalidEntriesPerTemplate(usize)`, `InvalidEntrySize(usize)`, `InvalidTtlDuration`, `EmptyAllowedVersions` + - `ConfigError` gains `InvalidAllowedVersion(u16)`, `InvalidFieldCount(usize)`, `InvalidTemplateTotalSize(usize)`, `InvalidEntriesPerTemplate(usize)`, `InvalidEntrySize(usize)`, `InvalidTtlDuration`, `EmptyAllowedVersions`, `InvalidPendingTotalBytes { max_total_bytes, max_entry_size_bytes }` * **`RouterScopedParser::iter_packets_from_source` and `AutoScopedParser::iter_packets_from_source` now return `Result`** - Return type changed from `impl Iterator` to `Result` @@ -175,7 +175,7 @@ - `field_types` module is designed for future custom field type additions * **New V9 field types (IDs 128-175)** - - Added 46 new `V9Field` variants from the IANA IPFIX Information Elements registry + - Added 48 new `V9Field` variants from the IANA IPFIX Information Elements registry - Includes: `BgpNextAdjacentAsNumber`, `ExporterIpv4Address`, `ExporterIpv6Address`, `DroppedOctetDeltaCount`, `FlowEndReason`, `WlanSsid`, `FlowStartSeconds`, `FlowEndSeconds`, `FlowStartMicroseconds`, `FlowEndMicroseconds`, `FlowStartNanoseconds`, `FlowEndNanoseconds`, `DestinationIpv6Prefix`, `SourceIpv6Prefix`, and more - Each field has the correct `FieldDataType` mapping per the IANA registry diff --git a/src/variable_versions/field_value.rs b/src/variable_versions/field_value.rs index 4723168..d885010 100644 --- a/src/variable_versions/field_value.rs +++ b/src/variable_versions/field_value.rs @@ -691,7 +691,7 @@ impl FieldValue { FieldValue::TcpControlBits(t, w) => { let val = u16::from(*t); if *w == 1 { - buf.push(val as u8); + buf.push((val & 0xFF) as u8); } else { buf.extend_from_slice(&val.to_be_bytes()); } diff --git a/src/variable_versions/ipfix/types.rs b/src/variable_versions/ipfix/types.rs index 7b66328..a59d21c 100644 --- a/src/variable_versions/ipfix/types.rs +++ b/src/variable_versions/ipfix/types.rs @@ -210,6 +210,16 @@ impl Data { fields: Vec, template_field_lengths: Vec, ) -> Self { + if !fields.is_empty() && !template_field_lengths.is_empty() { + let record_len = fields[0].len(); + assert_eq!( + template_field_lengths.len(), + record_len, + "template_field_lengths length ({}) must match record field count ({})", + template_field_lengths.len(), + record_len, + ); + } Self { fields, padding: vec![], @@ -300,7 +310,7 @@ pub struct Template { pub struct TemplateField { #[nom( PostExec = "let (field_type_number, is_enterprise) = if field_type_number > 32767 { - (field_type_number.overflowing_sub(32768).0, true) + (field_type_number & 0x7FFF, true) } else { (field_type_number, false) };" )] pub field_type_number: u16, diff --git a/src/variable_versions/v9/lookup.rs b/src/variable_versions/v9/lookup.rs index e006be7..22aaaab 100644 --- a/src/variable_versions/v9/lookup.rs +++ b/src/variable_versions/v9/lookup.rs @@ -762,7 +762,7 @@ mod v9_lookup_tests { #[test] fn it_tests_field_lookup() { let mut fields = vec![]; - for i in 1..=282 { + for i in 1..=323 { let field: V9Field = i.into(); fields.push(field); } @@ -772,7 +772,7 @@ mod v9_lookup_tests { #[test] fn it_tests_field_data_type_lookup() { let mut fields: Vec = vec![]; - for i in 1..=282 { + for i in 1..=323 { let field: V9Field = i.into(); fields.push(field.into()); } diff --git a/src/variable_versions/v9/snapshots/netflow_parser__variable_versions__v9__lookup__v9_lookup_tests__it_tests_field_data_type_lookup.snap b/src/variable_versions/v9/snapshots/netflow_parser__variable_versions__v9__lookup__v9_lookup_tests__it_tests_field_data_type_lookup.snap index b804e85..6007e11 100644 --- a/src/variable_versions/v9/snapshots/netflow_parser__variable_versions__v9__lookup__v9_lookup_tests__it_tests_field_data_type_lookup.snap +++ b/src/variable_versions/v9/snapshots/netflow_parser__variable_versions__v9__lookup__v9_lookup_tests__it_tests_field_data_type_lookup.snap @@ -284,3 +284,44 @@ expression: fields - Unknown - Ip6Addr - Ip6Addr +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- Unknown +- DurationMillis diff --git a/src/variable_versions/v9/snapshots/netflow_parser__variable_versions__v9__lookup__v9_lookup_tests__it_tests_field_lookup.snap b/src/variable_versions/v9/snapshots/netflow_parser__variable_versions__v9__lookup__v9_lookup_tests__it_tests_field_lookup.snap index 3494d1f..4f75b5e 100644 --- a/src/variable_versions/v9/snapshots/netflow_parser__variable_versions__v9__lookup__v9_lookup_tests__it_tests_field_lookup.snap +++ b/src/variable_versions/v9/snapshots/netflow_parser__variable_versions__v9__lookup__v9_lookup_tests__it_tests_field_lookup.snap @@ -284,3 +284,44 @@ expression: fields - IanaAssigned: 280 - PostNATSourceIpv6Address - PostNATDestinationIpv6Address +- IanaAssigned: 283 +- IanaAssigned: 284 +- IanaAssigned: 285 +- IanaAssigned: 286 +- IanaAssigned: 287 +- IanaAssigned: 288 +- IanaAssigned: 289 +- IanaAssigned: 290 +- IanaAssigned: 291 +- IanaAssigned: 292 +- IanaAssigned: 293 +- IanaAssigned: 294 +- IanaAssigned: 295 +- IanaAssigned: 296 +- IanaAssigned: 297 +- IanaAssigned: 298 +- IanaAssigned: 299 +- IanaAssigned: 300 +- IanaAssigned: 301 +- IanaAssigned: 302 +- IanaAssigned: 303 +- IanaAssigned: 304 +- IanaAssigned: 305 +- IanaAssigned: 306 +- IanaAssigned: 307 +- IanaAssigned: 308 +- IanaAssigned: 309 +- IanaAssigned: 310 +- IanaAssigned: 311 +- IanaAssigned: 312 +- IanaAssigned: 313 +- IanaAssigned: 314 +- IanaAssigned: 315 +- IanaAssigned: 316 +- IanaAssigned: 317 +- IanaAssigned: 318 +- IanaAssigned: 319 +- IanaAssigned: 320 +- IanaAssigned: 321 +- IanaAssigned: 322 +- ObservationTimeMilliseconds From c111b1c4ed4f6ffd9f71fa3b0bcc2649f4dfba49 Mon Sep 17 00:00:00 2001 From: mikemiles-dev Date: Sun, 15 Mar 2026 16:19:49 -0500 Subject: [PATCH 2/4] fix: move more stuff under variable versions --- src/lib.rs | 17 +- src/netflow_common.rs | 793 ++++++------------ .../field_types/firewall_event.rs | 0 .../field_types/flow_end_reason.rs | 0 .../field_types/forwarding_status.rs | 0 .../field_types/fragment_flags.rs | 0 .../field_types/ipv4_options.rs | 0 .../field_types/ipv6_extension_headers.rs | 0 .../field_types/is_multicast.rs | 0 .../field_types/mod.rs | 0 .../field_types/mpls_label_exp.rs | 0 .../field_types/mpls_top_label_type.rs | 0 .../field_types/nat_event.rs | 0 .../nat_originating_address_realm.rs | 0 ...all_event_tests__all_firewall_events.snap} | 2 +- ...d_reason_tests__all_flow_end_reasons.snap} | 2 +- ...tatus_tests__all_forwarding_statuses.snap} | 2 +- ...ment_flags_tests__all_fragment_flags.snap} | 2 +- ...type_tests__all_mpls_top_label_types.snap} | 2 +- ...ent__nat_event_tests__all_nat_events.snap} | 2 +- ...control_bits_tests__all_single_flags.snap} | 2 +- .../field_types/tcp_control_bits.rs | 0 .../field_types/tcp_options.rs | 0 src/variable_versions/field_value.rs | 102 ++- src/variable_versions/mod.rs | 2 + .../template_events.rs | 0 26 files changed, 382 insertions(+), 546 deletions(-) rename src/{ => variable_versions}/field_types/firewall_event.rs (100%) rename src/{ => variable_versions}/field_types/flow_end_reason.rs (100%) rename src/{ => variable_versions}/field_types/forwarding_status.rs (100%) rename src/{ => variable_versions}/field_types/fragment_flags.rs (100%) rename src/{ => variable_versions}/field_types/ipv4_options.rs (100%) rename src/{ => variable_versions}/field_types/ipv6_extension_headers.rs (100%) rename src/{ => variable_versions}/field_types/is_multicast.rs (100%) rename src/{ => variable_versions}/field_types/mod.rs (100%) rename src/{ => variable_versions}/field_types/mpls_label_exp.rs (100%) rename src/{ => variable_versions}/field_types/mpls_top_label_type.rs (100%) rename src/{ => variable_versions}/field_types/nat_event.rs (100%) rename src/{ => variable_versions}/field_types/nat_originating_address_realm.rs (100%) rename src/{field_types/snapshots/netflow_parser__field_types__firewall_event__firewall_event_tests__all_firewall_events.snap => variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__firewall_event__firewall_event_tests__all_firewall_events.snap} (70%) rename src/{field_types/snapshots/netflow_parser__field_types__flow_end_reason__flow_end_reason_tests__all_flow_end_reasons.snap => variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__flow_end_reason__flow_end_reason_tests__all_flow_end_reasons.snap} (74%) rename src/{field_types/snapshots/netflow_parser__field_types__forwarding_status__forwarding_status_tests__all_forwarding_statuses.snap => variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__forwarding_status__forwarding_status_tests__all_forwarding_statuses.snap} (98%) rename src/{field_types/snapshots/netflow_parser__field_types__fragment_flags__fragment_flags_tests__all_fragment_flags.snap => variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__fragment_flags__fragment_flags_tests__all_fragment_flags.snap} (91%) rename src/{field_types/snapshots/netflow_parser__field_types__mpls_top_label_type__mpls_top_label_type_tests__all_mpls_top_label_types.snap => variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__mpls_top_label_type__mpls_top_label_type_tests__all_mpls_top_label_types.snap} (81%) rename src/{field_types/snapshots/netflow_parser__field_types__nat_event__nat_event_tests__all_nat_events.snap => variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__nat_event__nat_event_tests__all_nat_events.snap} (88%) rename src/{field_types/snapshots/netflow_parser__field_types__tcp_control_bits__tcp_control_bits_tests__all_single_flags.snap => variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__tcp_control_bits__tcp_control_bits_tests__all_single_flags.snap} (94%) rename src/{ => variable_versions}/field_types/tcp_control_bits.rs (100%) rename src/{ => variable_versions}/field_types/tcp_options.rs (100%) rename src/{ => variable_versions}/template_events.rs (100%) diff --git a/src/lib.rs b/src/lib.rs index 5585f58..d17c839 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,24 @@ #![forbid(unsafe_code)] #![doc = include_str!("../README.md")] -pub mod field_types; +#[deprecated(since = "1.0.0", note = "moved to `variable_versions::field_types`")] +pub mod field_types { + //! Deprecated re-export — use [`super::variable_versions::field_types`] instead. + pub use super::variable_versions::field_types::*; +} #[cfg(feature = "netflow_common")] pub mod netflow_common; pub mod protocol; pub mod scoped_parser; pub mod static_versions; -pub mod template_events; +#[deprecated( + since = "1.0.0", + note = "moved to `variable_versions::template_events`" +)] +pub mod template_events { + //! Deprecated re-export — use [`super::variable_versions::template_events`] instead. + pub use super::variable_versions::template_events::*; +} mod tests; pub mod variable_versions; @@ -44,7 +55,7 @@ pub use scoped_parser::{ }; // Re-export template event types for convenience -pub use template_events::{ +pub use variable_versions::template_events::{ TemplateEvent, TemplateHook, TemplateHookError, TemplateHooks, TemplateProtocol, }; diff --git a/src/netflow_common.rs b/src/netflow_common.rs index c7da3d7..9bb0737 100644 --- a/src/netflow_common.rs +++ b/src/netflow_common.rs @@ -5,7 +5,7 @@ use serde::Serialize; use crate::NetflowPacket; use crate::protocol::ProtocolTypes; use crate::static_versions::{v5::V5, v7::V7}; -use crate::variable_versions::field_value::{DurationValue, FieldValue}; +use crate::variable_versions::field_value::FieldValue; use crate::variable_versions::ipfix::lookup::{IANAIPFixField, IPFixField}; use crate::variable_versions::v9::lookup::V9Field; use crate::variable_versions::{ @@ -302,13 +302,12 @@ impl From<&V7> for NetflowCommon { } } -/// Helper structure to store all found V9 fields in a single pass -#[derive(Copy, Clone)] -struct V9FieldCache<'a> { - src_addr_v4: Option<&'a FieldValue>, - src_addr_v6: Option<&'a FieldValue>, - dst_addr_v4: Option<&'a FieldValue>, - dst_addr_v6: Option<&'a FieldValue>, +/// Unified field cache for collecting common flow fields in a single pass. +/// Used by both V9 and IPFIX default and config-based conversions. +#[derive(Copy, Clone, Default)] +struct FieldCache<'a> { + src_addr: Option<&'a FieldValue>, + dst_addr: Option<&'a FieldValue>, src_port: Option<&'a FieldValue>, dst_port: Option<&'a FieldValue>, protocol: Option<&'a FieldValue>, @@ -318,29 +317,33 @@ struct V9FieldCache<'a> { dst_mac: Option<&'a FieldValue>, } -impl<'a> V9FieldCache<'a> { - fn from_fields(fields: &'a [(V9Field, FieldValue)]) -> Self { - let mut cache = Self { - src_addr_v4: None, - src_addr_v6: None, - dst_addr_v4: None, - dst_addr_v6: None, - src_port: None, - dst_port: None, - protocol: None, - first_seen: None, - last_seen: None, - src_mac: None, - dst_mac: None, - }; +impl<'a> FieldCache<'a> { + fn to_common_flowset(&self) -> NetflowCommonFlowSet { + NetflowCommonFlowSet { + src_addr: self.src_addr.and_then(|v| v.try_into().ok()), + dst_addr: self.dst_addr.and_then(|v| v.try_into().ok()), + src_port: self.src_port.and_then(|v| v.as_u16()), + dst_port: self.dst_port.and_then(|v| v.as_u16()), + protocol_number: self.protocol.and_then(|v| v.as_u8()), + protocol_type: self + .protocol + .and_then(|v| v.as_u8()) + .map(ProtocolTypes::from), + first_seen: self.first_seen.and_then(|v| v.as_u64()), + last_seen: self.last_seen.and_then(|v| v.as_u64()), + src_mac: self.src_mac.and_then(|v| v.try_into().ok()), + dst_mac: self.dst_mac.and_then(|v| v.try_into().ok()), + } + } - // Single pass through all fields + fn from_v9_fields(fields: &'a [(V9Field, FieldValue)]) -> Self { + let mut cache = Self::default(); for (field_type, field_value) in fields { match field_type { - V9Field::Ipv4SrcAddr => cache.src_addr_v4 = Some(field_value), - V9Field::Ipv6SrcAddr => cache.src_addr_v6 = Some(field_value), - V9Field::Ipv4DstAddr => cache.dst_addr_v4 = Some(field_value), - V9Field::Ipv6DstAddr => cache.dst_addr_v6 = Some(field_value), + V9Field::Ipv4SrcAddr => cache.src_addr = cache.src_addr.or(Some(field_value)), + V9Field::Ipv6SrcAddr => cache.src_addr = cache.src_addr.or(Some(field_value)), + V9Field::Ipv4DstAddr => cache.dst_addr = cache.dst_addr.or(Some(field_value)), + V9Field::Ipv6DstAddr => cache.dst_addr = cache.dst_addr.or(Some(field_value)), V9Field::L4SrcPort => cache.src_port = Some(field_value), V9Field::L4DstPort => cache.dst_port = Some(field_value), V9Field::Protocol => cache.protocol = Some(field_value), @@ -348,163 +351,101 @@ impl<'a> V9FieldCache<'a> { V9Field::LastSwitched => cache.last_seen = Some(field_value), V9Field::InSrcMac => cache.src_mac = Some(field_value), V9Field::InDstMac => cache.dst_mac = Some(field_value), - _ => {} // Ignore other fields + _ => {} } } - cache } -} - -/// Extract a u8 from a FieldValue, handling both DataNumber and ProtocolType variants. -/// Supports numeric downcasting for DataNumber values that fit in u8. -fn extract_u8(v: &FieldValue) -> Option { - use crate::variable_versions::field_value::DataNumber; - match v { - FieldValue::ProtocolType(p) => Some(u8::from(*p)), - FieldValue::DataNumber(d) => match d { - DataNumber::U8(n) => Some(*n), - DataNumber::I8(n) => u8::try_from(*n).ok(), - DataNumber::U16(n) => u8::try_from(*n).ok(), - DataNumber::I16(n) => u8::try_from(*n).ok(), - DataNumber::U24(n) => u8::try_from(*n).ok(), - DataNumber::I24(n) => u8::try_from(*n).ok(), - DataNumber::U32(n) => u8::try_from(*n).ok(), - DataNumber::I32(n) => u8::try_from(*n).ok(), - DataNumber::U64(n) => u8::try_from(*n).ok(), - DataNumber::I64(n) => u8::try_from(*n).ok(), - DataNumber::U128(n) => u8::try_from(*n).ok(), - DataNumber::I128(n) => u8::try_from(*n).ok(), - DataNumber::Vec(_) => None, - }, - _ => None, - } -} - -/// Extract a u16 from a FieldValue, supporting numeric downcasting. -fn extract_u16(v: &FieldValue) -> Option { - use crate::variable_versions::field_value::DataNumber; - match v { - FieldValue::DataNumber(d) => match d { - DataNumber::U8(n) => Some(u16::from(*n)), - DataNumber::I8(n) => u16::try_from(*n).ok(), - DataNumber::U16(n) => Some(*n), - DataNumber::I16(n) => u16::try_from(*n).ok(), - DataNumber::U24(n) => u16::try_from(*n).ok(), - DataNumber::I24(n) => u16::try_from(*n).ok(), - DataNumber::U32(n) => u16::try_from(*n).ok(), - DataNumber::I32(n) => u16::try_from(*n).ok(), - DataNumber::U64(n) => u16::try_from(*n).ok(), - DataNumber::I64(n) => u16::try_from(*n).ok(), - DataNumber::U128(n) => u16::try_from(*n).ok(), - DataNumber::I128(n) => u16::try_from(*n).ok(), - DataNumber::Vec(_) => None, - }, - _ => None, - } -} -/// Extract a timestamp value (in milliseconds) from a FieldValue. -/// Handles both DataNumber (unsigned integers) and Duration variants. -fn extract_timestamp_millis(v: &FieldValue) -> Option { - use crate::variable_versions::field_value::DataNumber; - match v { - FieldValue::DataNumber(d) => { - // Try to extract as u64 first, then u32, then handle signed variants - if let Ok(val) = u64::try_from(d) { - Some(val) - } else if let Ok(val) = u32::try_from(d) { - Some(u64::from(val)) - } else { - // Handle remaining variants (small unsigned + all signed) - match d { - DataNumber::U8(n) => Some(u64::from(*n)), - DataNumber::U16(n) => Some(u64::from(*n)), - DataNumber::I8(n) => u64::try_from(*n).ok(), - DataNumber::I16(n) => u64::try_from(*n).ok(), - DataNumber::I24(n) => u64::try_from(*n).ok(), - DataNumber::I32(n) => u64::try_from(*n).ok(), - DataNumber::I64(n) => u64::try_from(*n).ok(), - DataNumber::I128(n) => u64::try_from(*n).ok(), - DataNumber::U128(n) => u64::try_from(*n).ok(), - _ => None, + fn from_ipfix_fields(fields: &'a [(IPFixField, FieldValue)]) -> Self { + let mut cache = Self::default(); + for (field_type, field_value) in fields { + match field_type { + IPFixField::IANA(IANAIPFixField::SourceIpv4address) => { + cache.src_addr = cache.src_addr.or(Some(field_value)) + } + IPFixField::IANA(IANAIPFixField::SourceIpv6address) => { + cache.src_addr = cache.src_addr.or(Some(field_value)) + } + IPFixField::IANA(IANAIPFixField::DestinationIpv4address) => { + cache.dst_addr = cache.dst_addr.or(Some(field_value)) + } + IPFixField::IANA(IANAIPFixField::DestinationIpv6address) => { + cache.dst_addr = cache.dst_addr.or(Some(field_value)) + } + IPFixField::IANA(IANAIPFixField::SourceTransportPort) => { + cache.src_port = Some(field_value) + } + IPFixField::IANA(IANAIPFixField::DestinationTransportPort) => { + cache.dst_port = Some(field_value) + } + IPFixField::IANA(IANAIPFixField::ProtocolIdentifier) => { + cache.protocol = Some(field_value) + } + IPFixField::IANA(IANAIPFixField::FlowStartSysUpTime) => { + cache.first_seen = Some(field_value) + } + IPFixField::IANA(IANAIPFixField::FlowEndSysUpTime) => { + cache.last_seen = Some(field_value) } + IPFixField::IANA(IANAIPFixField::SourceMacaddress) => { + cache.src_mac = Some(field_value) + } + IPFixField::IANA(IANAIPFixField::DestinationMacaddress) => { + cache.dst_mac = Some(field_value) + } + _ => {} } } - FieldValue::Duration(d) => match d { - DurationValue::Millis { value, .. } => Some(*value), - DurationValue::Seconds { value, .. } => value.checked_mul(1000), - DurationValue::MicrosNtp { seconds, fraction } => { - let millis = ((u64::from(*fraction)).saturating_mul(1_000)) >> 32; - u64::from(*seconds) - .checked_mul(1000) - .and_then(|s| s.checked_add(millis)) - } - DurationValue::NanosNtp { seconds, fraction } => { - let millis = ((u64::from(*fraction)).saturating_mul(1_000)) >> 32; - u64::from(*seconds) - .checked_mul(1000) - .and_then(|s| s.checked_add(millis)) - } - }, - _ => None, + cache } -} -/// Macro to create NetflowCommonFlowSet from a cache structure with separate v4/v6 fields -macro_rules! create_common_flowset_with_ip_versions { - ($cache:expr, $src_v4:ident, $src_v6:ident, $dst_v4:ident, $dst_v6:ident) => { - NetflowCommonFlowSet { - src_addr: $cache - .$src_v4 - .or($cache.$src_v6) - .and_then(|v| v.try_into().ok()), - dst_addr: $cache - .$dst_v4 - .or($cache.$dst_v6) - .and_then(|v| v.try_into().ok()), - src_port: $cache.src_port.and_then(|v| extract_u16(v)), - dst_port: $cache.dst_port.and_then(|v| extract_u16(v)), - protocol_number: $cache.protocol.and_then(|v| extract_u8(v)), - protocol_type: $cache - .protocol - .and_then(|v| extract_u8(v)) - .map(ProtocolTypes::from), - first_seen: $cache.first_seen.and_then(extract_timestamp_millis), - last_seen: $cache.last_seen.and_then(extract_timestamp_millis), - src_mac: $cache.src_mac.and_then(|v| v.try_into().ok()), - dst_mac: $cache.dst_mac.and_then(|v| v.try_into().ok()), + fn from_v9_fields_with_config( + fields: &'a [(V9Field, FieldValue)], + config: &V9FieldMappingConfig, + ) -> Self { + let mut cache = Self::default(); + for (field_type, field_value) in fields { + check_field_mapping!(field_type, field_value, cache, config, src_addr); + check_field_mapping!(field_type, field_value, cache, config, dst_addr); + check_field_mapping!(field_type, field_value, cache, config, src_port); + check_field_mapping!(field_type, field_value, cache, config, dst_port); + check_field_mapping!(field_type, field_value, cache, config, protocol); + check_field_mapping!(field_type, field_value, cache, config, first_seen); + check_field_mapping!(field_type, field_value, cache, config, last_seen); + check_field_mapping!(field_type, field_value, cache, config, src_mac); + check_field_mapping!(field_type, field_value, cache, config, dst_mac); } - }; -} + cache + } -/// Macro to create NetflowCommonFlowSet from a cache structure -macro_rules! create_common_flowset { - ($cache:expr) => { - NetflowCommonFlowSet { - src_addr: $cache.src_addr.and_then(|v| v.try_into().ok()), - dst_addr: $cache.dst_addr.and_then(|v| v.try_into().ok()), - src_port: $cache.src_port.and_then(|v| extract_u16(v)), - dst_port: $cache.dst_port.and_then(|v| extract_u16(v)), - protocol_number: $cache.protocol.and_then(|v| extract_u8(v)), - protocol_type: $cache - .protocol - .and_then(|v| extract_u8(v)) - .map(ProtocolTypes::from), - first_seen: $cache.first_seen.and_then(extract_timestamp_millis), - last_seen: $cache.last_seen.and_then(extract_timestamp_millis), - src_mac: $cache.src_mac.and_then(|v| v.try_into().ok()), - dst_mac: $cache.dst_mac.and_then(|v| v.try_into().ok()), + fn from_ipfix_fields_with_config( + fields: &'a [(IPFixField, FieldValue)], + config: &IPFixFieldMappingConfig, + ) -> Self { + let mut cache = Self::default(); + for (field_type, field_value) in fields { + check_field_mapping!(field_type, field_value, cache, config, src_addr); + check_field_mapping!(field_type, field_value, cache, config, dst_addr); + check_field_mapping!(field_type, field_value, cache, config, src_port); + check_field_mapping!(field_type, field_value, cache, config, dst_port); + check_field_mapping!(field_type, field_value, cache, config, protocol); + check_field_mapping!(field_type, field_value, cache, config, first_seen); + check_field_mapping!(field_type, field_value, cache, config, last_seen); + check_field_mapping!(field_type, field_value, cache, config, src_mac); + check_field_mapping!(field_type, field_value, cache, config, dst_mac); } - }; + cache + } } -/// Macro to generate field cache checking logic for config-based caches +/// Macro to check if a field matches a config mapping (primary or fallback). +/// Works for both Copy types (V9Field) and reference types (IPFixField). macro_rules! check_field_mapping { ($field_type:expr, $field_value:expr, $cache:expr, $config:expr, $field_name:ident) => { if *$field_type == $config.$field_name.primary { $cache.$field_name = Some($field_value); - } else if Some(*$field_type) == $config.$field_name.fallback + } else if $config.$field_name.fallback.as_ref() == Some($field_type) && $cache.$field_name.is_none() { $cache.$field_name = Some($field_value); @@ -512,52 +453,159 @@ macro_rules! check_field_mapping { }; } -/// Helper structure to cache V9 field lookups with custom mapping in a single pass -#[derive(Copy, Clone)] -struct V9ConfigFieldCache<'a> { - src_addr: Option<&'a FieldValue>, - dst_addr: Option<&'a FieldValue>, - src_port: Option<&'a FieldValue>, - dst_port: Option<&'a FieldValue>, - protocol: Option<&'a FieldValue>, - first_seen: Option<&'a FieldValue>, - last_seen: Option<&'a FieldValue>, - src_mac: Option<&'a FieldValue>, - dst_mac: Option<&'a FieldValue>, +/// Collect common flowsets from V9 flowset bodies. +fn collect_v9_flowsets(value: &V9) -> Vec { + let mut flowsets = vec![]; + for flowset in &value.flowsets { + match &flowset.body { + V9FlowSetBody::Data(data) => { + for record in &data.fields { + flowsets.push(FieldCache::from_v9_fields(record).to_common_flowset()); + } + } + V9FlowSetBody::OptionsData(opts_data) => { + for record in &opts_data.fields { + flowsets.push( + FieldCache::from_v9_fields(&record.options_fields).to_common_flowset(), + ); + } + } + V9FlowSetBody::Template(_) + | V9FlowSetBody::OptionsTemplate(_) + | V9FlowSetBody::NoTemplate(_) + | V9FlowSetBody::Empty => {} + } + } + flowsets } -impl<'a> V9ConfigFieldCache<'a> { - fn from_fields_with_config( - fields: &'a [(V9Field, FieldValue)], - config: &V9FieldMappingConfig, - ) -> Self { - let mut cache = Self { - src_addr: None, - dst_addr: None, - src_port: None, - dst_port: None, - protocol: None, - first_seen: None, - last_seen: None, - src_mac: None, - dst_mac: None, - }; +/// Collect common flowsets from V9 flowset bodies using a custom config. +fn collect_v9_flowsets_with_config( + value: &V9, + config: &V9FieldMappingConfig, +) -> Vec { + let mut flowsets = vec![]; + for flowset in &value.flowsets { + match &flowset.body { + V9FlowSetBody::Data(data) => { + for record in &data.fields { + flowsets.push( + FieldCache::from_v9_fields_with_config(record, config) + .to_common_flowset(), + ); + } + } + V9FlowSetBody::OptionsData(opts_data) => { + for record in &opts_data.fields { + flowsets.push( + FieldCache::from_v9_fields_with_config(&record.options_fields, config) + .to_common_flowset(), + ); + } + } + V9FlowSetBody::Template(_) + | V9FlowSetBody::OptionsTemplate(_) + | V9FlowSetBody::NoTemplate(_) + | V9FlowSetBody::Empty => {} + } + } + flowsets +} - // Single pass through all fields, collecting based on config - for (field_type, field_value) in fields { - check_field_mapping!(field_type, field_value, cache, config, src_addr); - check_field_mapping!(field_type, field_value, cache, config, dst_addr); - check_field_mapping!(field_type, field_value, cache, config, src_port); - check_field_mapping!(field_type, field_value, cache, config, dst_port); - check_field_mapping!(field_type, field_value, cache, config, protocol); - check_field_mapping!(field_type, field_value, cache, config, first_seen); - check_field_mapping!(field_type, field_value, cache, config, last_seen); - check_field_mapping!(field_type, field_value, cache, config, src_mac); - check_field_mapping!(field_type, field_value, cache, config, dst_mac); +/// Collect common flowsets from IPFIX flowset bodies. +/// Embedded V9-style flowsets use V9 field mapping. +fn collect_ipfix_flowsets(value: &IPFix) -> Vec { + let mut flowsets = vec![]; + for flowset in &value.flowsets { + match &flowset.body { + IPFixFlowSetBody::Data(data) => { + for record in &data.fields { + flowsets.push(FieldCache::from_ipfix_fields(record).to_common_flowset()); + } + } + IPFixFlowSetBody::OptionsData(opts_data) => { + for record in &opts_data.fields { + flowsets.push(FieldCache::from_ipfix_fields(record).to_common_flowset()); + } + } + IPFixFlowSetBody::V9Data(v9_data) => { + for record in &v9_data.fields { + flowsets.push(FieldCache::from_v9_fields(record).to_common_flowset()); + } + } + IPFixFlowSetBody::V9OptionsData(v9_opts_data) => { + for record in &v9_opts_data.fields { + flowsets.push( + FieldCache::from_v9_fields(&record.options_fields).to_common_flowset(), + ); + } + } + IPFixFlowSetBody::Template(_) + | IPFixFlowSetBody::Templates(_) + | IPFixFlowSetBody::V9Template(_) + | IPFixFlowSetBody::V9Templates(_) + | IPFixFlowSetBody::OptionsTemplate(_) + | IPFixFlowSetBody::OptionsTemplates(_) + | IPFixFlowSetBody::V9OptionsTemplate(_) + | IPFixFlowSetBody::V9OptionsTemplates(_) + | IPFixFlowSetBody::NoTemplate(_) + | IPFixFlowSetBody::Empty => {} } + } + flowsets +} - cache +/// Collect common flowsets from IPFIX flowset bodies using a custom config. +/// Embedded V9-style flowsets use default V9 field mapping (config is IPFIX-only). +fn collect_ipfix_flowsets_with_config( + value: &IPFix, + config: &IPFixFieldMappingConfig, +) -> Vec { + let mut flowsets = vec![]; + for flowset in &value.flowsets { + match &flowset.body { + IPFixFlowSetBody::Data(data) => { + for record in &data.fields { + flowsets.push( + FieldCache::from_ipfix_fields_with_config(record, config) + .to_common_flowset(), + ); + } + } + IPFixFlowSetBody::OptionsData(opts_data) => { + for record in &opts_data.fields { + flowsets.push( + FieldCache::from_ipfix_fields_with_config(record, config) + .to_common_flowset(), + ); + } + } + // V9-style flowsets embedded in IPFIX use V9 field types + IPFixFlowSetBody::V9Data(v9_data) => { + for record in &v9_data.fields { + flowsets.push(FieldCache::from_v9_fields(record).to_common_flowset()); + } + } + IPFixFlowSetBody::V9OptionsData(v9_opts_data) => { + for record in &v9_opts_data.fields { + flowsets.push( + FieldCache::from_v9_fields(&record.options_fields).to_common_flowset(), + ); + } + } + IPFixFlowSetBody::Template(_) + | IPFixFlowSetBody::Templates(_) + | IPFixFlowSetBody::V9Template(_) + | IPFixFlowSetBody::V9Templates(_) + | IPFixFlowSetBody::OptionsTemplate(_) + | IPFixFlowSetBody::OptionsTemplates(_) + | IPFixFlowSetBody::V9OptionsTemplate(_) + | IPFixFlowSetBody::V9OptionsTemplates(_) + | IPFixFlowSetBody::NoTemplate(_) + | IPFixFlowSetBody::Empty => {} + } } + flowsets } impl NetflowCommon { @@ -580,223 +628,21 @@ impl NetflowCommon { /// // Then use: NetflowCommon::from_v9_with_config(&v9, &config); /// ``` pub fn from_v9_with_config(value: &V9, config: &V9FieldMappingConfig) -> Self { - let mut flowsets = vec![]; - - for flowset in &value.flowsets { - match &flowset.body { - V9FlowSetBody::Data(data) => { - for data_field in &data.fields { - // Single pass through fields to collect all values with config - let cache = - V9ConfigFieldCache::from_fields_with_config(data_field, config); - flowsets.push(create_common_flowset!(cache)); - } - } - V9FlowSetBody::OptionsData(opts_data) => { - for record in &opts_data.fields { - let cache = V9ConfigFieldCache::from_fields_with_config( - &record.options_fields, - config, - ); - flowsets.push(create_common_flowset!(cache)); - } - } - V9FlowSetBody::Template(_) - | V9FlowSetBody::OptionsTemplate(_) - | V9FlowSetBody::NoTemplate(_) - | V9FlowSetBody::Empty => {} - } - } - NetflowCommon { version: value.header.version, timestamp: value.header.sys_up_time, - flowsets, + flowsets: collect_v9_flowsets_with_config(value, config), } } } impl From<&V9> for NetflowCommon { fn from(value: &V9) -> Self { - // Convert V9 to NetflowCommon using default configuration with single-pass field lookup - let mut flowsets = vec![]; - - for flowset in &value.flowsets { - match &flowset.body { - V9FlowSetBody::Data(data) => { - for data_field in &data.fields { - // Single pass through fields to collect all values - let cache = V9FieldCache::from_fields(data_field); - flowsets.push(create_common_flowset_with_ip_versions!( - cache, - src_addr_v4, - src_addr_v6, - dst_addr_v4, - dst_addr_v6 - )); - } - } - V9FlowSetBody::OptionsData(opts_data) => { - for record in &opts_data.fields { - let cache = V9FieldCache::from_fields(&record.options_fields); - flowsets.push(create_common_flowset_with_ip_versions!( - cache, - src_addr_v4, - src_addr_v6, - dst_addr_v4, - dst_addr_v6 - )); - } - } - V9FlowSetBody::Template(_) - | V9FlowSetBody::OptionsTemplate(_) - | V9FlowSetBody::NoTemplate(_) - | V9FlowSetBody::Empty => {} - } - } - NetflowCommon { version: value.header.version, timestamp: value.header.sys_up_time, - flowsets, - } - } -} - -/// Helper structure to store all found IPFIX fields in a single pass -#[derive(Copy, Clone)] -struct IPFixFieldCache<'a> { - src_addr_v4: Option<&'a FieldValue>, - src_addr_v6: Option<&'a FieldValue>, - dst_addr_v4: Option<&'a FieldValue>, - dst_addr_v6: Option<&'a FieldValue>, - src_port: Option<&'a FieldValue>, - dst_port: Option<&'a FieldValue>, - protocol: Option<&'a FieldValue>, - first_seen: Option<&'a FieldValue>, - last_seen: Option<&'a FieldValue>, - src_mac: Option<&'a FieldValue>, - dst_mac: Option<&'a FieldValue>, -} - -impl<'a> IPFixFieldCache<'a> { - fn from_fields(fields: &'a [(IPFixField, FieldValue)]) -> Self { - let mut cache = Self { - src_addr_v4: None, - src_addr_v6: None, - dst_addr_v4: None, - dst_addr_v6: None, - src_port: None, - dst_port: None, - protocol: None, - first_seen: None, - last_seen: None, - src_mac: None, - dst_mac: None, - }; - - // Single pass through all fields - for (field_type, field_value) in fields { - match field_type { - IPFixField::IANA(IANAIPFixField::SourceIpv4address) => { - cache.src_addr_v4 = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::SourceIpv6address) => { - cache.src_addr_v6 = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::DestinationIpv4address) => { - cache.dst_addr_v4 = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::DestinationIpv6address) => { - cache.dst_addr_v6 = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::SourceTransportPort) => { - cache.src_port = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::DestinationTransportPort) => { - cache.dst_port = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::ProtocolIdentifier) => { - cache.protocol = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::FlowStartSysUpTime) => { - cache.first_seen = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::FlowEndSysUpTime) => { - cache.last_seen = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::SourceMacaddress) => { - cache.src_mac = Some(field_value) - } - IPFixField::IANA(IANAIPFixField::DestinationMacaddress) => { - cache.dst_mac = Some(field_value) - } - _ => {} // Ignore other fields - } + flowsets: collect_v9_flowsets(value), } - - cache - } -} - -/// Macro to check field mapping for types that need as_ref() for fallback comparison -macro_rules! check_field_mapping_ref { - ($field_type:expr, $field_value:expr, $cache:expr, $config:expr, $field_name:ident) => { - if *$field_type == $config.$field_name.primary { - $cache.$field_name = Some($field_value); - } else if $config.$field_name.fallback.as_ref() == Some($field_type) - && $cache.$field_name.is_none() - { - $cache.$field_name = Some($field_value); - } - }; -} - -/// Helper structure to cache IPFIX field lookups with custom mapping in a single pass -#[derive(Copy, Clone)] -struct IPFixConfigFieldCache<'a> { - src_addr: Option<&'a FieldValue>, - dst_addr: Option<&'a FieldValue>, - src_port: Option<&'a FieldValue>, - dst_port: Option<&'a FieldValue>, - protocol: Option<&'a FieldValue>, - first_seen: Option<&'a FieldValue>, - last_seen: Option<&'a FieldValue>, - src_mac: Option<&'a FieldValue>, - dst_mac: Option<&'a FieldValue>, -} - -impl<'a> IPFixConfigFieldCache<'a> { - fn from_fields_with_config( - fields: &'a [(IPFixField, FieldValue)], - config: &IPFixFieldMappingConfig, - ) -> Self { - let mut cache = Self { - src_addr: None, - dst_addr: None, - src_port: None, - dst_port: None, - protocol: None, - first_seen: None, - last_seen: None, - src_mac: None, - dst_mac: None, - }; - - // Single pass through all fields, collecting based on config - for (field_type, field_value) in fields { - check_field_mapping_ref!(field_type, field_value, cache, config, src_addr); - check_field_mapping_ref!(field_type, field_value, cache, config, dst_addr); - check_field_mapping_ref!(field_type, field_value, cache, config, src_port); - check_field_mapping_ref!(field_type, field_value, cache, config, dst_port); - check_field_mapping_ref!(field_type, field_value, cache, config, protocol); - check_field_mapping_ref!(field_type, field_value, cache, config, first_seen); - check_field_mapping_ref!(field_type, field_value, cache, config, last_seen); - check_field_mapping_ref!(field_type, field_value, cache, config, src_mac); - check_field_mapping_ref!(field_type, field_value, cache, config, dst_mac); - } - - cache } } @@ -824,143 +670,20 @@ impl NetflowCommon { /// // Then use: NetflowCommon::from_ipfix_with_config(&ipfix, &config); /// ``` pub fn from_ipfix_with_config(value: &IPFix, config: &IPFixFieldMappingConfig) -> Self { - let mut flowsets = vec![]; - - for flowset in &value.flowsets { - match &flowset.body { - IPFixFlowSetBody::Data(data) => { - for data_field in &data.fields { - let cache = - IPFixConfigFieldCache::from_fields_with_config(data_field, config); - flowsets.push(create_common_flowset!(cache)); - } - } - IPFixFlowSetBody::OptionsData(opts_data) => { - for data_field in &opts_data.fields { - let cache = - IPFixConfigFieldCache::from_fields_with_config(data_field, config); - flowsets.push(create_common_flowset!(cache)); - } - } - IPFixFlowSetBody::V9Data(v9_data) => { - // V9Data embedded in IPFIX uses V9 field types; use default V9 field mapping - for data_field in &v9_data.fields { - let cache = V9FieldCache::from_fields(data_field); - flowsets.push(create_common_flowset_with_ip_versions!( - cache, - src_addr_v4, - src_addr_v6, - dst_addr_v4, - dst_addr_v6 - )); - } - } - IPFixFlowSetBody::V9OptionsData(v9_opts_data) => { - // V9OptionsData embedded in IPFIX uses V9 field types; use default V9 field mapping - for record in &v9_opts_data.fields { - let cache = V9FieldCache::from_fields(&record.options_fields); - flowsets.push(create_common_flowset_with_ip_versions!( - cache, - src_addr_v4, - src_addr_v6, - dst_addr_v4, - dst_addr_v6 - )); - } - } - IPFixFlowSetBody::Template(_) - | IPFixFlowSetBody::Templates(_) - | IPFixFlowSetBody::V9Template(_) - | IPFixFlowSetBody::V9Templates(_) - | IPFixFlowSetBody::OptionsTemplate(_) - | IPFixFlowSetBody::OptionsTemplates(_) - | IPFixFlowSetBody::V9OptionsTemplate(_) - | IPFixFlowSetBody::V9OptionsTemplates(_) - | IPFixFlowSetBody::NoTemplate(_) - | IPFixFlowSetBody::Empty => {} - } - } - NetflowCommon { version: value.header.version, timestamp: value.header.export_time, - flowsets, + flowsets: collect_ipfix_flowsets_with_config(value, config), } } } impl From<&IPFix> for NetflowCommon { fn from(value: &IPFix) -> Self { - // Convert IPFix to NetflowCommon with single-pass field lookup - let mut flowsets = vec![]; - - for flowset in &value.flowsets { - match &flowset.body { - IPFixFlowSetBody::Data(data) => { - for data_field in &data.fields { - let cache = IPFixFieldCache::from_fields(data_field); - flowsets.push(create_common_flowset_with_ip_versions!( - cache, - src_addr_v4, - src_addr_v6, - dst_addr_v4, - dst_addr_v6 - )); - } - } - IPFixFlowSetBody::OptionsData(opts_data) => { - for data_field in &opts_data.fields { - let cache = IPFixFieldCache::from_fields(data_field); - flowsets.push(create_common_flowset_with_ip_versions!( - cache, - src_addr_v4, - src_addr_v6, - dst_addr_v4, - dst_addr_v6 - )); - } - } - IPFixFlowSetBody::V9Data(v9_data) => { - for data_field in &v9_data.fields { - let cache = V9FieldCache::from_fields(data_field); - flowsets.push(create_common_flowset_with_ip_versions!( - cache, - src_addr_v4, - src_addr_v6, - dst_addr_v4, - dst_addr_v6 - )); - } - } - IPFixFlowSetBody::V9OptionsData(v9_opts_data) => { - for record in &v9_opts_data.fields { - let cache = V9FieldCache::from_fields(&record.options_fields); - flowsets.push(create_common_flowset_with_ip_versions!( - cache, - src_addr_v4, - src_addr_v6, - dst_addr_v4, - dst_addr_v6 - )); - } - } - IPFixFlowSetBody::Template(_) - | IPFixFlowSetBody::Templates(_) - | IPFixFlowSetBody::V9Template(_) - | IPFixFlowSetBody::V9Templates(_) - | IPFixFlowSetBody::OptionsTemplate(_) - | IPFixFlowSetBody::OptionsTemplates(_) - | IPFixFlowSetBody::V9OptionsTemplate(_) - | IPFixFlowSetBody::V9OptionsTemplates(_) - | IPFixFlowSetBody::NoTemplate(_) - | IPFixFlowSetBody::Empty => {} - } - } - NetflowCommon { version: value.header.version, timestamp: value.header.export_time, - flowsets, + flowsets: collect_ipfix_flowsets(value), } } } diff --git a/src/field_types/firewall_event.rs b/src/variable_versions/field_types/firewall_event.rs similarity index 100% rename from src/field_types/firewall_event.rs rename to src/variable_versions/field_types/firewall_event.rs diff --git a/src/field_types/flow_end_reason.rs b/src/variable_versions/field_types/flow_end_reason.rs similarity index 100% rename from src/field_types/flow_end_reason.rs rename to src/variable_versions/field_types/flow_end_reason.rs diff --git a/src/field_types/forwarding_status.rs b/src/variable_versions/field_types/forwarding_status.rs similarity index 100% rename from src/field_types/forwarding_status.rs rename to src/variable_versions/field_types/forwarding_status.rs diff --git a/src/field_types/fragment_flags.rs b/src/variable_versions/field_types/fragment_flags.rs similarity index 100% rename from src/field_types/fragment_flags.rs rename to src/variable_versions/field_types/fragment_flags.rs diff --git a/src/field_types/ipv4_options.rs b/src/variable_versions/field_types/ipv4_options.rs similarity index 100% rename from src/field_types/ipv4_options.rs rename to src/variable_versions/field_types/ipv4_options.rs diff --git a/src/field_types/ipv6_extension_headers.rs b/src/variable_versions/field_types/ipv6_extension_headers.rs similarity index 100% rename from src/field_types/ipv6_extension_headers.rs rename to src/variable_versions/field_types/ipv6_extension_headers.rs diff --git a/src/field_types/is_multicast.rs b/src/variable_versions/field_types/is_multicast.rs similarity index 100% rename from src/field_types/is_multicast.rs rename to src/variable_versions/field_types/is_multicast.rs diff --git a/src/field_types/mod.rs b/src/variable_versions/field_types/mod.rs similarity index 100% rename from src/field_types/mod.rs rename to src/variable_versions/field_types/mod.rs diff --git a/src/field_types/mpls_label_exp.rs b/src/variable_versions/field_types/mpls_label_exp.rs similarity index 100% rename from src/field_types/mpls_label_exp.rs rename to src/variable_versions/field_types/mpls_label_exp.rs diff --git a/src/field_types/mpls_top_label_type.rs b/src/variable_versions/field_types/mpls_top_label_type.rs similarity index 100% rename from src/field_types/mpls_top_label_type.rs rename to src/variable_versions/field_types/mpls_top_label_type.rs diff --git a/src/field_types/nat_event.rs b/src/variable_versions/field_types/nat_event.rs similarity index 100% rename from src/field_types/nat_event.rs rename to src/variable_versions/field_types/nat_event.rs diff --git a/src/field_types/nat_originating_address_realm.rs b/src/variable_versions/field_types/nat_originating_address_realm.rs similarity index 100% rename from src/field_types/nat_originating_address_realm.rs rename to src/variable_versions/field_types/nat_originating_address_realm.rs diff --git a/src/field_types/snapshots/netflow_parser__field_types__firewall_event__firewall_event_tests__all_firewall_events.snap b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__firewall_event__firewall_event_tests__all_firewall_events.snap similarity index 70% rename from src/field_types/snapshots/netflow_parser__field_types__firewall_event__firewall_event_tests__all_firewall_events.snap rename to src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__firewall_event__firewall_event_tests__all_firewall_events.snap index 077051d..669953c 100644 --- a/src/field_types/snapshots/netflow_parser__field_types__firewall_event__firewall_event_tests__all_firewall_events.snap +++ b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__firewall_event__firewall_event_tests__all_firewall_events.snap @@ -1,5 +1,5 @@ --- -source: src/field_types/firewall_event.rs +source: src/variable_versions/field_types/firewall_event.rs expression: events --- - Ignore diff --git a/src/field_types/snapshots/netflow_parser__field_types__flow_end_reason__flow_end_reason_tests__all_flow_end_reasons.snap b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__flow_end_reason__flow_end_reason_tests__all_flow_end_reasons.snap similarity index 74% rename from src/field_types/snapshots/netflow_parser__field_types__flow_end_reason__flow_end_reason_tests__all_flow_end_reasons.snap rename to src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__flow_end_reason__flow_end_reason_tests__all_flow_end_reasons.snap index 83c3d2b..49dba79 100644 --- a/src/field_types/snapshots/netflow_parser__field_types__flow_end_reason__flow_end_reason_tests__all_flow_end_reasons.snap +++ b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__flow_end_reason__flow_end_reason_tests__all_flow_end_reasons.snap @@ -1,5 +1,5 @@ --- -source: src/field_types/flow_end_reason.rs +source: src/variable_versions/field_types/flow_end_reason.rs expression: reasons --- - Unknown: 0 diff --git a/src/field_types/snapshots/netflow_parser__field_types__forwarding_status__forwarding_status_tests__all_forwarding_statuses.snap b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__forwarding_status__forwarding_status_tests__all_forwarding_statuses.snap similarity index 98% rename from src/field_types/snapshots/netflow_parser__field_types__forwarding_status__forwarding_status_tests__all_forwarding_statuses.snap rename to src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__forwarding_status__forwarding_status_tests__all_forwarding_statuses.snap index 881dd0f..b3c5a99 100644 --- a/src/field_types/snapshots/netflow_parser__field_types__forwarding_status__forwarding_status_tests__all_forwarding_statuses.snap +++ b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__forwarding_status__forwarding_status_tests__all_forwarding_statuses.snap @@ -1,5 +1,5 @@ --- -source: src/field_types/forwarding_status.rs +source: src/variable_versions/field_types/forwarding_status.rs expression: statuses --- - Unknown: 0 diff --git a/src/field_types/snapshots/netflow_parser__field_types__fragment_flags__fragment_flags_tests__all_fragment_flags.snap b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__fragment_flags__fragment_flags_tests__all_fragment_flags.snap similarity index 91% rename from src/field_types/snapshots/netflow_parser__field_types__fragment_flags__fragment_flags_tests__all_fragment_flags.snap rename to src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__fragment_flags__fragment_flags_tests__all_fragment_flags.snap index 5f92afc..ce2e681 100644 --- a/src/field_types/snapshots/netflow_parser__field_types__fragment_flags__fragment_flags_tests__all_fragment_flags.snap +++ b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__fragment_flags__fragment_flags_tests__all_fragment_flags.snap @@ -1,5 +1,5 @@ --- -source: src/field_types/fragment_flags.rs +source: src/variable_versions/field_types/fragment_flags.rs expression: flags --- - reserved: false diff --git a/src/field_types/snapshots/netflow_parser__field_types__mpls_top_label_type__mpls_top_label_type_tests__all_mpls_top_label_types.snap b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__mpls_top_label_type__mpls_top_label_type_tests__all_mpls_top_label_types.snap similarity index 81% rename from src/field_types/snapshots/netflow_parser__field_types__mpls_top_label_type__mpls_top_label_type_tests__all_mpls_top_label_types.snap rename to src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__mpls_top_label_type__mpls_top_label_type_tests__all_mpls_top_label_types.snap index f3bf64f..fd794a2 100644 --- a/src/field_types/snapshots/netflow_parser__field_types__mpls_top_label_type__mpls_top_label_type_tests__all_mpls_top_label_types.snap +++ b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__mpls_top_label_type__mpls_top_label_type_tests__all_mpls_top_label_types.snap @@ -1,5 +1,5 @@ --- -source: src/field_types/mpls_top_label_type.rs +source: src/variable_versions/field_types/mpls_top_label_type.rs expression: types --- - Unknown diff --git a/src/field_types/snapshots/netflow_parser__field_types__nat_event__nat_event_tests__all_nat_events.snap b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__nat_event__nat_event_tests__all_nat_events.snap similarity index 88% rename from src/field_types/snapshots/netflow_parser__field_types__nat_event__nat_event_tests__all_nat_events.snap rename to src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__nat_event__nat_event_tests__all_nat_events.snap index 378fcc0..168cdd6 100644 --- a/src/field_types/snapshots/netflow_parser__field_types__nat_event__nat_event_tests__all_nat_events.snap +++ b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__nat_event__nat_event_tests__all_nat_events.snap @@ -1,5 +1,5 @@ --- -source: src/field_types/nat_event.rs +source: src/variable_versions/field_types/nat_event.rs expression: events --- - Unknown: 0 diff --git a/src/field_types/snapshots/netflow_parser__field_types__tcp_control_bits__tcp_control_bits_tests__all_single_flags.snap b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__tcp_control_bits__tcp_control_bits_tests__all_single_flags.snap similarity index 94% rename from src/field_types/snapshots/netflow_parser__field_types__tcp_control_bits__tcp_control_bits_tests__all_single_flags.snap rename to src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__tcp_control_bits__tcp_control_bits_tests__all_single_flags.snap index 4667cad..5ffce91 100644 --- a/src/field_types/snapshots/netflow_parser__field_types__tcp_control_bits__tcp_control_bits_tests__all_single_flags.snap +++ b/src/variable_versions/field_types/snapshots/netflow_parser__variable_versions__field_types__tcp_control_bits__tcp_control_bits_tests__all_single_flags.snap @@ -1,5 +1,5 @@ --- -source: src/field_types/tcp_control_bits.rs +source: src/variable_versions/field_types/tcp_control_bits.rs expression: flags --- - fin: true diff --git a/src/field_types/tcp_control_bits.rs b/src/variable_versions/field_types/tcp_control_bits.rs similarity index 100% rename from src/field_types/tcp_control_bits.rs rename to src/variable_versions/field_types/tcp_control_bits.rs diff --git a/src/field_types/tcp_options.rs b/src/variable_versions/field_types/tcp_options.rs similarity index 100% rename from src/field_types/tcp_options.rs rename to src/variable_versions/field_types/tcp_options.rs diff --git a/src/variable_versions/field_value.rs b/src/variable_versions/field_value.rs index d885010..4a979cb 100644 --- a/src/variable_versions/field_value.rs +++ b/src/variable_versions/field_value.rs @@ -1,4 +1,4 @@ -use crate::field_types::{ +use super::field_types::{ FirewallEvent, FlowEndReason, ForwardingStatus, FragmentFlags, Ipv4Options, Ipv6ExtensionHeaders, IsMulticast, MplsLabelExp, MplsTopLabelType, NatEvent, NatOriginatingAddressRealm, TcpControlBits, TcpOptions, @@ -70,6 +70,63 @@ pub enum DataNumber { } impl DataNumber { + /// Try to extract a `u8` from any numeric variant, narrowing if the value fits. + pub fn as_u8(&self) -> Option { + match self { + DataNumber::U8(n) => Some(*n), + DataNumber::I8(n) => u8::try_from(*n).ok(), + DataNumber::U16(n) => u8::try_from(*n).ok(), + DataNumber::I16(n) => u8::try_from(*n).ok(), + DataNumber::U24(n) => u8::try_from(*n).ok(), + DataNumber::I24(n) => u8::try_from(*n).ok(), + DataNumber::U32(n) => u8::try_from(*n).ok(), + DataNumber::I32(n) => u8::try_from(*n).ok(), + DataNumber::U64(n) => u8::try_from(*n).ok(), + DataNumber::I64(n) => u8::try_from(*n).ok(), + DataNumber::U128(n) => u8::try_from(*n).ok(), + DataNumber::I128(n) => u8::try_from(*n).ok(), + DataNumber::Vec(_) => None, + } + } + + /// Try to extract a `u16` from any numeric variant, narrowing if the value fits. + pub fn as_u16(&self) -> Option { + match self { + DataNumber::U8(n) => Some(u16::from(*n)), + DataNumber::I8(n) => u16::try_from(*n).ok(), + DataNumber::U16(n) => Some(*n), + DataNumber::I16(n) => u16::try_from(*n).ok(), + DataNumber::U24(n) => u16::try_from(*n).ok(), + DataNumber::I24(n) => u16::try_from(*n).ok(), + DataNumber::U32(n) => u16::try_from(*n).ok(), + DataNumber::I32(n) => u16::try_from(*n).ok(), + DataNumber::U64(n) => u16::try_from(*n).ok(), + DataNumber::I64(n) => u16::try_from(*n).ok(), + DataNumber::U128(n) => u16::try_from(*n).ok(), + DataNumber::I128(n) => u16::try_from(*n).ok(), + DataNumber::Vec(_) => None, + } + } + + /// Try to extract a `u64` from any numeric variant, widening or narrowing as needed. + pub fn as_u64(&self) -> Option { + match self { + DataNumber::U8(n) => Some(u64::from(*n)), + DataNumber::I8(n) => u64::try_from(*n).ok(), + DataNumber::U16(n) => Some(u64::from(*n)), + DataNumber::I16(n) => u64::try_from(*n).ok(), + DataNumber::U24(n) => Some(u64::from(*n)), + DataNumber::I24(n) => u64::try_from(*n).ok(), + DataNumber::U32(n) => Some(u64::from(*n)), + DataNumber::I32(n) => u64::try_from(*n).ok(), + DataNumber::U64(n) => Some(*n), + DataNumber::I64(n) => u64::try_from(*n).ok(), + DataNumber::U128(n) => u64::try_from(*n).ok(), + DataNumber::I128(n) => u64::try_from(*n).ok(), + DataNumber::Vec(_) => None, + } + } + /// Convert to i128 for numeric comparison across all variants. fn to_i128(&self) -> i128 { match self { @@ -606,6 +663,49 @@ impl Serialize for FieldValue { } impl FieldValue { + /// Try to extract a `u8`, narrowing across `DataNumber` variants and + /// converting `ProtocolType` to its numeric value. + pub fn as_u8(&self) -> Option { + match self { + FieldValue::DataNumber(d) => d.as_u8(), + FieldValue::ProtocolType(p) => Some(u8::from(*p)), + _ => None, + } + } + + /// Try to extract a `u16`, narrowing across `DataNumber` variants. + pub fn as_u16(&self) -> Option { + match self { + FieldValue::DataNumber(d) => d.as_u16(), + _ => None, + } + } + + /// Try to extract a `u64`, widening or narrowing across `DataNumber` variants + /// and converting `Duration` to milliseconds. + pub fn as_u64(&self) -> Option { + match self { + FieldValue::DataNumber(d) => d.as_u64(), + FieldValue::Duration(d) => match d { + DurationValue::Millis { value, .. } => Some(*value), + DurationValue::Seconds { value, .. } => value.checked_mul(1000), + DurationValue::MicrosNtp { seconds, fraction } => { + let millis = ((u64::from(*fraction)).saturating_mul(1_000)) >> 32; + u64::from(*seconds) + .checked_mul(1000) + .and_then(|s| s.checked_add(millis)) + } + DurationValue::NanosNtp { seconds, fraction } => { + let millis = ((u64::from(*fraction)).saturating_mul(1_000)) >> 32; + u64::from(*seconds) + .checked_mul(1000) + .and_then(|s| s.checked_add(millis)) + } + }, + _ => None, + } + } + /// Returns the number of bytes this value occupies when serialized. pub fn byte_len(&self) -> usize { match self { diff --git a/src/variable_versions/mod.rs b/src/variable_versions/mod.rs index c2d0869..ff3de30 100644 --- a/src/variable_versions/mod.rs +++ b/src/variable_versions/mod.rs @@ -68,6 +68,7 @@ //! - [`metrics`] - Template cache performance metrics pub(crate) mod config; +pub mod field_types; pub mod field_value; #[deprecated(since = "1.0.0", note = "renamed to `field_value`")] pub mod data_number { @@ -78,6 +79,7 @@ pub mod enterprise_registry; pub mod ipfix; pub mod metrics; pub(crate) mod pending_flows; +pub mod template_events; pub mod ttl; pub mod v9; diff --git a/src/template_events.rs b/src/variable_versions/template_events.rs similarity index 100% rename from src/template_events.rs rename to src/variable_versions/template_events.rs From 89767d136beecdff82a2b2414a126739a6a4dcee Mon Sep 17 00:00:00 2001 From: mikemiles-dev Date: Sun, 15 Mar 2026 16:29:55 -0500 Subject: [PATCH 3/4] fix: fmt --- RELEASES.md | 21 ++++++ src/variable_versions/config.rs | 13 +++- src/variable_versions/field_value.rs | 88 +++++++++++++++++++++++ src/variable_versions/ipfix/parser.rs | 3 + src/variable_versions/ipfix/serializer.rs | 5 ++ src/variable_versions/pending_flows.rs | 8 ++- src/variable_versions/v9/parser.rs | 3 + 7 files changed, 139 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index ed460ac..e802a51 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -210,6 +210,13 @@ * **`#![forbid(unsafe_code)]` enforced** - The crate contains zero `unsafe` blocks; this is now enforced at the crate level +* **Cross-variant numeric extraction on `DataNumber` and `FieldValue`** + - `DataNumber::as_u8()`, `as_u16()`, `as_u64()` — try to extract a value from any numeric variant, narrowing or widening as needed (returns `None` if the value doesn't fit) + - `FieldValue::as_u8()` — delegates to `DataNumber` and also converts `ProtocolType` to its numeric value + - `FieldValue::as_u16()` — delegates to `DataNumber` + - `FieldValue::as_u64()` — delegates to `DataNumber` and converts `Duration` variants to milliseconds + - Unlike the existing `TryFrom` impls (which only match the exact variant), these methods work across all numeric widths + * **Expanded root re-exports** - `Config`, `ConfigError`, `TtlConfig`, `EnterpriseFieldRegistry`, `CacheMetrics`, `NoTemplateInfo`, `DEFAULT_MAX_RECORDS_PER_FLOWSET`, `DEFAULT_MAX_SOURCES` — now available at crate root - `DataNumber`, `FieldDataType`, `FieldValue` — commonly used field/data types at crate root @@ -382,6 +389,8 @@ - Split `v9.rs` into `v9/{mod.rs, parser.rs, serializer.rs}` - Split `ipfix.rs` into `ipfix/{mod.rs, parser.rs, serializer.rs}` - Renamed `data_number.rs` → `field_value.rs` (deprecated re-export module preserves backward compatibility) + - Moved `field_types` from crate root to `variable_versions::field_types` (deprecated re-export at `crate::field_types`) + - Moved `template_events` from crate root to `variable_versions::template_events` (deprecated re-export at `crate::template_events`) * **Code cleanup** - Removed unused `enterprise_registry` field from `V9Parser` (was `#[allow(dead_code)]`) @@ -424,6 +433,18 @@ * **`v9_available_template_ids()` and `ipfix_available_template_ids()` now return sorted, deduplicated results** - Same template ID could previously appear twice (once from templates cache, once from options_templates cache) +* **`clear_v9_pending_flows()` and `clear_ipfix_pending_flows()` now record dropped metrics** + - Previously, clearing pending flows did not update `pending_dropped` counters + - Now records the count of cleared entries via `record_pending_dropped_n()` + +* **`Data::with_template_field_lengths()` now validates field count** + - Panics if `template_field_lengths` length doesn't match the field count of the first record + - Prevents silent corrupt serialization from mismatched metadata + +* **IPFIX variable-length field serialization rejects zero-length fields** + - `to_be_bytes()` now returns an error for zero-length variable-length fields per RFC 7011 Section 7 + - Previously wrote a `0x00` prefix which the parser would reject, breaking round-trip + ## Known Limitations * **IPFIX variable-length field serialization requires `template_field_lengths`** diff --git a/src/variable_versions/config.rs b/src/variable_versions/config.rs index cc43c39..0a1b6a3 100644 --- a/src/variable_versions/config.rs +++ b/src/variable_versions/config.rs @@ -1,5 +1,6 @@ //! Parser configuration, traits, and constants for V9 and IPFIX parsers. +use super::metrics::CacheMetricsInner; use super::pending_flows::{PendingFlowCache, PendingFlowsConfig}; use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry; use crate::variable_versions::ttl::TtlConfig; @@ -268,6 +269,7 @@ pub(crate) trait ParserFields { fn set_enterprise_registry(&mut self, _registry: Arc) {} fn pending_flows(&self) -> &Option; fn pending_flows_mut(&mut self) -> &mut Option; + fn metrics_mut(&mut self) -> &mut CacheMetricsInner; } /// Trait for parsers that support template caching and TTL configuration @@ -360,10 +362,19 @@ pub trait ParserConfig: ParserFields { .unwrap_or(0) } - /// Clear all pending flows. + /// Clear all pending flows, recording dropped metrics. fn clear_pending_flows(&mut self) { + // Count entries before clearing to record metrics. + let count = self + .pending_flows() + .as_ref() + .map(|cache| cache.count()) + .unwrap_or(0); if let Some(cache) = self.pending_flows_mut() { cache.clear(); } + if count > 0 { + self.metrics_mut().record_pending_dropped_n(count as u64); + } } } diff --git a/src/variable_versions/field_value.rs b/src/variable_versions/field_value.rs index 4a979cb..251965c 100644 --- a/src/variable_versions/field_value.rs +++ b/src/variable_versions/field_value.rs @@ -1230,4 +1230,92 @@ mod field_value_tests { FieldValue::Ip6Addr(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)) ); } + + #[test] + fn test_data_number_as_u8() { + assert_eq!(DataNumber::U8(42).as_u8(), Some(42)); + assert_eq!(DataNumber::U16(255).as_u8(), Some(255)); + assert_eq!(DataNumber::U16(256).as_u8(), None); + assert_eq!(DataNumber::U32(100).as_u8(), Some(100)); + assert_eq!(DataNumber::U64(300).as_u8(), None); + assert_eq!(DataNumber::I8(-1).as_u8(), None); + assert_eq!(DataNumber::I8(127).as_u8(), Some(127)); + assert_eq!(DataNumber::I16(-1).as_u8(), None); + assert_eq!(DataNumber::I32(200).as_u8(), Some(200)); + assert_eq!(DataNumber::U128(255).as_u8(), Some(255)); + assert_eq!(DataNumber::U128(256).as_u8(), None); + assert_eq!(DataNumber::Vec(vec![1, 2]).as_u8(), None); + } + + #[test] + fn test_data_number_as_u16() { + assert_eq!(DataNumber::U8(42).as_u16(), Some(42)); + assert_eq!(DataNumber::U16(65535).as_u16(), Some(65535)); + assert_eq!(DataNumber::U32(65536).as_u16(), None); + assert_eq!(DataNumber::U32(1000).as_u16(), Some(1000)); + assert_eq!(DataNumber::I8(-1).as_u16(), None); + assert_eq!(DataNumber::I16(32000).as_u16(), Some(32000)); + assert_eq!(DataNumber::I16(-1).as_u16(), None); + assert_eq!(DataNumber::U64(70000).as_u16(), None); + assert_eq!(DataNumber::Vec(vec![1]).as_u16(), None); + } + + #[test] + fn test_data_number_as_u64() { + assert_eq!(DataNumber::U8(42).as_u64(), Some(42)); + assert_eq!(DataNumber::U16(1000).as_u64(), Some(1000)); + assert_eq!(DataNumber::U32(100_000).as_u64(), Some(100_000)); + assert_eq!(DataNumber::U64(u64::MAX).as_u64(), Some(u64::MAX)); + assert_eq!(DataNumber::I8(-1).as_u64(), None); + assert_eq!(DataNumber::I64(1_000_000).as_u64(), Some(1_000_000)); + assert_eq!(DataNumber::I64(-1).as_u64(), None); + assert_eq!(DataNumber::U128(u64::MAX as u128).as_u64(), Some(u64::MAX)); + assert_eq!(DataNumber::U128(u64::MAX as u128 + 1).as_u64(), None); + assert_eq!(DataNumber::Vec(vec![1]).as_u64(), None); + } + + #[test] + fn test_field_value_as_u8() { + assert_eq!(FieldValue::DataNumber(DataNumber::U8(6)).as_u8(), Some(6)); + assert_eq!( + FieldValue::ProtocolType(ProtocolTypes::Tcp).as_u8(), + Some(6) + ); + assert_eq!(FieldValue::Float64(1.0).as_u8(), None); + assert_eq!(FieldValue::Ip4Addr(Ipv4Addr::LOCALHOST).as_u8(), None); + } + + #[test] + fn test_field_value_as_u16() { + assert_eq!( + FieldValue::DataNumber(DataNumber::U16(80)).as_u16(), + Some(80) + ); + assert_eq!(FieldValue::Float64(1.0).as_u16(), None); + } + + #[test] + fn test_field_value_as_u64() { + assert_eq!( + FieldValue::DataNumber(DataNumber::U32(1000)).as_u64(), + Some(1000) + ); + assert_eq!( + FieldValue::Duration(DurationValue::Millis { + value: 5000, + width: 4 + }) + .as_u64(), + Some(5000) + ); + assert_eq!( + FieldValue::Duration(DurationValue::Seconds { + value: 10, + width: 4 + }) + .as_u64(), + Some(10_000) + ); + assert_eq!(FieldValue::Float64(1.0).as_u64(), None); + } } diff --git a/src/variable_versions/ipfix/parser.rs b/src/variable_versions/ipfix/parser.rs index d37ee11..550889b 100644 --- a/src/variable_versions/ipfix/parser.rs +++ b/src/variable_versions/ipfix/parser.rs @@ -124,6 +124,9 @@ impl ParserFields for IPFixParser { fn pending_flows_mut(&mut self) -> &mut Option { &mut self.pending_flows } + fn metrics_mut(&mut self) -> &mut CacheMetricsInner { + &mut self.metrics + } } impl ParserConfig for IPFixParser { diff --git a/src/variable_versions/ipfix/serializer.rs b/src/variable_versions/ipfix/serializer.rs index e331c69..9d2e4d6 100644 --- a/src/variable_versions/ipfix/serializer.rs +++ b/src/variable_versions/ipfix/serializer.rs @@ -15,6 +15,11 @@ fn write_varlen_prefix( buf: &mut Vec, value_len: usize, ) -> Result<(), Box> { + if value_len == 0 { + return Err( + "IPFIX variable-length field cannot have zero length (RFC 7011 Section 7)".into(), + ); + } if value_len < 255 { buf.push(value_len as u8); } else { diff --git a/src/variable_versions/pending_flows.rs b/src/variable_versions/pending_flows.rs index 3eeeb78..c50138f 100644 --- a/src/variable_versions/pending_flows.rs +++ b/src/variable_versions/pending_flows.rs @@ -156,7 +156,10 @@ impl PendingFlowCache { return Err(ConfigError::InvalidEntrySize(config.max_entry_size_bytes)); } if config.max_total_bytes == 0 { - return Err(ConfigError::InvalidPendingCacheSize(0)); + return Err(ConfigError::InvalidPendingTotalBytes { + max_total_bytes: 0, + max_entry_size_bytes: config.max_entry_size_bytes, + }); } if config.max_total_bytes < config.max_entry_size_bytes { return Err(ConfigError::InvalidPendingTotalBytes { @@ -410,6 +413,9 @@ impl PendingFlowCache { } /// Clear all pending flows. + /// + /// Callers are responsible for recording dropped metrics via + /// `metrics.record_pending_dropped_n(cache.count())` before calling this. pub(crate) fn clear(&mut self) { self.cache.clear(); self.total_bytes = 0; diff --git a/src/variable_versions/v9/parser.rs b/src/variable_versions/v9/parser.rs index e40218f..feb2fcb 100644 --- a/src/variable_versions/v9/parser.rs +++ b/src/variable_versions/v9/parser.rs @@ -130,6 +130,9 @@ impl ParserFields for V9Parser { fn pending_flows_mut(&mut self) -> &mut Option { &mut self.pending_flows } + fn metrics_mut(&mut self) -> &mut CacheMetricsInner { + &mut self.metrics + } } impl ParserConfig for V9Parser { From 36f13b963f6f4732c6fdf0fe272499dd19a1143a Mon Sep 17 00:00:00 2001 From: mikemiles-dev Date: Sun, 15 Mar 2026 16:32:40 -0500 Subject: [PATCH 4/4] fix: clippy --- src/netflow_common.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/netflow_common.rs b/src/netflow_common.rs index 9bb0737..2286349 100644 --- a/src/netflow_common.rs +++ b/src/netflow_common.rs @@ -318,7 +318,7 @@ struct FieldCache<'a> { } impl<'a> FieldCache<'a> { - fn to_common_flowset(&self) -> NetflowCommonFlowSet { + fn to_common_flowset(self) -> NetflowCommonFlowSet { NetflowCommonFlowSet { src_addr: self.src_addr.and_then(|v| v.try_into().ok()), dst_addr: self.dst_addr.and_then(|v| v.try_into().ok()), @@ -399,7 +399,23 @@ impl<'a> FieldCache<'a> { } cache } +} + +/// Macro to check if a field matches a config mapping (primary or fallback). +/// Works for both Copy types (V9Field) and reference types (IPFixField). +macro_rules! check_field_mapping { + ($field_type:expr, $field_value:expr, $cache:expr, $config:expr, $field_name:ident) => { + if *$field_type == $config.$field_name.primary { + $cache.$field_name = Some($field_value); + } else if $config.$field_name.fallback.as_ref() == Some($field_type) + && $cache.$field_name.is_none() + { + $cache.$field_name = Some($field_value); + } + }; +} +impl<'a> FieldCache<'a> { fn from_v9_fields_with_config( fields: &'a [(V9Field, FieldValue)], config: &V9FieldMappingConfig, @@ -439,20 +455,6 @@ impl<'a> FieldCache<'a> { } } -/// Macro to check if a field matches a config mapping (primary or fallback). -/// Works for both Copy types (V9Field) and reference types (IPFixField). -macro_rules! check_field_mapping { - ($field_type:expr, $field_value:expr, $cache:expr, $config:expr, $field_name:ident) => { - if *$field_type == $config.$field_name.primary { - $cache.$field_name = Some($field_value); - } else if $config.$field_name.fallback.as_ref() == Some($field_type) - && $cache.$field_name.is_none() - { - $cache.$field_name = Some($field_value); - } - }; -} - /// Collect common flowsets from V9 flowset bodies. fn collect_v9_flowsets(value: &V9) -> Vec { let mut flowsets = vec![];