Skip to main content

bgpkit_parser/models/bgp/
community.rs

1use crate::models::Asn;
2use num_enum::{FromPrimitive, IntoPrimitive};
3use std::fmt::{Display, Formatter};
4use std::net::{Ipv4Addr, Ipv6Addr};
5
6#[derive(Debug, PartialEq, Copy, Clone, Eq)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[cfg_attr(feature = "serde", serde(untagged))]
9pub enum MetaCommunity {
10    Plain(Community),
11    Extended(ExtendedCommunity),
12    Ipv6Extended(Ipv6AddrExtCommunity),
13    Large(LargeCommunity),
14}
15
16#[derive(Debug, PartialEq, Copy, Clone, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub enum Community {
19    NoExport,
20    NoAdvertise,
21    NoExportSubConfed,
22    Custom(Asn, u16),
23}
24
25/// Large community structure as defined in [RFC8092](https://datatracker.ietf.org/doc/html/rfc8092)
26///
27/// ## Display
28///
29/// Large community is displayed as `GLOBAL_ADMINISTRATOR:LOCAL_DATA_1:LOCAL_DATA_2`
30#[derive(Debug, PartialEq, Clone, Copy, Eq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct LargeCommunity {
33    pub global_admin: u32,
34    pub local_data: [u32; 2],
35}
36
37impl LargeCommunity {
38    pub fn new(global_admin: u32, local_data: [u32; 2]) -> LargeCommunity {
39        LargeCommunity {
40            global_admin,
41            local_data,
42        }
43    }
44}
45
46/// Type definitions of extended communities
47#[derive(Debug, FromPrimitive, IntoPrimitive, PartialEq, Eq, Hash, Copy, Clone)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[repr(u8)]
50pub enum ExtendedCommunityType {
51    // transitive types
52    TransitiveTwoOctetAs = 0x00,
53    TransitiveIpv4Addr = 0x01,
54    TransitiveFourOctetAs = 0x02,
55    TransitiveOpaque = 0x03,
56
57    // non-transitive types
58    NonTransitiveTwoOctetAs = 0x40,
59    NonTransitiveIpv4Addr = 0x41,
60    NonTransitiveFourOctetAs = 0x42,
61    NonTransitiveOpaque = 0x43,
62    // the rest are either draft or experimental
63    #[num_enum(catch_all)]
64    Unknown(u8),
65}
66
67/// Extended Communities.
68///
69/// ## Overview  
70///
71/// It is a 8-octet data that has flexible definition based on the types:
72/// <https://datatracker.ietf.org/doc/html/rfc4360>
73///
74/// For more up-to-date definitions, see [IANA' website](https://www.iana.org/assignments/bgp-extended-communities/bgp-extended-communities.xhtml).
75///
76/// ```text
77///    Each Extended Community is encoded as an 8-octet quantity, as
78///    follows:
79///
80///       - Type Field  : 1 or 2 octets
81///       - Value Field : Remaining octets
82///
83///        0                   1                   2                   3
84///        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
85///       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
86///       |  Type high    |  Type low(*)  |                               |
87///       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+          Value                |
88///       |                                                               |
89///       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90///
91///       (*) Present for Extended types only, used for the Value field
92///           otherwise.
93/// ```
94#[derive(Debug, PartialEq, Clone, Copy, Eq)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub enum ExtendedCommunity {
97    TransitiveTwoOctetAs(TwoOctetAsExtCommunity),
98    TransitiveIpv4Addr(Ipv4AddrExtCommunity),
99    TransitiveFourOctetAs(FourOctetAsExtCommunity),
100    TransitiveOpaque(OpaqueExtCommunity),
101    NonTransitiveTwoOctetAs(TwoOctetAsExtCommunity),
102    NonTransitiveIpv4Addr(Ipv4AddrExtCommunity),
103    NonTransitiveFourOctetAs(FourOctetAsExtCommunity),
104    NonTransitiveOpaque(OpaqueExtCommunity),
105    /// Flow-Spec Traffic Rate - RFC 8955
106    FlowSpecTrafficRate(FlowSpecTrafficRate),
107    /// Flow-Spec Traffic Action - RFC 8955  
108    FlowSpecTrafficAction(FlowSpecTrafficAction),
109    /// Flow-Spec Redirect - RFC 8955
110    FlowSpecRedirect(TwoOctetAsExtCommunity),
111    /// Flow-Spec Traffic Marking - RFC 8955
112    FlowSpecTrafficMarking(FlowSpecTrafficMarking),
113    /// BGP Link Bandwidth - RFC 10005
114    LinkBandwidth(LinkBandwidth),
115    Raw([u8; 8]),
116}
117
118impl ExtendedCommunity {
119    pub const fn community_type(&self) -> ExtendedCommunityType {
120        use ExtendedCommunityType::*;
121        match self {
122            ExtendedCommunity::TransitiveTwoOctetAs(_) => TransitiveTwoOctetAs,
123            ExtendedCommunity::TransitiveIpv4Addr(_) => TransitiveIpv4Addr,
124            ExtendedCommunity::TransitiveFourOctetAs(_) => TransitiveFourOctetAs,
125            ExtendedCommunity::TransitiveOpaque(_) => TransitiveOpaque,
126            ExtendedCommunity::NonTransitiveTwoOctetAs(_) => NonTransitiveTwoOctetAs,
127            ExtendedCommunity::NonTransitiveIpv4Addr(_) => NonTransitiveIpv4Addr,
128            ExtendedCommunity::NonTransitiveFourOctetAs(_) => NonTransitiveFourOctetAs,
129            ExtendedCommunity::NonTransitiveOpaque(_) => NonTransitiveOpaque,
130            ExtendedCommunity::FlowSpecTrafficRate(_) => NonTransitiveTwoOctetAs,
131            ExtendedCommunity::FlowSpecTrafficAction(_) => NonTransitiveTwoOctetAs,
132            ExtendedCommunity::FlowSpecRedirect(_) => NonTransitiveTwoOctetAs,
133            ExtendedCommunity::FlowSpecTrafficMarking(_) => NonTransitiveTwoOctetAs,
134            ExtendedCommunity::LinkBandwidth(link_bandwidth) => {
135                if link_bandwidth.transitive {
136                    TransitiveTwoOctetAs
137                } else {
138                    NonTransitiveTwoOctetAs
139                }
140            }
141            ExtendedCommunity::Raw(buffer) => Unknown(buffer[0]),
142        }
143    }
144}
145
146#[derive(Debug, PartialEq, Clone, Copy, Eq)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148pub struct Ipv6AddrExtCommunity {
149    pub community_type: ExtendedCommunityType,
150    pub subtype: u8,
151    // 16 octets
152    pub global_admin: Ipv6Addr,
153    // 2 octets
154    pub local_admin: [u8; 2],
155}
156
157/// Two-Octet AS Specific Extended Community
158///
159/// <https://datatracker.ietf.org/doc/html/rfc4360#section-3.1>
160#[derive(Debug, PartialEq, Clone, Copy, Eq)]
161#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
162pub struct TwoOctetAsExtCommunity {
163    pub subtype: u8,
164    // 2 octet
165    pub global_admin: Asn,
166    // 4 octet
167    pub local_admin: [u8; 4],
168}
169
170/// Four-Octet AS Specific Extended Community
171///
172/// <https://datatracker.ietf.org/doc/html/rfc5668#section-2>
173#[derive(Debug, PartialEq, Clone, Copy, Eq)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
175pub struct FourOctetAsExtCommunity {
176    pub subtype: u8,
177    // 4 octet
178    pub global_admin: Asn,
179    // 2 octet
180    pub local_admin: [u8; 2],
181}
182
183/// IPv4 Address Specific Extended Community
184///
185/// <https://datatracker.ietf.org/doc/html/rfc4360#section-3.2>
186#[derive(Debug, PartialEq, Clone, Copy, Eq)]
187#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
188pub struct Ipv4AddrExtCommunity {
189    pub subtype: u8,
190    // 4 octet
191    pub global_admin: Ipv4Addr,
192    // 2 octet
193    pub local_admin: [u8; 2],
194}
195
196/// Opaque Extended Community
197///
198/// <https://datatracker.ietf.org/doc/html/rfc4360#section-3.3>
199#[derive(Debug, PartialEq, Clone, Copy, Eq)]
200#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
201pub struct OpaqueExtCommunity {
202    pub subtype: u8,
203    // 6 octet
204    pub value: [u8; 6],
205}
206
207/// BGP Link Bandwidth Extended Community
208///
209/// RFC 10005 - subtype 0x04
210#[derive(Debug, Clone, Copy)]
211#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
212pub struct LinkBandwidth {
213    /// Global Administrator value (2 octets)
214    pub global_admin: u16,
215    /// Bandwidth in bytes per second (IEEE 754 single-precision float)
216    pub bandwidth: f32,
217    /// Whether the community uses type 0x00 rather than non-transitive type 0x40
218    pub transitive: bool,
219}
220
221impl PartialEq for LinkBandwidth {
222    fn eq(&self, other: &Self) -> bool {
223        self.global_admin == other.global_admin
224            && self.bandwidth.to_bits() == other.bandwidth.to_bits()
225            && self.transitive == other.transitive
226    }
227}
228
229impl Eq for LinkBandwidth {}
230
231/// Flow-Spec Traffic Rate Extended Community
232///
233/// RFC 8955 - subtype 0x06
234#[derive(Debug, Clone, Copy)]
235#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
236pub struct FlowSpecTrafficRate {
237    /// AS Number (2 octets)
238    pub as_number: u16,
239    /// Rate in bytes per second (IEEE 754 single precision float)
240    pub rate_bytes_per_sec: f32,
241}
242
243impl PartialEq for FlowSpecTrafficRate {
244    fn eq(&self, other: &Self) -> bool {
245        self.as_number == other.as_number
246            && self.rate_bytes_per_sec.to_bits() == other.rate_bytes_per_sec.to_bits()
247    }
248}
249
250impl Eq for FlowSpecTrafficRate {}
251
252/// Flow-Spec Traffic Action Extended Community
253///
254/// RFC 8955 - subtype 0x07  
255#[derive(Debug, PartialEq, Clone, Copy, Eq)]
256#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
257pub struct FlowSpecTrafficAction {
258    /// AS Number (2 octets)
259    pub as_number: u16,
260    /// Terminal action - stop processing additional flow-specs
261    pub terminal: bool,
262    /// Sample action - enable traffic sampling
263    pub sample: bool,
264}
265
266/// Flow-Spec Traffic Marking Extended Community
267///
268/// RFC 8955 - subtype 0x09
269#[derive(Debug, PartialEq, Clone, Copy, Eq)]
270#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
271pub struct FlowSpecTrafficMarking {
272    /// AS Number (2 octets)
273    pub as_number: u16,
274    /// DSCP value (6 bits)
275    pub dscp: u8,
276}
277
278impl FlowSpecTrafficRate {
279    /// Create a new traffic rate community
280    pub fn new(as_number: u16, rate_bytes_per_sec: f32) -> Self {
281        Self {
282            as_number,
283            rate_bytes_per_sec,
284        }
285    }
286
287    /// Create a "discard all traffic" rate (rate = 0.0)
288    pub fn discard(as_number: u16) -> Self {
289        Self {
290            as_number,
291            rate_bytes_per_sec: 0.0,
292        }
293    }
294}
295
296impl FlowSpecTrafficAction {
297    /// Create a new traffic action community
298    pub fn new(as_number: u16, terminal: bool, sample: bool) -> Self {
299        Self {
300            as_number,
301            terminal,
302            sample,
303        }
304    }
305}
306
307impl FlowSpecTrafficMarking {
308    /// Create a new traffic marking community
309    pub fn new(as_number: u16, dscp: u8) -> Self {
310        Self {
311            as_number,
312            dscp: dscp & 0x3F,
313        } // Mask to 6 bits
314    }
315}
316
317/////////////
318// DISPLAY //
319/////////////
320
321struct ToHexString<'a>(&'a [u8]);
322
323impl Display for ToHexString<'_> {
324    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
325        for byte in self.0 {
326            write!(f, "{byte:02X}")?;
327        }
328        Ok(())
329    }
330}
331
332impl Display for Community {
333    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
334        match self {
335            Community::NoExport => write!(f, "no-export"),
336            Community::NoAdvertise => write!(f, "no-advertise"),
337            Community::NoExportSubConfed => write!(f, "no-export-sub-confed"),
338            Community::Custom(asn, value) => write!(f, "{asn}:{value}"),
339        }
340    }
341}
342
343impl Display for LargeCommunity {
344    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
345        write!(
346            f,
347            "{}:{}:{}",
348            self.global_admin, self.local_data[0], self.local_data[1]
349        )
350    }
351}
352
353impl Display for ExtendedCommunity {
354    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
355        let ec_type = u8::from(self.community_type());
356        match self {
357            ExtendedCommunity::TransitiveTwoOctetAs(ec)
358            | ExtendedCommunity::NonTransitiveTwoOctetAs(ec) => {
359                write!(
360                    f,
361                    "{}:{}:{}:{}",
362                    ec_type,
363                    ec.subtype,
364                    ec.global_admin,
365                    ToHexString(&ec.local_admin)
366                )
367            }
368            ExtendedCommunity::TransitiveIpv4Addr(ec)
369            | ExtendedCommunity::NonTransitiveIpv4Addr(ec) => {
370                write!(
371                    f,
372                    "{}:{}:{}:{}",
373                    ec_type,
374                    ec.subtype,
375                    ec.global_admin,
376                    ToHexString(&ec.local_admin)
377                )
378            }
379            ExtendedCommunity::TransitiveFourOctetAs(ec)
380            | ExtendedCommunity::NonTransitiveFourOctetAs(ec) => {
381                write!(
382                    f,
383                    "{}:{}:{}:{}",
384                    ec_type,
385                    ec.subtype,
386                    ec.global_admin,
387                    ToHexString(&ec.local_admin)
388                )
389            }
390            ExtendedCommunity::TransitiveOpaque(ec)
391            | ExtendedCommunity::NonTransitiveOpaque(ec) => {
392                write!(f, "{}:{}:{}", ec_type, ec.subtype, ToHexString(&ec.value))
393            }
394            ExtendedCommunity::FlowSpecTrafficRate(rate) => {
395                write!(
396                    f,
397                    "rate:{} bytes/sec (AS {})",
398                    rate.rate_bytes_per_sec, rate.as_number
399                )
400            }
401            ExtendedCommunity::FlowSpecTrafficAction(action) => {
402                let mut flags = Vec::new();
403                if action.terminal {
404                    flags.push("terminal");
405                }
406                if action.sample {
407                    flags.push("sample");
408                }
409                write!(f, "action:{} (AS {})", flags.join(","), action.as_number)
410            }
411            ExtendedCommunity::FlowSpecRedirect(redirect) => {
412                write!(
413                    f,
414                    "redirect:AS{}:{}",
415                    redirect.global_admin,
416                    ToHexString(&redirect.local_admin)
417                )
418            }
419            ExtendedCommunity::FlowSpecTrafficMarking(marking) => {
420                write!(f, "mark:DSCP{} (AS {})", marking.dscp, marking.as_number)
421            }
422            ExtendedCommunity::LinkBandwidth(link_bandwidth) => {
423                write!(
424                    f,
425                    "{}:{}:{}:{}",
426                    ec_type, 0x04, link_bandwidth.global_admin, link_bandwidth.bandwidth
427                )
428            }
429            ExtendedCommunity::Raw(ec) => {
430                write!(f, "{}", ToHexString(ec))
431            }
432        }
433    }
434}
435
436impl Display for Ipv6AddrExtCommunity {
437    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
438        write!(
439            f,
440            "{}:{}:{}:{}",
441            u8::from(self.community_type),
442            self.subtype,
443            self.global_admin,
444            ToHexString(&self.local_admin)
445        )
446    }
447}
448
449impl Display for MetaCommunity {
450    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
451        match self {
452            MetaCommunity::Plain(c) => write!(f, "{c}"),
453            MetaCommunity::Extended(c) => write!(f, "{c}"),
454            MetaCommunity::Large(c) => write!(f, "{c}"),
455            MetaCommunity::Ipv6Extended(c) => write!(f, "{c}"),
456        }
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn test_large_community_new() {
466        let global_admin = 56;
467        let local_data = [3, 4];
468        let large_comm = LargeCommunity::new(global_admin, local_data);
469        assert_eq!(large_comm.global_admin, global_admin);
470        assert_eq!(large_comm.local_data, local_data);
471    }
472
473    #[test]
474    fn test_extended_community_community_type() {
475        let two_octet_as_ext_comm = TwoOctetAsExtCommunity {
476            subtype: 0,
477            global_admin: Asn::new_32bit(0),
478            local_admin: [0; 4],
479        };
480        let extended_community = ExtendedCommunity::TransitiveTwoOctetAs(two_octet_as_ext_comm);
481        assert_eq!(
482            extended_community.community_type(),
483            ExtendedCommunityType::TransitiveTwoOctetAs
484        );
485    }
486
487    #[test]
488    fn test_display_community() {
489        assert_eq!(format!("{}", Community::NoExport), "no-export");
490        assert_eq!(format!("{}", Community::NoAdvertise), "no-advertise");
491        assert_eq!(
492            format!("{}", Community::NoExportSubConfed),
493            "no-export-sub-confed"
494        );
495        assert_eq!(
496            format!("{}", Community::Custom(Asn::new_32bit(64512), 100)),
497            "64512:100"
498        );
499    }
500
501    #[test]
502    fn test_display_large_community() {
503        let large_community = LargeCommunity::new(1, [2, 3]);
504        assert_eq!(format!("{large_community}"), "1:2:3");
505    }
506
507    #[test]
508    fn test_display_extended_community() {
509        let two_octet_as_ext_comm = TwoOctetAsExtCommunity {
510            subtype: 0,
511            global_admin: Asn::new_32bit(0),
512            local_admin: [0; 4],
513        };
514        let extended_community = ExtendedCommunity::TransitiveTwoOctetAs(two_octet_as_ext_comm);
515        assert_eq!(format!("{extended_community}"), "0:0:0:00000000");
516
517        let two_octet_as_ext_comm = TwoOctetAsExtCommunity {
518            subtype: 0,
519            global_admin: Asn::new_32bit(0),
520            local_admin: [0; 4],
521        };
522        let extended_community = ExtendedCommunity::NonTransitiveTwoOctetAs(two_octet_as_ext_comm);
523        assert_eq!(format!("{extended_community}"), "64:0:0:00000000");
524
525        let ipv4_ext_comm = Ipv4AddrExtCommunity {
526            subtype: 1,
527            global_admin: "192.168.1.1".parse().unwrap(),
528            local_admin: [5, 6],
529        };
530        let extended_community = ExtendedCommunity::TransitiveIpv4Addr(ipv4_ext_comm);
531        assert_eq!(format!("{extended_community}"), "1:1:192.168.1.1:0506");
532
533        let ipv4_ext_comm = Ipv4AddrExtCommunity {
534            subtype: 1,
535            global_admin: "192.168.1.1".parse().unwrap(),
536            local_admin: [5, 6],
537        };
538        let extended_community = ExtendedCommunity::NonTransitiveIpv4Addr(ipv4_ext_comm);
539        assert_eq!(format!("{extended_community}"), "65:1:192.168.1.1:0506");
540
541        let four_octet_as_ext_comm = FourOctetAsExtCommunity {
542            subtype: 2,
543            global_admin: Asn::new_32bit(64512),
544            local_admin: [7, 8],
545        };
546        let extended_community = ExtendedCommunity::TransitiveFourOctetAs(four_octet_as_ext_comm);
547        assert_eq!(format!("{extended_community}"), "2:2:64512:0708");
548
549        let four_octet_as_ext_comm = FourOctetAsExtCommunity {
550            subtype: 2,
551            global_admin: Asn::new_32bit(64512),
552            local_admin: [7, 8],
553        };
554        let extended_community =
555            ExtendedCommunity::NonTransitiveFourOctetAs(four_octet_as_ext_comm);
556        assert_eq!(format!("{extended_community}"), "66:2:64512:0708");
557
558        let opaque_ext_comm = OpaqueExtCommunity {
559            subtype: 3,
560            value: [9, 10, 11, 12, 13, 14],
561        };
562        let extended_community = ExtendedCommunity::TransitiveOpaque(opaque_ext_comm);
563        assert_eq!(format!("{extended_community}"), "3:3:090A0B0C0D0E");
564
565        let opaque_ext_comm = OpaqueExtCommunity {
566            subtype: 3,
567            value: [9, 10, 11, 12, 13, 14],
568        };
569        let extended_community = ExtendedCommunity::NonTransitiveOpaque(opaque_ext_comm);
570        assert_eq!(format!("{extended_community}"), "67:3:090A0B0C0D0E");
571
572        let link_bandwidth = ExtendedCommunity::LinkBandwidth(LinkBandwidth {
573            global_admin: 1,
574            bandwidth: 1000.0,
575            transitive: true,
576        });
577        assert_eq!(format!("{link_bandwidth}"), "0:4:1:1000");
578
579        let link_bandwidth = ExtendedCommunity::LinkBandwidth(LinkBandwidth {
580            global_admin: 1,
581            bandwidth: 1000.0,
582            transitive: false,
583        });
584        assert_eq!(format!("{link_bandwidth}"), "64:4:1:1000");
585
586        let raw_ext_comm = [0, 1, 2, 3, 4, 5, 6, 7];
587        let extended_community = ExtendedCommunity::Raw(raw_ext_comm);
588        assert_eq!(format!("{extended_community}"), "0001020304050607");
589    }
590
591    #[test]
592    fn test_display_ipv6_addr_ext_community() {
593        let ipv6_addr_ext_comm = Ipv6AddrExtCommunity {
594            community_type: ExtendedCommunityType::TransitiveTwoOctetAs,
595            subtype: 0,
596            global_admin: "2001:db8::8a2e:370:7334".parse().unwrap(),
597            local_admin: [0, 1],
598        };
599        assert_eq!(
600            format!("{ipv6_addr_ext_comm}"),
601            "0:0:2001:db8::8a2e:370:7334:0001"
602        );
603    }
604
605    #[test]
606    fn test_display_meta_community() {
607        let large_community = LargeCommunity::new(1, [2, 3]);
608        let meta_community = MetaCommunity::Large(large_community);
609        assert_eq!(format!("{meta_community}"), "1:2:3");
610    }
611
612    #[test]
613    fn test_to_hex_string() {
614        // Test empty array
615        assert_eq!(format!("{}", ToHexString(&[])), "");
616
617        // Test single byte
618        assert_eq!(format!("{}", ToHexString(&[0x0A])), "0A");
619
620        // Test multiple bytes
621        assert_eq!(format!("{}", ToHexString(&[0x0A, 0x0B, 0x0C])), "0A0B0C");
622
623        // Test zero byte
624        assert_eq!(format!("{}", ToHexString(&[0x00])), "00");
625
626        // Test byte with value > 0x0F (needs two hex digits)
627        assert_eq!(format!("{}", ToHexString(&[0x10])), "10");
628
629        // Test mixed bytes
630        assert_eq!(
631            format!("{}", ToHexString(&[0x00, 0x0F, 0x10, 0xFF])),
632            "000F10FF"
633        );
634    }
635
636    #[test]
637    #[cfg(feature = "serde")]
638    fn test_serde() {
639        let meta_community = MetaCommunity::Large(LargeCommunity::new(1, [2, 3]));
640        let serialized = serde_json::to_string(&meta_community).unwrap();
641        let deserialized: MetaCommunity = serde_json::from_str(&serialized).unwrap();
642        assert_eq!(meta_community, deserialized);
643
644        let meta_community = MetaCommunity::Extended(ExtendedCommunity::TransitiveTwoOctetAs(
645            TwoOctetAsExtCommunity {
646                subtype: 0,
647                global_admin: Asn::new_32bit(0),
648                local_admin: [0; 4],
649            },
650        ));
651        let serialized = serde_json::to_string(&meta_community).unwrap();
652        let deserialized: MetaCommunity = serde_json::from_str(&serialized).unwrap();
653        assert_eq!(meta_community, deserialized);
654
655        let meta_community = MetaCommunity::Plain(Community::NoExport);
656        let serialized = serde_json::to_string(&meta_community).unwrap();
657        let deserialized: MetaCommunity = serde_json::from_str(&serialized).unwrap();
658        assert_eq!(meta_community, deserialized);
659
660        let meta_community = MetaCommunity::Ipv6Extended(Ipv6AddrExtCommunity {
661            community_type: ExtendedCommunityType::TransitiveTwoOctetAs,
662            subtype: 0,
663            global_admin: "2001:db8::8a2e:370:7334".parse().unwrap(),
664            local_admin: [0, 1],
665        });
666        let serialized = serde_json::to_string(&meta_community).unwrap();
667        let deserialized: MetaCommunity = serde_json::from_str(&serialized).unwrap();
668        assert_eq!(meta_community, deserialized);
669    }
670
671    #[test]
672    fn test_flowspec_traffic_rate() {
673        let rate = FlowSpecTrafficRate::new(64512, 1000.0);
674        assert_eq!(rate.as_number, 64512);
675        assert_eq!(rate.rate_bytes_per_sec, 1000.0);
676
677        let discard = FlowSpecTrafficRate::discard(64512);
678        assert_eq!(discard.rate_bytes_per_sec, 0.0);
679    }
680
681    #[test]
682    fn test_flowspec_traffic_action() {
683        let action = FlowSpecTrafficAction::new(64512, true, false);
684        assert_eq!(action.as_number, 64512);
685        assert!(action.terminal);
686        assert!(!action.sample);
687    }
688
689    #[test]
690    fn test_flowspec_traffic_marking() {
691        let marking = FlowSpecTrafficMarking::new(64512, 46); // EF DSCP
692        assert_eq!(marking.as_number, 64512);
693        assert_eq!(marking.dscp, 46);
694
695        // Test DSCP masking
696        let masked = FlowSpecTrafficMarking::new(64512, 255);
697        assert_eq!(masked.dscp, 63); // Should be masked to 6 bits
698    }
699
700    #[test]
701    fn test_flowspec_community_display() {
702        let rate = ExtendedCommunity::FlowSpecTrafficRate(FlowSpecTrafficRate::new(64512, 1000.0));
703        assert_eq!(format!("{}", rate), "rate:1000 bytes/sec (AS 64512)");
704
705        let action =
706            ExtendedCommunity::FlowSpecTrafficAction(FlowSpecTrafficAction::new(64512, true, true));
707        assert_eq!(format!("{}", action), "action:terminal,sample (AS 64512)");
708
709        let marking =
710            ExtendedCommunity::FlowSpecTrafficMarking(FlowSpecTrafficMarking::new(64512, 46));
711        assert_eq!(format!("{}", marking), "mark:DSCP46 (AS 64512)");
712    }
713}