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