Skip to main content

rustbgpd_wire/
attribute.rs

1use std::fmt;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3
4use bytes::Bytes;
5
6use crate::capability::{Afi, Safi};
7use crate::constants::{as_path_segment, attr_flags, attr_type};
8use crate::error::DecodeError;
9use crate::nlri::{NlriEntry, Prefix};
10use crate::notification::update_subcode;
11
12/// Origin attribute values per RFC 4271 §5.1.1.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(u8)]
15pub enum Origin {
16    /// Learned via IGP.
17    Igp = 0,
18    /// Learned via EGP.
19    Egp = 1,
20    /// Origin undetermined.
21    Incomplete = 2,
22}
23
24impl Origin {
25    /// Create from a raw byte value.
26    #[must_use]
27    pub fn from_u8(value: u8) -> Option<Self> {
28        match value {
29            0 => Some(Self::Igp),
30            1 => Some(Self::Egp),
31            2 => Some(Self::Incomplete),
32            _ => None,
33        }
34    }
35}
36
37impl std::fmt::Display for Origin {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::Igp => write!(f, "IGP"),
41            Self::Egp => write!(f, "EGP"),
42            Self::Incomplete => write!(f, "INCOMPLETE"),
43        }
44    }
45}
46
47/// `AS_PATH` segment types per RFC 4271 §4.3.
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49pub enum AsPathSegment {
50    /// `AS_SET` — unordered set of ASNs.
51    AsSet(Vec<u32>),
52    /// `AS_SEQUENCE` — ordered sequence of ASNs.
53    AsSequence(Vec<u32>),
54}
55
56/// `AS_PATH` attribute.
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct AsPath {
59    /// Ordered list of path segments.
60    pub segments: Vec<AsPathSegment>,
61}
62
63impl AsPath {
64    /// Count the total number of ASNs in the path for best-path comparison.
65    /// `AS_SET` counts as 1 regardless of size (RFC 4271 §9.1.2.2).
66    #[must_use]
67    pub fn len(&self) -> usize {
68        self.segments
69            .iter()
70            .map(|seg| match seg {
71                AsPathSegment::AsSequence(asns) => asns.len(),
72                AsPathSegment::AsSet(_) => 1,
73            })
74            .sum()
75    }
76
77    /// Returns `true` if the path has no segments.
78    #[must_use]
79    pub fn is_empty(&self) -> bool {
80        self.segments.is_empty()
81    }
82
83    /// Returns true if `asn` appears in any segment (`AS_SEQUENCE` or `AS_SET`).
84    /// Used for loop detection per RFC 4271 §9.1.2.
85    #[must_use]
86    pub fn contains_asn(&self, asn: u32) -> bool {
87        self.segments.iter().any(|seg| match seg {
88            AsPathSegment::AsSequence(asns) | AsPathSegment::AsSet(asns) => asns.contains(&asn),
89        })
90    }
91
92    /// Extract the origin ASN from the `AS_PATH`.
93    ///
94    /// The origin AS is the last ASN in the rightmost `AS_SEQUENCE` segment.
95    /// Returns `None` if the path has no `AS_SEQUENCE` segments or all
96    /// `AS_SEQUENCE` segments are empty.
97    #[must_use]
98    pub fn origin_asn(&self) -> Option<u32> {
99        self.segments.iter().rev().find_map(|seg| match seg {
100            AsPathSegment::AsSequence(asns) => asns.last().copied(),
101            AsPathSegment::AsSet(_) => None,
102        })
103    }
104
105    /// Returns `true` if every ASN in the path is a private ASN.
106    ///
107    /// Returns `false` for empty paths (no ASNs to check).
108    #[must_use]
109    pub fn all_private(&self) -> bool {
110        let mut count = 0;
111        for seg in &self.segments {
112            match seg {
113                AsPathSegment::AsSequence(asns) | AsPathSegment::AsSet(asns) => {
114                    for asn in asns {
115                        count += 1;
116                        if !is_private_asn(*asn) {
117                            return false;
118                        }
119                    }
120                }
121            }
122        }
123        count > 0
124    }
125
126    /// Convert to a string representation for regex matching.
127    ///
128    /// `AS_SEQUENCE` segments produce space-separated ASNs.
129    /// `AS_SET` segments produce `{ASN1 ASN2}` (curly braces, space-separated).
130    /// Multiple segments are space-separated.
131    ///
132    /// Examples: `"65001 65002"`, `"65001 {65003 65004}"`, `""` (empty path).
133    #[must_use]
134    pub fn to_aspath_string(&self) -> String {
135        let mut parts = Vec::new();
136        for seg in &self.segments {
137            match seg {
138                AsPathSegment::AsSequence(asns) => {
139                    for asn in asns {
140                        parts.push(asn.to_string());
141                    }
142                }
143                AsPathSegment::AsSet(asns) => {
144                    let inner: Vec<String> = asns.iter().map(ToString::to_string).collect();
145                    parts.push(format!("{{{}}}", inner.join(" ")));
146                }
147            }
148        }
149        parts.join(" ")
150    }
151}
152
153/// Returns `true` if the given ASN falls in a private-use range.
154///
155/// Private ranges (RFC 5398 + RFC 6996):
156/// - 16-bit: 64512–65534
157/// - 32-bit: 4200000000–4294967294
158#[must_use]
159pub fn is_private_asn(asn: u32) -> bool {
160    (64512..=65534).contains(&asn) || (4_200_000_000..=4_294_967_294).contains(&asn)
161}
162
163/// RFC 4760 `MP_REACH_NLRI` attribute (type code 14).
164///
165/// Uses [`NlriEntry`] to carry Add-Path path IDs alongside each prefix.
166/// For non-Add-Path peers, `path_id` is always 0.
167#[derive(Debug, Clone, PartialEq, Eq, Hash)]
168pub struct MpReachNlri {
169    /// Address family.
170    pub afi: Afi,
171    /// Sub-address family.
172    pub safi: Safi,
173    /// Next-hop address for the announced prefixes.
174    ///
175    /// For IPv6, this stores only the global address. When a 32-byte
176    /// next-hop is received (global + link-local per RFC 4760 §3), the
177    /// decoder extracts the first 16 bytes (global) and discards the
178    /// link-local portion. `IpAddr` can only hold a single address, and
179    /// link-local next-hops are not needed for routing decisions.
180    ///
181    /// RFC 8950 allows IPv4 unicast NLRI to use an IPv6 next hop in
182    /// `MP_REACH_NLRI`, so this field may be IPv6 even when `afi == Ipv4`.
183    ///
184    /// For `FlowSpec` (SAFI 133), next-hop length is 0 and this field is
185    /// unused (defaults to `0.0.0.0`).
186    pub next_hop: IpAddr,
187    /// Announced NLRI entries.
188    pub announced: Vec<NlriEntry>,
189    /// `FlowSpec` NLRI rules (RFC 8955). Populated only when `safi == FlowSpec`.
190    pub flowspec_announced: Vec<crate::flowspec::FlowSpecRule>,
191    /// EVPN NLRI routes (RFC 7432). Populated only when `safi == Evpn`.
192    pub evpn_announced: Vec<crate::evpn::EvpnRoute>,
193}
194
195/// RFC 4760 `MP_UNREACH_NLRI` attribute (type 15).
196///
197/// Uses [`NlriEntry`] to carry Add-Path path IDs alongside each prefix.
198/// For non-Add-Path peers, `path_id` is always 0.
199#[derive(Debug, Clone, PartialEq, Eq, Hash)]
200pub struct MpUnreachNlri {
201    /// Address family.
202    pub afi: Afi,
203    /// Sub-address family.
204    pub safi: Safi,
205    /// Withdrawn NLRI entries.
206    pub withdrawn: Vec<NlriEntry>,
207    /// `FlowSpec` NLRI rules withdrawn (RFC 8955). Populated only when `safi == FlowSpec`.
208    pub flowspec_withdrawn: Vec<crate::flowspec::FlowSpecRule>,
209    /// EVPN NLRI routes withdrawn (RFC 7432). Populated only when `safi == Evpn`.
210    pub evpn_withdrawn: Vec<crate::evpn::EvpnRoute>,
211}
212
213/// RFC 4360 Extended Community — 8-byte value stored as `u64`.
214///
215/// Wire layout: type (1) + sub-type (1) + value (6).
216/// Bit 6 of the type byte: 0 = transitive, 1 = non-transitive.
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
218pub struct ExtendedCommunity(u64);
219
220impl ExtendedCommunity {
221    /// Create from a raw 8-byte value.
222    #[must_use]
223    pub fn new(raw: u64) -> Self {
224        Self(raw)
225    }
226
227    /// Return the raw 8-byte value.
228    #[must_use]
229    pub fn as_u64(self) -> u64 {
230        self.0
231    }
232
233    /// High byte — IANA-assigned type.
234    #[must_use]
235    pub fn type_byte(self) -> u8 {
236        (self.0 >> 56) as u8
237    }
238
239    /// Second byte — sub-type within the type.
240    #[must_use]
241    pub fn subtype(self) -> u8 {
242        self.0.to_be_bytes()[1]
243    }
244
245    /// Transitive if bit 6 of the type byte is 0.
246    #[must_use]
247    pub fn is_transitive(self) -> bool {
248        self.type_byte() & 0x40 == 0
249    }
250
251    /// Bytes 2-7 of the community value.
252    #[must_use]
253    pub fn value_bytes(self) -> [u8; 6] {
254        let b = self.0.to_be_bytes();
255        [b[2], b[3], b[4], b[5], b[6], b[7]]
256    }
257
258    /// Decode as Route Target (sub-type 0x02).
259    ///
260    /// Returns `(global_admin, local_admin)` as raw u32 values. The
261    /// interpretation of `global_admin` depends on the type byte:
262    /// - Type 0x00 (2-octet AS specific): global = ASN (fits u16), local = u32
263    /// - Type 0x01 (IPv4 address specific): global = IPv4 addr as u32, local = u16
264    /// - Type 0x02 (4-octet AS specific): global = ASN (u32), local = u16
265    ///
266    /// Callers that need to distinguish these encodings (e.g. for display as
267    /// `RT:192.0.2.1:100` vs `RT:65001:100`) must also check [`type_byte()`](Self::type_byte).
268    #[must_use]
269    pub fn route_target(self) -> Option<(u32, u32)> {
270        if self.subtype() != 0x02 {
271            return None;
272        }
273        self.decode_two_part()
274    }
275
276    /// Decode as Route Origin (sub-type 0x03).
277    ///
278    /// Same layout as [`route_target()`](Self::route_target) — returns raw
279    /// `(global_admin, local_admin)` with the same type-byte-dependent
280    /// interpretation. Check [`type_byte()`](Self::type_byte) to distinguish
281    /// 2-octet AS, IPv4-address, and 4-octet AS encodings.
282    #[must_use]
283    pub fn route_origin(self) -> Option<(u32, u32)> {
284        if self.subtype() != 0x03 {
285            return None;
286        }
287        self.decode_two_part()
288    }
289
290    // -------------------------------------------------------------------
291    // EVPN-specific typed accessors (RFC 7432 / RFC 8365 / RFC 9135)
292    // -------------------------------------------------------------------
293
294    /// Decode as BGP Encapsulation Extended Community (RFC 9012 §4.1, encoded
295    /// per the widely-deployed RFC 5512 layout: 4-byte reserved + 2-byte
296    /// Tunnel Type). Type 0x03, subtype 0x0C.
297    ///
298    /// Returns the Tunnel Type code. For VXLAN-EVPN (RFC 8365), the value is
299    /// 8. Other common values: 7 = NVGRE, 11 = MPLS-over-GRE.
300    ///
301    /// The reserved bytes are intentionally not validated here: RFC 5512
302    /// specifies MUST-zero on send, ignored on receive. FRR, `GoBGP`, Cisco,
303    /// and Juniper all emit zeros in practice; rejecting non-zero reserves
304    /// would break interop in the rare case an unknown implementation
305    /// re-purposes those bytes. Consumers should treat the returned
306    /// `tunnel_type` as the semantic signal.
307    #[must_use]
308    pub fn as_bgp_encapsulation(self) -> Option<u16> {
309        if self.type_byte() & 0x3F != 0x03 || self.subtype() != 0x0C {
310            return None;
311        }
312        let v = self.value_bytes();
313        Some(u16::from_be_bytes([v[4], v[5]]))
314    }
315
316    /// Construct a BGP Encapsulation Extended Community (RFC 9012 §4.1).
317    ///
318    /// Writes 4 bytes of reserved zero followed by the 16-bit tunnel type.
319    #[must_use]
320    pub fn bgp_encapsulation(tunnel_type: u16) -> Self {
321        let tt = tunnel_type.to_be_bytes();
322        let raw = u64::from_be_bytes([0x03, 0x0C, 0, 0, 0, 0, tt[0], tt[1]]);
323        Self(raw)
324    }
325
326    /// Decode as MAC Mobility Extended Community (RFC 7432 §7.7).
327    /// Type 0x06, subtype 0x00.
328    ///
329    /// Returns `(sticky, sequence_number)`. The sticky bit (bit 0 of the
330    /// flags byte) marks the MAC as non-movable; receivers must not displace
331    /// a sticky MAC with a higher-sequence non-sticky advertisement.
332    #[must_use]
333    pub fn as_mac_mobility(self) -> Option<(bool, u32)> {
334        if self.type_byte() & 0x3F != 0x06 || self.subtype() != 0x00 {
335            return None;
336        }
337        let v = self.value_bytes();
338        let sticky = (v[0] & 0x01) != 0;
339        let seq = u32::from_be_bytes([v[2], v[3], v[4], v[5]]);
340        Some((sticky, seq))
341    }
342
343    /// Construct a MAC Mobility Extended Community (RFC 7432 §7.7).
344    #[must_use]
345    pub fn mac_mobility(sticky: bool, sequence: u32) -> Self {
346        let flags = u8::from(sticky);
347        let s = sequence.to_be_bytes();
348        let raw = u64::from_be_bytes([0x06, 0x00, flags, 0, s[0], s[1], s[2], s[3]]);
349        Self(raw)
350    }
351
352    /// Decode as ESI Label Extended Community (RFC 7432 §7.5).
353    /// Type 0x06, subtype 0x01.
354    ///
355    /// Returns `(single_active, label)`. The single-active flag (bit 0 of
356    /// the flags byte) signals single-active multi-homing mode.
357    #[must_use]
358    pub fn as_esi_label(self) -> Option<(bool, u32)> {
359        if self.type_byte() & 0x3F != 0x06 || self.subtype() != 0x01 {
360            return None;
361        }
362        let v = self.value_bytes();
363        let single_active = (v[0] & 0x01) != 0;
364        let label = (u32::from(v[3]) << 16) | (u32::from(v[4]) << 8) | u32::from(v[5]);
365        Some((single_active, label))
366    }
367
368    /// Construct an ESI Label Extended Community (RFC 7432 §7.5).
369    ///
370    /// `label` is a 24-bit MPLS label or VXLAN VNI; high 8 bits are masked.
371    #[must_use]
372    pub fn esi_label(single_active: bool, label: u32) -> Self {
373        let flags = u8::from(single_active);
374        let l = label & 0x00FF_FFFF;
375        #[expect(clippy::cast_possible_truncation)]
376        let raw = u64::from_be_bytes([
377            0x06,
378            0x01,
379            flags,
380            0,
381            0,
382            (l >> 16) as u8,
383            (l >> 8) as u8,
384            l as u8,
385        ]);
386        Self(raw)
387    }
388
389    /// Decode as ES-Import Route Target Extended Community (RFC 7432 §7.6).
390    /// Type 0x06, subtype 0x02.
391    ///
392    /// Returns the 6-byte MAC address that serves as the import target for
393    /// Type 4 ES routes.
394    #[must_use]
395    pub fn as_es_import_rt(self) -> Option<[u8; 6]> {
396        if self.type_byte() & 0x3F != 0x06 || self.subtype() != 0x02 {
397            return None;
398        }
399        Some(self.value_bytes())
400    }
401
402    /// Construct an ES-Import Route Target Extended Community.
403    #[must_use]
404    pub fn es_import_rt(mac: [u8; 6]) -> Self {
405        let raw = u64::from_be_bytes([0x06, 0x02, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]]);
406        Self(raw)
407    }
408
409    /// Decode as Router MAC Extended Community (RFC 9135 §4.1).
410    /// Type 0x06, subtype 0x03.
411    ///
412    /// Returns the 6-byte router MAC used for symmetric IRB.
413    #[must_use]
414    pub fn as_router_mac(self) -> Option<[u8; 6]> {
415        if self.type_byte() & 0x3F != 0x06 || self.subtype() != 0x03 {
416            return None;
417        }
418        Some(self.value_bytes())
419    }
420
421    /// Construct a Router MAC Extended Community (RFC 9135 §4.1).
422    #[must_use]
423    pub fn router_mac(mac: [u8; 6]) -> Self {
424        let raw = u64::from_be_bytes([0x06, 0x03, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]]);
425        Self(raw)
426    }
427
428    /// Decode as Default Gateway Extended Community (RFC 4761 §3.2.5 /
429    /// RFC 7432). Type 0x03, subtype 0x0D. This is a flag-only community:
430    /// presence is the signal and the 6-byte value field must be all zeros.
431    /// Malformed advertisements with non-zero value bytes are treated as
432    /// non-matches rather than silently accepted — downstream policy and
433    /// validation consumers treat this accessor as semantic truth.
434    #[must_use]
435    pub fn as_default_gateway(self) -> bool {
436        self.type_byte() & 0x3F == 0x03 && self.subtype() == 0x0D && self.value_bytes() == [0u8; 6]
437    }
438
439    /// Construct a Default Gateway Extended Community.
440    #[must_use]
441    pub fn default_gateway() -> Self {
442        let raw = u64::from_be_bytes([0x03, 0x0D, 0, 0, 0, 0, 0, 0]);
443        Self(raw)
444    }
445
446    /// Decode the 6-byte value field as `(global_admin, local_admin)`.
447    ///
448    /// Handles all three RFC 4360 two-part layouts (2-octet AS, IPv4, 4-octet
449    /// AS). Returns raw u32 values — the caller decides how to interpret
450    /// `global_admin` (ASN vs IPv4 address) based on `type_byte()`.
451    fn decode_two_part(self) -> Option<(u32, u32)> {
452        let v = self.value_bytes();
453        let t = self.type_byte() & 0x3F; // mask off high two bits
454        match t {
455            // 2-octet AS specific: AS(2) + value(4)
456            0x00 => {
457                let global = u32::from(u16::from_be_bytes([v[0], v[1]]));
458                let local = u32::from_be_bytes([v[2], v[3], v[4], v[5]]);
459                Some((global, local))
460            }
461            // IPv4 Address specific (0x01) or 4-octet AS specific (0x02): 4 + 2
462            0x01 | 0x02 => {
463                let global = u32::from_be_bytes([v[0], v[1], v[2], v[3]]);
464                let local = u32::from(u16::from_be_bytes([v[4], v[5]]));
465                Some((global, local))
466            }
467            _ => None,
468        }
469    }
470}
471
472impl fmt::Display for ExtendedCommunity {
473    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474        let is_ipv4 = self.type_byte() & 0x3F == 0x01;
475        if let Some((g, l)) = self.route_target() {
476            if is_ipv4 {
477                write!(f, "RT:{}:{l}", Ipv4Addr::from(g))
478            } else {
479                write!(f, "RT:{g}:{l}")
480            }
481        } else if let Some((g, l)) = self.route_origin() {
482            if is_ipv4 {
483                write!(f, "RO:{}:{l}", Ipv4Addr::from(g))
484            } else {
485                write!(f, "RO:{g}:{l}")
486            }
487        } else {
488            write!(f, "0x{:016x}", self.0)
489        }
490    }
491}
492
493/// RFC 8092 Large Community — 12-byte value: `(global_admin, local_data1, local_data2)`.
494///
495/// Each field is a 32-bit unsigned integer. Display format: `"65001:100:200"`.
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
497pub struct LargeCommunity {
498    /// Global administrator (typically ASN).
499    pub global_admin: u32,
500    /// First local data part.
501    pub local_data1: u32,
502    /// Second local data part.
503    pub local_data2: u32,
504}
505
506impl LargeCommunity {
507    /// Create a new large community value.
508    #[must_use]
509    pub fn new(global_admin: u32, local_data1: u32, local_data2: u32) -> Self {
510        Self {
511            global_admin,
512            local_data1,
513            local_data2,
514        }
515    }
516}
517
518impl fmt::Display for LargeCommunity {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        write!(
521            f,
522            "{}:{}:{}",
523            self.global_admin, self.local_data1, self.local_data2
524        )
525    }
526}
527
528/// A known path attribute or raw preserved bytes.
529///
530/// Known attributes are decoded into typed variants. Unknown attributes
531/// are preserved as `RawAttribute` for pass-through with the Partial bit.
532#[derive(Debug, Clone, PartialEq, Eq, Hash)]
533pub enum PathAttribute {
534    /// `ORIGIN` attribute (type 1).
535    Origin(Origin),
536    /// `AS_PATH` attribute (type 2).
537    AsPath(AsPath),
538    /// `NEXT_HOP` attribute (type 3).
539    NextHop(Ipv4Addr),
540    /// `LOCAL_PREF` attribute (type 5).
541    LocalPref(u32),
542    /// `MULTI_EXIT_DISC` attribute (type 4).
543    Med(u32),
544    /// RFC 1997 COMMUNITIES — each u32 is high16=ASN, low16=value.
545    Communities(Vec<u32>),
546    /// RFC 4360 EXTENDED COMMUNITIES.
547    ExtendedCommunities(Vec<ExtendedCommunity>),
548    /// RFC 8092 LARGE COMMUNITIES.
549    LargeCommunities(Vec<LargeCommunity>),
550    /// RFC 4456 `ORIGINATOR_ID` — original router-id of the route.
551    OriginatorId(Ipv4Addr),
552    /// RFC 4456 `CLUSTER_LIST` — list of cluster-ids traversed.
553    ClusterList(Vec<Ipv4Addr>),
554    /// RFC 4760 `MP_REACH_NLRI`.
555    MpReachNlri(MpReachNlri),
556    /// RFC 4760 `MP_UNREACH_NLRI`.
557    MpUnreachNlri(MpUnreachNlri),
558    /// Unknown or unrecognized attribute, preserved for re-advertisement.
559    Unknown(RawAttribute),
560}
561
562impl PathAttribute {
563    /// Return the type code of this attribute.
564    #[must_use]
565    pub fn type_code(&self) -> u8 {
566        match self {
567            Self::Origin(_) => attr_type::ORIGIN,
568            Self::AsPath(_) => attr_type::AS_PATH,
569            Self::NextHop(_) => attr_type::NEXT_HOP,
570            Self::LocalPref(_) => attr_type::LOCAL_PREF,
571            Self::Med(_) => attr_type::MULTI_EXIT_DISC,
572            Self::Communities(_) => attr_type::COMMUNITIES,
573            Self::OriginatorId(_) => attr_type::ORIGINATOR_ID,
574            Self::ClusterList(_) => attr_type::CLUSTER_LIST,
575            Self::ExtendedCommunities(_) => attr_type::EXTENDED_COMMUNITIES,
576            Self::LargeCommunities(_) => attr_type::LARGE_COMMUNITIES,
577            Self::MpReachNlri(_) => attr_type::MP_REACH_NLRI,
578            Self::MpUnreachNlri(_) => attr_type::MP_UNREACH_NLRI,
579            Self::Unknown(raw) => raw.type_code,
580        }
581    }
582
583    /// Return the wire flags for this attribute.
584    #[must_use]
585    pub fn flags(&self) -> u8 {
586        match self {
587            Self::Origin(_) | Self::AsPath(_) | Self::NextHop(_) | Self::LocalPref(_) => {
588                attr_flags::TRANSITIVE
589            }
590            Self::Med(_)
591            | Self::OriginatorId(_)
592            | Self::ClusterList(_)
593            | Self::MpReachNlri(_)
594            | Self::MpUnreachNlri(_) => attr_flags::OPTIONAL,
595            Self::Communities(_) | Self::ExtendedCommunities(_) | Self::LargeCommunities(_) => {
596                attr_flags::OPTIONAL | attr_flags::TRANSITIVE
597            }
598            Self::Unknown(raw) => raw.flags,
599        }
600    }
601}
602
603/// Raw attribute preserved for pass-through (RFC 4271 §5).
604///
605/// On re-advertisement, the Partial bit (0x20) is OR'd into `flags`.
606/// All other flags and bytes are preserved unchanged.
607#[derive(Debug, Clone, PartialEq, Eq, Hash)]
608pub struct RawAttribute {
609    /// Attribute flags byte (optional, transitive, partial, extended-length).
610    pub flags: u8,
611    /// Attribute type code.
612    pub type_code: u8,
613    /// Raw attribute value bytes.
614    pub data: Bytes,
615}
616
617/// Decode path attributes from wire bytes (RFC 4271 §4.3).
618///
619/// Each attribute is: flags(1) + type(1) + length(1 or 2) + value.
620/// The Extended Length flag determines 1-byte vs 2-byte length.
621///
622/// `four_octet_as` controls whether AS numbers in `AS_PATH` are 2 or 4 bytes.
623///
624/// # Errors
625///
626/// Returns `DecodeError` on truncated data or malformed attribute values.
627pub fn decode_path_attributes(
628    mut buf: &[u8],
629    four_octet_as: bool,
630    add_path_families: &[(Afi, Safi)],
631) -> Result<Vec<PathAttribute>, DecodeError> {
632    let mut attrs = Vec::new();
633
634    while !buf.is_empty() {
635        // Need at least flags(1) + type(1) = 2
636        if buf.len() < 2 {
637            return Err(DecodeError::MalformedField {
638                message_type: "UPDATE",
639                detail: "truncated attribute header".to_string(),
640            });
641        }
642
643        let flags = buf[0];
644        let type_code = buf[1];
645        buf = &buf[2..];
646
647        let extended = (flags & attr_flags::EXTENDED_LENGTH) != 0;
648        let value_len = if extended {
649            if buf.len() < 2 {
650                return Err(DecodeError::MalformedField {
651                    message_type: "UPDATE",
652                    detail: "truncated extended-length attribute".to_string(),
653                });
654            }
655            let len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
656            buf = &buf[2..];
657            len
658        } else {
659            if buf.is_empty() {
660                return Err(DecodeError::MalformedField {
661                    message_type: "UPDATE",
662                    detail: "truncated attribute length".to_string(),
663                });
664            }
665            let len = buf[0] as usize;
666            buf = &buf[1..];
667            len
668        };
669
670        if buf.len() < value_len {
671            return Err(DecodeError::MalformedField {
672                message_type: "UPDATE",
673                detail: format!(
674                    "attribute type {type_code} value truncated: need {value_len}, have {}",
675                    buf.len()
676                ),
677            });
678        }
679
680        let value = &buf[..value_len];
681        buf = &buf[value_len..];
682
683        let attr =
684            decode_attribute_value(flags, type_code, value, four_octet_as, add_path_families)?;
685        attrs.push(attr);
686    }
687
688    Ok(attrs)
689}
690
691/// Decode a single attribute value given its flags, type code, and raw bytes.
692#[expect(clippy::too_many_lines)]
693fn decode_attribute_value(
694    flags: u8,
695    type_code: u8,
696    value: &[u8],
697    four_octet_as: bool,
698    add_path_families: &[(Afi, Safi)],
699) -> Result<PathAttribute, DecodeError> {
700    // Validate Optional + Transitive flags for known attribute types (RFC 4271 §6.3).
701    let flags_mask = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
702    if let Some(expected) = expected_flags(type_code)
703        && (flags & flags_mask) != expected
704    {
705        return Err(DecodeError::UpdateAttributeError {
706            subcode: update_subcode::ATTRIBUTE_FLAGS_ERROR,
707            data: attr_error_data(flags, type_code, value),
708            detail: format!(
709                "type {} flags {:#04x} (expected {:#04x})",
710                type_code,
711                flags & flags_mask,
712                expected
713            ),
714        });
715    }
716
717    match type_code {
718        attr_type::ORIGIN => {
719            if value.len() != 1 {
720                return Err(DecodeError::UpdateAttributeError {
721                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
722                    data: attr_error_data(flags, type_code, value),
723                    detail: format!("ORIGIN length {} (expected 1)", value.len()),
724                });
725            }
726            match Origin::from_u8(value[0]) {
727                Some(origin) => Ok(PathAttribute::Origin(origin)),
728                None => Err(DecodeError::UpdateAttributeError {
729                    subcode: update_subcode::INVALID_ORIGIN,
730                    data: attr_error_data(flags, type_code, value),
731                    detail: format!("invalid ORIGIN value {}", value[0]),
732                }),
733            }
734        }
735
736        attr_type::AS_PATH => {
737            let segments = decode_as_path(value, four_octet_as).map_err(|e| {
738                DecodeError::UpdateAttributeError {
739                    subcode: update_subcode::MALFORMED_AS_PATH,
740                    data: attr_error_data(flags, type_code, value),
741                    detail: e.to_string(),
742                }
743            })?;
744            Ok(PathAttribute::AsPath(AsPath { segments }))
745        }
746
747        attr_type::NEXT_HOP => {
748            if value.len() != 4 {
749                return Err(DecodeError::UpdateAttributeError {
750                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
751                    data: attr_error_data(flags, type_code, value),
752                    detail: format!("NEXT_HOP length {} (expected 4)", value.len()),
753                });
754            }
755            let addr = Ipv4Addr::new(value[0], value[1], value[2], value[3]);
756            Ok(PathAttribute::NextHop(addr))
757        }
758
759        attr_type::MULTI_EXIT_DISC => {
760            if value.len() != 4 {
761                return Err(DecodeError::UpdateAttributeError {
762                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
763                    data: attr_error_data(flags, type_code, value),
764                    detail: format!("MED length {} (expected 4)", value.len()),
765                });
766            }
767            let med = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
768            Ok(PathAttribute::Med(med))
769        }
770
771        attr_type::LOCAL_PREF => {
772            if value.len() != 4 {
773                return Err(DecodeError::UpdateAttributeError {
774                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
775                    data: attr_error_data(flags, type_code, value),
776                    detail: format!("LOCAL_PREF length {} (expected 4)", value.len()),
777                });
778            }
779            let lp = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
780            Ok(PathAttribute::LocalPref(lp))
781        }
782
783        attr_type::COMMUNITIES => {
784            if !value.len().is_multiple_of(4) {
785                return Err(DecodeError::UpdateAttributeError {
786                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
787                    data: attr_error_data(flags, type_code, value),
788                    detail: format!("COMMUNITIES length {} not a multiple of 4", value.len()),
789                });
790            }
791            let communities = value
792                .chunks_exact(4)
793                .map(|c| u32::from_be_bytes([c[0], c[1], c[2], c[3]]))
794                .collect();
795            Ok(PathAttribute::Communities(communities))
796        }
797
798        attr_type::EXTENDED_COMMUNITIES => {
799            if !value.len().is_multiple_of(8) {
800                return Err(DecodeError::UpdateAttributeError {
801                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
802                    data: attr_error_data(flags, type_code, value),
803                    detail: format!(
804                        "EXTENDED_COMMUNITIES length {} not a multiple of 8",
805                        value.len()
806                    ),
807                });
808            }
809            let communities = value
810                .chunks_exact(8)
811                .map(|c| {
812                    ExtendedCommunity::new(u64::from_be_bytes([
813                        c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7],
814                    ]))
815                })
816                .collect();
817            Ok(PathAttribute::ExtendedCommunities(communities))
818        }
819
820        attr_type::ORIGINATOR_ID => {
821            if value.len() != 4 {
822                return Err(DecodeError::UpdateAttributeError {
823                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
824                    data: attr_error_data(flags, type_code, value),
825                    detail: format!("ORIGINATOR_ID length {} (expected 4)", value.len()),
826                });
827            }
828            let addr = Ipv4Addr::new(value[0], value[1], value[2], value[3]);
829            Ok(PathAttribute::OriginatorId(addr))
830        }
831
832        attr_type::CLUSTER_LIST => {
833            if !value.len().is_multiple_of(4) {
834                return Err(DecodeError::UpdateAttributeError {
835                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
836                    data: attr_error_data(flags, type_code, value),
837                    detail: format!("CLUSTER_LIST length {} not a multiple of 4", value.len()),
838                });
839            }
840            let ids = value
841                .chunks_exact(4)
842                .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3]))
843                .collect();
844            Ok(PathAttribute::ClusterList(ids))
845        }
846
847        attr_type::LARGE_COMMUNITIES => {
848            if value.is_empty() || !value.len().is_multiple_of(12) {
849                return Err(DecodeError::UpdateAttributeError {
850                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
851                    data: attr_error_data(flags, type_code, value),
852                    detail: format!(
853                        "LARGE_COMMUNITIES length {} invalid (must be non-zero multiple of 12)",
854                        value.len()
855                    ),
856                });
857            }
858            let communities = value
859                .chunks_exact(12)
860                .map(|c| {
861                    LargeCommunity::new(
862                        u32::from_be_bytes([c[0], c[1], c[2], c[3]]),
863                        u32::from_be_bytes([c[4], c[5], c[6], c[7]]),
864                        u32::from_be_bytes([c[8], c[9], c[10], c[11]]),
865                    )
866                })
867                .collect();
868            Ok(PathAttribute::LargeCommunities(communities))
869        }
870
871        attr_type::MP_REACH_NLRI => decode_mp_reach_nlri(value, add_path_families),
872        attr_type::MP_UNREACH_NLRI => decode_mp_unreach_nlri(value, add_path_families),
873
874        // ATOMIC_AGGREGATE, AGGREGATOR, and any unknown type → RawAttribute
875        _ => Ok(PathAttribute::Unknown(RawAttribute {
876            flags,
877            type_code,
878            data: Bytes::copy_from_slice(value),
879        })),
880    }
881}
882
883/// Decode `MP_REACH_NLRI` (type 14) attribute value.
884///
885/// Wire layout (RFC 4760 §3):
886///   AFI (2) | SAFI (1) | NH-Len (1) | Next Hop (variable) | Reserved (1) | NLRI (variable)
887#[expect(clippy::too_many_lines)]
888fn decode_mp_reach_nlri(
889    value: &[u8],
890    add_path_families: &[(Afi, Safi)],
891) -> Result<PathAttribute, DecodeError> {
892    if value.len() < 5 {
893        return Err(DecodeError::MalformedField {
894            message_type: "UPDATE",
895            detail: format!("MP_REACH_NLRI too short: {} bytes", value.len()),
896        });
897    }
898
899    let afi_raw = u16::from_be_bytes([value[0], value[1]]);
900    let safi_raw = value[2];
901    let nh_len = value[3] as usize;
902
903    let afi = Afi::from_u16(afi_raw).ok_or_else(|| DecodeError::MalformedField {
904        message_type: "UPDATE",
905        detail: format!("MP_REACH_NLRI unsupported AFI {afi_raw}"),
906    })?;
907    let safi = Safi::from_u8(safi_raw).ok_or_else(|| DecodeError::MalformedField {
908        message_type: "UPDATE",
909        detail: format!("MP_REACH_NLRI unsupported SAFI {safi_raw}"),
910    })?;
911
912    // 4 bytes for AFI+SAFI+NH-Len, then nh_len bytes, then 1 reserved byte
913    if value.len() < 4 + nh_len + 1 {
914        return Err(DecodeError::MalformedField {
915            message_type: "UPDATE",
916            detail: format!(
917                "MP_REACH_NLRI truncated: NH-Len={nh_len}, have {} bytes total",
918                value.len()
919            ),
920        });
921    }
922
923    let nh_bytes = &value[4..4 + nh_len];
924    // FlowSpec (SAFI 133): NH length is 0 — no next-hop for filter rules
925    let next_hop = if safi == Safi::FlowSpec {
926        if nh_len != 0 {
927            return Err(DecodeError::MalformedField {
928                message_type: "UPDATE",
929                detail: format!("MP_REACH_NLRI FlowSpec next-hop length {nh_len} (expected 0)"),
930            });
931        }
932        IpAddr::V4(Ipv4Addr::UNSPECIFIED)
933    } else {
934        match afi {
935            Afi::Ipv4 => match nh_len {
936                4 => IpAddr::V4(Ipv4Addr::new(
937                    nh_bytes[0],
938                    nh_bytes[1],
939                    nh_bytes[2],
940                    nh_bytes[3],
941                )),
942                16 | 32 => {
943                    let mut octets = [0u8; 16];
944                    octets.copy_from_slice(&nh_bytes[..16]);
945                    IpAddr::V6(Ipv6Addr::from(octets))
946                }
947                _ => {
948                    return Err(DecodeError::MalformedField {
949                        message_type: "UPDATE",
950                        detail: format!(
951                            "MP_REACH_NLRI IPv4 next-hop length {nh_len} (expected 4, 16, or 32)"
952                        ),
953                    });
954                }
955            },
956            Afi::Ipv6 => {
957                if nh_len != 16 && nh_len != 32 {
958                    return Err(DecodeError::MalformedField {
959                        message_type: "UPDATE",
960                        detail: format!(
961                            "MP_REACH_NLRI IPv6 next-hop length {nh_len} (expected 16 or 32)"
962                        ),
963                    });
964                }
965                // Take first 16 bytes (global address); ignore link-local if 32
966                let mut octets = [0u8; 16];
967                octets.copy_from_slice(&nh_bytes[..16]);
968                IpAddr::V6(Ipv6Addr::from(octets))
969            }
970            Afi::L2Vpn => match nh_len {
971                4 => IpAddr::V4(Ipv4Addr::new(
972                    nh_bytes[0],
973                    nh_bytes[1],
974                    nh_bytes[2],
975                    nh_bytes[3],
976                )),
977                16 => {
978                    let mut octets = [0u8; 16];
979                    octets.copy_from_slice(&nh_bytes[..16]);
980                    IpAddr::V6(Ipv6Addr::from(octets))
981                }
982                _ => {
983                    return Err(DecodeError::MalformedField {
984                        message_type: "UPDATE",
985                        detail: format!(
986                            "MP_REACH_NLRI L2VPN next-hop length {nh_len} (expected 4 or 16)"
987                        ),
988                    });
989                }
990            },
991        }
992    };
993
994    // Skip reserved byte
995    let nlri_start = 4 + nh_len + 1;
996    let nlri_bytes = &value[nlri_start..];
997
998    // FlowSpec (SAFI 133): NLRI is FlowSpec rules, not prefixes
999    if safi == Safi::FlowSpec {
1000        let flowspec_rules = crate::flowspec::decode_flowspec_nlri(nlri_bytes, afi)?;
1001        return Ok(PathAttribute::MpReachNlri(MpReachNlri {
1002            afi,
1003            safi,
1004            next_hop,
1005            announced: vec![],
1006            flowspec_announced: flowspec_rules,
1007            evpn_announced: vec![],
1008        }));
1009    }
1010
1011    // EVPN (AFI 25 / SAFI 70): NLRI is typed EVPN routes, not prefixes
1012    if afi == Afi::L2Vpn && safi == Safi::Evpn {
1013        let routes = crate::evpn::decode_evpn_nlri(nlri_bytes)?;
1014        return Ok(PathAttribute::MpReachNlri(MpReachNlri {
1015            afi,
1016            safi,
1017            next_hop,
1018            announced: vec![],
1019            flowspec_announced: vec![],
1020            evpn_announced: routes,
1021        }));
1022    }
1023
1024    // SAFI 70 (EVPN) is only defined for AFI 25 (L2VPN). Reject any other
1025    // AFI explicitly so the unicast NLRI fallthrough below cannot
1026    // misinterpret the typed EVPN payload as a prefix list.
1027    if safi == Safi::Evpn {
1028        return Err(DecodeError::MalformedField {
1029            message_type: "UPDATE",
1030            detail: format!(
1031                "MP_REACH_NLRI SAFI EVPN with non-L2VPN AFI {} (only AFI L2VPN supported)",
1032                afi as u16
1033            ),
1034        });
1035    }
1036
1037    let add_path = add_path_families.contains(&(afi, safi));
1038    let announced = match (afi, add_path) {
1039        (Afi::Ipv4, false) => crate::nlri::decode_nlri(nlri_bytes)?
1040            .into_iter()
1041            .map(|p| NlriEntry {
1042                path_id: 0,
1043                prefix: Prefix::V4(p),
1044            })
1045            .collect(),
1046        (Afi::Ipv4, true) => crate::nlri::decode_nlri_addpath(nlri_bytes)?
1047            .into_iter()
1048            .map(|e| NlriEntry {
1049                path_id: e.path_id,
1050                prefix: Prefix::V4(e.prefix),
1051            })
1052            .collect(),
1053        (Afi::Ipv6, false) => crate::nlri::decode_ipv6_nlri(nlri_bytes)?
1054            .into_iter()
1055            .map(|p| NlriEntry {
1056                path_id: 0,
1057                prefix: Prefix::V6(p),
1058            })
1059            .collect(),
1060        (Afi::Ipv6, true) => crate::nlri::decode_ipv6_nlri_addpath(nlri_bytes)?,
1061        (Afi::L2Vpn, _) => {
1062            return Err(DecodeError::MalformedField {
1063                message_type: "UPDATE",
1064                detail: format!(
1065                    "MP_REACH_NLRI L2VPN with unsupported SAFI {} (only EVPN supported)",
1066                    safi as u8
1067                ),
1068            });
1069        }
1070    };
1071
1072    Ok(PathAttribute::MpReachNlri(MpReachNlri {
1073        afi,
1074        safi,
1075        next_hop,
1076        announced,
1077        flowspec_announced: vec![],
1078        evpn_announced: vec![],
1079    }))
1080}
1081
1082/// Decode `MP_UNREACH_NLRI` (type 15) attribute value.
1083///
1084/// Wire layout (RFC 4760 §4):
1085///   AFI (2) | SAFI (1) | Withdrawn Routes (variable)
1086fn decode_mp_unreach_nlri(
1087    value: &[u8],
1088    add_path_families: &[(Afi, Safi)],
1089) -> Result<PathAttribute, DecodeError> {
1090    if value.len() < 3 {
1091        return Err(DecodeError::MalformedField {
1092            message_type: "UPDATE",
1093            detail: format!("MP_UNREACH_NLRI too short: {} bytes", value.len()),
1094        });
1095    }
1096
1097    let afi_raw = u16::from_be_bytes([value[0], value[1]]);
1098    let safi_raw = value[2];
1099
1100    let afi = Afi::from_u16(afi_raw).ok_or_else(|| DecodeError::MalformedField {
1101        message_type: "UPDATE",
1102        detail: format!("MP_UNREACH_NLRI unsupported AFI {afi_raw}"),
1103    })?;
1104    let safi = Safi::from_u8(safi_raw).ok_or_else(|| DecodeError::MalformedField {
1105        message_type: "UPDATE",
1106        detail: format!("MP_UNREACH_NLRI unsupported SAFI {safi_raw}"),
1107    })?;
1108
1109    let withdrawn_bytes = &value[3..];
1110
1111    // FlowSpec (SAFI 133): withdrawn is FlowSpec rules
1112    if safi == Safi::FlowSpec {
1113        let flowspec_rules = crate::flowspec::decode_flowspec_nlri(withdrawn_bytes, afi)?;
1114        return Ok(PathAttribute::MpUnreachNlri(MpUnreachNlri {
1115            afi,
1116            safi,
1117            withdrawn: vec![],
1118            flowspec_withdrawn: flowspec_rules,
1119            evpn_withdrawn: vec![],
1120        }));
1121    }
1122
1123    // EVPN (AFI 25 / SAFI 70): withdrawn is typed EVPN routes, not prefixes
1124    if afi == Afi::L2Vpn && safi == Safi::Evpn {
1125        let routes = crate::evpn::decode_evpn_nlri(withdrawn_bytes)?;
1126        return Ok(PathAttribute::MpUnreachNlri(MpUnreachNlri {
1127            afi,
1128            safi,
1129            withdrawn: vec![],
1130            flowspec_withdrawn: vec![],
1131            evpn_withdrawn: routes,
1132        }));
1133    }
1134
1135    // SAFI 70 (EVPN) is only defined for AFI 25 (L2VPN). Reject any other
1136    // AFI explicitly so the unicast NLRI fallthrough below cannot
1137    // misinterpret the typed EVPN payload as a prefix list.
1138    if safi == Safi::Evpn {
1139        return Err(DecodeError::MalformedField {
1140            message_type: "UPDATE",
1141            detail: format!(
1142                "MP_UNREACH_NLRI SAFI EVPN with non-L2VPN AFI {} (only AFI L2VPN supported)",
1143                afi as u16
1144            ),
1145        });
1146    }
1147
1148    let add_path = add_path_families.contains(&(afi, safi));
1149    let withdrawn = match (afi, add_path) {
1150        (Afi::Ipv4, false) => crate::nlri::decode_nlri(withdrawn_bytes)?
1151            .into_iter()
1152            .map(|p| NlriEntry {
1153                path_id: 0,
1154                prefix: Prefix::V4(p),
1155            })
1156            .collect(),
1157        (Afi::Ipv4, true) => crate::nlri::decode_nlri_addpath(withdrawn_bytes)?
1158            .into_iter()
1159            .map(|e| NlriEntry {
1160                path_id: e.path_id,
1161                prefix: Prefix::V4(e.prefix),
1162            })
1163            .collect(),
1164        (Afi::Ipv6, false) => crate::nlri::decode_ipv6_nlri(withdrawn_bytes)?
1165            .into_iter()
1166            .map(|p| NlriEntry {
1167                path_id: 0,
1168                prefix: Prefix::V6(p),
1169            })
1170            .collect(),
1171        (Afi::Ipv6, true) => crate::nlri::decode_ipv6_nlri_addpath(withdrawn_bytes)?,
1172        (Afi::L2Vpn, _) => {
1173            return Err(DecodeError::MalformedField {
1174                message_type: "UPDATE",
1175                detail: format!(
1176                    "MP_UNREACH_NLRI L2VPN with unsupported SAFI {} (only EVPN supported)",
1177                    safi as u8
1178                ),
1179            });
1180        }
1181    };
1182
1183    Ok(PathAttribute::MpUnreachNlri(MpUnreachNlri {
1184        afi,
1185        safi,
1186        withdrawn,
1187        flowspec_withdrawn: vec![],
1188        evpn_withdrawn: vec![],
1189    }))
1190}
1191
1192/// Decode `AS_PATH` segments from the attribute value bytes.
1193fn decode_as_path(mut buf: &[u8], four_octet_as: bool) -> Result<Vec<AsPathSegment>, DecodeError> {
1194    let as_size: usize = if four_octet_as { 4 } else { 2 };
1195    let mut segments = Vec::new();
1196
1197    while !buf.is_empty() {
1198        if buf.len() < 2 {
1199            return Err(DecodeError::MalformedField {
1200                message_type: "UPDATE",
1201                detail: "truncated AS_PATH segment header".to_string(),
1202            });
1203        }
1204
1205        let seg_type = buf[0];
1206        let seg_count = buf[1] as usize;
1207        buf = &buf[2..];
1208
1209        let needed = seg_count * as_size;
1210        if buf.len() < needed {
1211            return Err(DecodeError::MalformedField {
1212                message_type: "UPDATE",
1213                detail: format!(
1214                    "AS_PATH segment truncated: need {needed} bytes for {seg_count} ASNs, have {}",
1215                    buf.len()
1216                ),
1217            });
1218        }
1219
1220        let mut asns = Vec::with_capacity(seg_count);
1221        for _ in 0..seg_count {
1222            let asn = if four_octet_as {
1223                let v = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
1224                buf = &buf[4..];
1225                v
1226            } else {
1227                let v = u32::from(u16::from_be_bytes([buf[0], buf[1]]));
1228                buf = &buf[2..];
1229                v
1230            };
1231            asns.push(asn);
1232        }
1233
1234        match seg_type {
1235            as_path_segment::AS_SET => segments.push(AsPathSegment::AsSet(asns)),
1236            as_path_segment::AS_SEQUENCE => segments.push(AsPathSegment::AsSequence(asns)),
1237            _ => {
1238                return Err(DecodeError::MalformedField {
1239                    message_type: "UPDATE",
1240                    detail: format!("unknown AS_PATH segment type {seg_type}"),
1241                });
1242            }
1243        }
1244    }
1245
1246    Ok(segments)
1247}
1248
1249/// Build the attribute-triplet (flags + type + length + value) used as
1250/// NOTIFICATION data in UPDATE error subcodes per RFC 4271 §6.3.
1251pub(crate) fn attr_error_data(flags: u8, type_code: u8, value: &[u8]) -> Vec<u8> {
1252    let mut buf = Vec::with_capacity(3 + value.len());
1253    if value.len() > 255 {
1254        buf.push(flags | attr_flags::EXTENDED_LENGTH);
1255        buf.push(type_code);
1256        #[expect(clippy::cast_possible_truncation)]
1257        let len = value.len() as u16;
1258        buf.extend_from_slice(&len.to_be_bytes());
1259    } else {
1260        buf.push(flags);
1261        buf.push(type_code);
1262        #[expect(clippy::cast_possible_truncation)]
1263        buf.push(value.len() as u8);
1264    }
1265    buf.extend_from_slice(value);
1266    buf
1267}
1268
1269/// Return the expected Optional + Transitive flags for known attribute types.
1270/// Returns `None` for unrecognized types (no validation performed).
1271fn expected_flags(type_code: u8) -> Option<u8> {
1272    match type_code {
1273        // Well-known mandatory/discretionary: Optional=0, Transitive=1
1274        attr_type::ORIGIN
1275        | attr_type::AS_PATH
1276        | attr_type::NEXT_HOP
1277        | attr_type::LOCAL_PREF
1278        | attr_type::ATOMIC_AGGREGATE => Some(attr_flags::TRANSITIVE),
1279        // Optional non-transitive (RFC 4760 §3/§4: MP_REACH/UNREACH are non-transitive;
1280        // RFC 4456: ORIGINATOR_ID and CLUSTER_LIST are optional non-transitive)
1281        attr_type::MULTI_EXIT_DISC
1282        | attr_type::ORIGINATOR_ID
1283        | attr_type::CLUSTER_LIST
1284        | attr_type::MP_REACH_NLRI
1285        | attr_type::MP_UNREACH_NLRI => Some(attr_flags::OPTIONAL),
1286        // Optional transitive
1287        attr_type::AGGREGATOR
1288        | attr_type::COMMUNITIES
1289        | attr_type::EXTENDED_COMMUNITIES
1290        | attr_type::LARGE_COMMUNITIES => Some(attr_flags::OPTIONAL | attr_flags::TRANSITIVE),
1291        _ => None,
1292    }
1293}
1294
1295/// Encode path attributes to wire bytes.
1296///
1297/// `four_octet_as` controls whether AS numbers in `AS_PATH` are 2 or 4 bytes.
1298/// Encode a list of path attributes into wire format.
1299///
1300/// When `add_path_mp` is true, `MP_REACH_NLRI` and `MP_UNREACH_NLRI` NLRI
1301/// entries include 4-byte path IDs per RFC 7911.
1302pub fn encode_path_attributes(
1303    attrs: &[PathAttribute],
1304    buf: &mut Vec<u8>,
1305    four_octet_as: bool,
1306    add_path_mp: bool,
1307) {
1308    for attr in attrs {
1309        let mut value = Vec::new();
1310        let flags;
1311        let type_code;
1312
1313        match attr {
1314            PathAttribute::Origin(origin) => {
1315                flags = attr_flags::TRANSITIVE;
1316                type_code = attr_type::ORIGIN;
1317                value.push(*origin as u8);
1318            }
1319            PathAttribute::AsPath(as_path) => {
1320                flags = attr_flags::TRANSITIVE;
1321                type_code = attr_type::AS_PATH;
1322                encode_as_path(as_path, &mut value, four_octet_as);
1323            }
1324            PathAttribute::NextHop(addr) => {
1325                flags = attr_flags::TRANSITIVE;
1326                type_code = attr_type::NEXT_HOP;
1327                value.extend_from_slice(&addr.octets());
1328            }
1329            PathAttribute::Med(med) => {
1330                flags = attr_flags::OPTIONAL;
1331                type_code = attr_type::MULTI_EXIT_DISC;
1332                value.extend_from_slice(&med.to_be_bytes());
1333            }
1334            PathAttribute::LocalPref(lp) => {
1335                flags = attr_flags::TRANSITIVE;
1336                type_code = attr_type::LOCAL_PREF;
1337                value.extend_from_slice(&lp.to_be_bytes());
1338            }
1339            PathAttribute::Communities(communities) => {
1340                flags = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
1341                type_code = attr_type::COMMUNITIES;
1342                for &c in communities {
1343                    value.extend_from_slice(&c.to_be_bytes());
1344                }
1345            }
1346            PathAttribute::ExtendedCommunities(communities) => {
1347                flags = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
1348                type_code = attr_type::EXTENDED_COMMUNITIES;
1349                for &c in communities {
1350                    value.extend_from_slice(&c.as_u64().to_be_bytes());
1351                }
1352            }
1353            PathAttribute::LargeCommunities(communities) => {
1354                flags = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
1355                type_code = attr_type::LARGE_COMMUNITIES;
1356                for &c in communities {
1357                    value.extend_from_slice(&c.global_admin.to_be_bytes());
1358                    value.extend_from_slice(&c.local_data1.to_be_bytes());
1359                    value.extend_from_slice(&c.local_data2.to_be_bytes());
1360                }
1361            }
1362            PathAttribute::OriginatorId(addr) => {
1363                flags = attr_flags::OPTIONAL;
1364                type_code = attr_type::ORIGINATOR_ID;
1365                value.extend_from_slice(&addr.octets());
1366            }
1367            PathAttribute::ClusterList(ids) => {
1368                flags = attr_flags::OPTIONAL;
1369                type_code = attr_type::CLUSTER_LIST;
1370                for id in ids {
1371                    value.extend_from_slice(&id.octets());
1372                }
1373            }
1374            PathAttribute::MpReachNlri(mp) => {
1375                flags = attr_flags::OPTIONAL;
1376                type_code = attr_type::MP_REACH_NLRI;
1377                encode_mp_reach_nlri(mp, &mut value, add_path_mp);
1378            }
1379            PathAttribute::MpUnreachNlri(mp) => {
1380                flags = attr_flags::OPTIONAL;
1381                type_code = attr_type::MP_UNREACH_NLRI;
1382                encode_mp_unreach_nlri(mp, &mut value, add_path_mp);
1383            }
1384            PathAttribute::Unknown(raw) => {
1385                // RFC 4271 §5: unrecognized *optional* transitive attributes
1386                // must be propagated with the Partial bit set. Well-known
1387                // transitive attributes (OPTIONAL=0) must NOT get PARTIAL.
1388                let optional_transitive = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
1389                flags = if (raw.flags & optional_transitive) == optional_transitive {
1390                    raw.flags | attr_flags::PARTIAL
1391                } else {
1392                    raw.flags
1393                };
1394                type_code = raw.type_code;
1395                value.extend_from_slice(&raw.data);
1396            }
1397        }
1398
1399        // Use extended length if value > 255 bytes
1400        if value.len() > 255 {
1401            buf.push(flags | attr_flags::EXTENDED_LENGTH);
1402            buf.push(type_code);
1403            #[expect(clippy::cast_possible_truncation)]
1404            let len = value.len() as u16;
1405            buf.extend_from_slice(&len.to_be_bytes());
1406        } else {
1407            buf.push(flags);
1408            buf.push(type_code);
1409            #[expect(clippy::cast_possible_truncation)]
1410            buf.push(value.len() as u8);
1411        }
1412        buf.extend_from_slice(&value);
1413    }
1414}
1415
1416/// Encode `MP_REACH_NLRI` value bytes.
1417///
1418/// When `add_path` is true, each NLRI entry includes a 4-byte path ID
1419/// prefix per RFC 7911.
1420fn encode_mp_reach_nlri(mp: &MpReachNlri, buf: &mut Vec<u8>, add_path: bool) {
1421    buf.extend_from_slice(&(mp.afi as u16).to_be_bytes());
1422    buf.push(mp.safi as u8);
1423
1424    // FlowSpec: NH length = 0, reserved = 0, then FlowSpec NLRI
1425    if mp.safi == Safi::FlowSpec {
1426        buf.push(0); // NH-Len = 0
1427        buf.push(0); // Reserved
1428        crate::flowspec::encode_flowspec_nlri(&mp.flowspec_announced, buf, mp.afi);
1429        return;
1430    }
1431
1432    // EVPN: next-hop is the VTEP loopback IP (4 or 16 bytes), then EVPN NLRI
1433    if mp.afi == Afi::L2Vpn && mp.safi == Safi::Evpn {
1434        match mp.next_hop {
1435            IpAddr::V4(addr) => {
1436                buf.push(4);
1437                buf.extend_from_slice(&addr.octets());
1438            }
1439            IpAddr::V6(addr) => {
1440                buf.push(16);
1441                buf.extend_from_slice(&addr.octets());
1442            }
1443        }
1444        buf.push(0); // Reserved
1445        crate::evpn::encode_evpn_nlri(&mp.evpn_announced, buf);
1446        return;
1447    }
1448
1449    match mp.next_hop {
1450        IpAddr::V4(addr) => {
1451            buf.push(4); // NH-Len
1452            buf.extend_from_slice(&addr.octets());
1453        }
1454        IpAddr::V6(addr) => {
1455            buf.push(16); // NH-Len
1456            buf.extend_from_slice(&addr.octets());
1457        }
1458    }
1459
1460    buf.push(0); // Reserved
1461
1462    if add_path {
1463        crate::nlri::encode_ipv6_nlri_addpath(&mp.announced, buf);
1464    } else {
1465        for entry in &mp.announced {
1466            match entry.prefix {
1467                Prefix::V4(p) => crate::nlri::encode_nlri(&[p], buf),
1468                Prefix::V6(p) => crate::nlri::encode_ipv6_nlri(&[p], buf),
1469            }
1470        }
1471    }
1472}
1473
1474/// Encode `MP_UNREACH_NLRI` value bytes.
1475///
1476/// When `add_path` is true, each withdrawn entry includes a 4-byte path ID.
1477fn encode_mp_unreach_nlri(mp: &MpUnreachNlri, buf: &mut Vec<u8>, add_path: bool) {
1478    buf.extend_from_slice(&(mp.afi as u16).to_be_bytes());
1479    buf.push(mp.safi as u8);
1480
1481    // FlowSpec: encode FlowSpec NLRI rules
1482    if mp.safi == Safi::FlowSpec {
1483        crate::flowspec::encode_flowspec_nlri(&mp.flowspec_withdrawn, buf, mp.afi);
1484        return;
1485    }
1486
1487    // EVPN: encode EVPN NLRI routes
1488    if mp.afi == Afi::L2Vpn && mp.safi == Safi::Evpn {
1489        crate::evpn::encode_evpn_nlri(&mp.evpn_withdrawn, buf);
1490        return;
1491    }
1492
1493    if add_path {
1494        crate::nlri::encode_ipv6_nlri_addpath(&mp.withdrawn, buf);
1495    } else {
1496        for entry in &mp.withdrawn {
1497            match entry.prefix {
1498                Prefix::V4(p) => crate::nlri::encode_nlri(&[p], buf),
1499                Prefix::V6(p) => crate::nlri::encode_ipv6_nlri(&[p], buf),
1500            }
1501        }
1502    }
1503}
1504
1505/// Encode `AS_PATH` segments into value bytes.
1506fn encode_as_path(as_path: &AsPath, buf: &mut Vec<u8>, four_octet_as: bool) {
1507    for segment in &as_path.segments {
1508        let (seg_type, asns) = match segment {
1509            AsPathSegment::AsSet(asns) => (as_path_segment::AS_SET, asns),
1510            AsPathSegment::AsSequence(asns) => (as_path_segment::AS_SEQUENCE, asns),
1511        };
1512        for chunk in asns.chunks(u8::MAX as usize) {
1513            buf.push(seg_type);
1514            #[expect(clippy::cast_possible_truncation)]
1515            buf.push(chunk.len() as u8);
1516            for &asn in chunk {
1517                if four_octet_as {
1518                    buf.extend_from_slice(&asn.to_be_bytes());
1519                } else {
1520                    // RFC 6793: ASNs > 65535 are mapped to AS_TRANS (23456)
1521                    // in 2-octet AS_PATH encoding.
1522                    let as2 = u16::try_from(asn).unwrap_or(crate::constants::AS_TRANS);
1523                    buf.extend_from_slice(&as2.to_be_bytes());
1524                }
1525            }
1526        }
1527    }
1528}
1529
1530#[cfg(test)]
1531mod tests {
1532    use super::*;
1533
1534    #[test]
1535    fn mp_reach_evpn_attribute_roundtrip() {
1536        use crate::evpn::{EthernetTagId, EvpnImet, EvpnRoute, RouteDistinguisher};
1537
1538        let mp = MpReachNlri {
1539            afi: Afi::L2Vpn,
1540            safi: Safi::Evpn,
1541            next_hop: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 100)),
1542            announced: vec![],
1543            flowspec_announced: vec![],
1544            evpn_announced: vec![EvpnRoute::Imet(EvpnImet {
1545                rd: RouteDistinguisher([0, 0, 0xFD, 0xE8, 0, 0, 0, 0x64]),
1546                ethernet_tag: EthernetTagId(100),
1547                originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 100)),
1548            })],
1549        };
1550        let attr = PathAttribute::MpReachNlri(mp);
1551
1552        let mut buf = Vec::new();
1553        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
1554        let decoded = decode_path_attributes(&buf, true, &[]).expect("decode");
1555        assert_eq!(decoded.len(), 1);
1556        assert_eq!(attr, decoded[0]);
1557
1558        let PathAttribute::MpReachNlri(dec) = &decoded[0] else {
1559            panic!("not MP_REACH after decode");
1560        };
1561        assert_eq!(dec.afi, Afi::L2Vpn);
1562        assert_eq!(dec.safi, Safi::Evpn);
1563        assert_eq!(dec.evpn_announced.len(), 1);
1564        assert!(matches!(dec.evpn_announced[0], EvpnRoute::Imet(_)));
1565    }
1566
1567    #[test]
1568    fn mp_unreach_evpn_attribute_roundtrip() {
1569        use crate::evpn::{EthernetSegmentIdentifier, EvpnEs, EvpnRoute, RouteDistinguisher};
1570
1571        let mp = MpUnreachNlri {
1572            afi: Afi::L2Vpn,
1573            safi: Safi::Evpn,
1574            withdrawn: vec![],
1575            flowspec_withdrawn: vec![],
1576            evpn_withdrawn: vec![EvpnRoute::Es(EvpnEs {
1577                rd: RouteDistinguisher([0, 0, 0xFD, 0xE8, 0, 0, 0, 0x64]),
1578                esi: EthernetSegmentIdentifier([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
1579                originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1580            })],
1581        };
1582        let attr = PathAttribute::MpUnreachNlri(mp);
1583        let mut buf = Vec::new();
1584        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
1585        let decoded = decode_path_attributes(&buf, true, &[]).expect("decode");
1586        assert_eq!(decoded.len(), 1);
1587        assert_eq!(attr, decoded[0]);
1588    }
1589
1590    // ---- EVPN extended community typed accessors (RFC 7432 / 8365 / 9135) ---
1591
1592    #[test]
1593    fn ext_comm_bgp_encapsulation_vxlan() {
1594        let c = ExtendedCommunity::bgp_encapsulation(8); // VXLAN
1595        assert_eq!(c.type_byte(), 0x03);
1596        assert_eq!(c.subtype(), 0x0C);
1597        assert_eq!(c.as_bgp_encapsulation(), Some(8));
1598        // Wire layout: 4 bytes reserved + 2-byte tunnel type
1599        let b = c.as_u64().to_be_bytes();
1600        assert_eq!(b[2..6], [0, 0, 0, 0]);
1601        assert_eq!(&b[6..8], &[0, 8]);
1602        // Negative: other subtypes return None
1603        assert_eq!(ExtendedCommunity::new(0).as_bgp_encapsulation(), None);
1604    }
1605
1606    #[test]
1607    fn ext_comm_mac_mobility_sticky_and_sequence() {
1608        let m1 = ExtendedCommunity::mac_mobility(false, 42);
1609        assert_eq!(m1.as_mac_mobility(), Some((false, 42)));
1610        let m2 = ExtendedCommunity::mac_mobility(true, 12345);
1611        assert_eq!(m2.as_mac_mobility(), Some((true, 12345)));
1612        // Round-trip max sequence
1613        let m3 = ExtendedCommunity::mac_mobility(true, u32::MAX);
1614        assert_eq!(m3.as_mac_mobility(), Some((true, u32::MAX)));
1615        assert_eq!(ExtendedCommunity::new(0).as_mac_mobility(), None);
1616    }
1617
1618    #[test]
1619    fn ext_comm_esi_label_flags_and_label() {
1620        let e1 = ExtendedCommunity::esi_label(false, 10_000);
1621        assert_eq!(e1.as_esi_label(), Some((false, 10_000)));
1622        let e2 = ExtendedCommunity::esi_label(true, 0x00FF_FFFF);
1623        assert_eq!(e2.as_esi_label(), Some((true, 0x00FF_FFFF)));
1624    }
1625
1626    #[test]
1627    fn ext_comm_es_import_rt_mac() {
1628        let mac = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55];
1629        let e = ExtendedCommunity::es_import_rt(mac);
1630        assert_eq!(e.as_es_import_rt(), Some(mac));
1631        assert_eq!(e.type_byte(), 0x06);
1632        assert_eq!(e.subtype(), 0x02);
1633    }
1634
1635    #[test]
1636    fn ext_comm_router_mac() {
1637        let mac = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
1638        let e = ExtendedCommunity::router_mac(mac);
1639        assert_eq!(e.as_router_mac(), Some(mac));
1640    }
1641
1642    #[test]
1643    fn ext_comm_default_gateway_flag_only() {
1644        let d = ExtendedCommunity::default_gateway();
1645        assert!(d.as_default_gateway());
1646        // Not a default gateway
1647        assert!(!ExtendedCommunity::bgp_encapsulation(8).as_default_gateway());
1648    }
1649
1650    /// Regression: Default Gateway is a flag-only community (RFC 7432).
1651    /// Malformed advertisements that set non-zero bytes in the value
1652    /// field must NOT be treated as default-gateway matches.
1653    #[test]
1654    fn ext_comm_default_gateway_rejects_nonzero_value() {
1655        // Correct type/subtype (0x03/0x0D) but bogus value.
1656        let malformed =
1657            ExtendedCommunity::new(u64::from_be_bytes([0x03, 0x0D, 0, 0, 0, 0, 0, 0x01]));
1658        assert!(
1659            !malformed.as_default_gateway(),
1660            "default-gateway accessor must require all-zero value bytes"
1661        );
1662        // Sanity: the clean form still passes.
1663        assert!(ExtendedCommunity::default_gateway().as_default_gateway());
1664    }
1665
1666    #[test]
1667    fn ext_comm_accessors_return_none_on_unrelated_communities() {
1668        let rt = ExtendedCommunity::new(u64::from_be_bytes([0x00, 0x02, 0xFD, 0xE8, 0, 0, 0, 100])); // RT:65000:100
1669        assert_eq!(rt.as_bgp_encapsulation(), None);
1670        assert_eq!(rt.as_mac_mobility(), None);
1671        assert_eq!(rt.as_esi_label(), None);
1672        assert_eq!(rt.as_es_import_rt(), None);
1673        assert_eq!(rt.as_router_mac(), None);
1674        assert!(!rt.as_default_gateway());
1675    }
1676
1677    #[test]
1678    fn origin_from_u8_roundtrip() {
1679        assert_eq!(Origin::from_u8(0), Some(Origin::Igp));
1680        assert_eq!(Origin::from_u8(1), Some(Origin::Egp));
1681        assert_eq!(Origin::from_u8(2), Some(Origin::Incomplete));
1682        assert_eq!(Origin::from_u8(3), None);
1683    }
1684
1685    #[test]
1686    fn origin_ordering() {
1687        assert!(Origin::Igp < Origin::Egp);
1688        assert!(Origin::Egp < Origin::Incomplete);
1689    }
1690
1691    #[test]
1692    fn as_path_length_calculation() {
1693        let path = AsPath {
1694            segments: vec![
1695                AsPathSegment::AsSequence(vec![65001, 65002, 65003]),
1696                AsPathSegment::AsSet(vec![65004, 65005]),
1697            ],
1698        };
1699        // Sequence: 3 ASNs, Set: counts as 1 → total 4
1700        assert_eq!(path.len(), 4);
1701    }
1702
1703    #[test]
1704    fn as_path_empty() {
1705        let path = AsPath { segments: vec![] };
1706        assert!(path.is_empty());
1707        assert_eq!(path.len(), 0);
1708    }
1709
1710    #[test]
1711    fn contains_asn_in_sequence() {
1712        let path = AsPath {
1713            segments: vec![AsPathSegment::AsSequence(vec![65001, 65002, 65003])],
1714        };
1715        assert!(path.contains_asn(65002));
1716        assert!(!path.contains_asn(65004));
1717    }
1718
1719    #[test]
1720    fn contains_asn_in_set() {
1721        let path = AsPath {
1722            segments: vec![AsPathSegment::AsSet(vec![65004, 65005])],
1723        };
1724        assert!(path.contains_asn(65005));
1725        assert!(!path.contains_asn(65001));
1726    }
1727
1728    #[test]
1729    fn contains_asn_multiple_segments() {
1730        let path = AsPath {
1731            segments: vec![
1732                AsPathSegment::AsSequence(vec![65001, 65002]),
1733                AsPathSegment::AsSet(vec![65003]),
1734            ],
1735        };
1736        assert!(path.contains_asn(65001));
1737        assert!(path.contains_asn(65003));
1738        assert!(!path.contains_asn(65004));
1739    }
1740
1741    #[test]
1742    fn contains_asn_empty_path() {
1743        let path = AsPath { segments: vec![] };
1744        assert!(!path.contains_asn(65001));
1745    }
1746
1747    #[test]
1748    fn is_private_asn_boundaries() {
1749        // 16-bit private range boundaries
1750        assert!(!is_private_asn(64_511));
1751        assert!(is_private_asn(64_512));
1752        assert!(is_private_asn(65_534));
1753        assert!(!is_private_asn(65_535));
1754
1755        // 32-bit private range boundaries
1756        assert!(!is_private_asn(4_199_999_999));
1757        assert!(is_private_asn(4_200_000_000));
1758        assert!(is_private_asn(4_294_967_294));
1759        assert!(!is_private_asn(4_294_967_295));
1760    }
1761
1762    #[test]
1763    fn all_private_empty_path_is_false() {
1764        let path = AsPath { segments: vec![] };
1765        assert!(!path.all_private());
1766    }
1767
1768    #[test]
1769    fn all_private_mixed_segments() {
1770        let path = AsPath {
1771            segments: vec![
1772                AsPathSegment::AsSet(vec![64_512, 65_000]),
1773                AsPathSegment::AsSequence(vec![4_200_000_000, 65_534]),
1774            ],
1775        };
1776        assert!(path.all_private());
1777
1778        let non_private = AsPath {
1779            segments: vec![
1780                AsPathSegment::AsSet(vec![64_512, 65_000]),
1781                AsPathSegment::AsSequence(vec![65_535]),
1782            ],
1783        };
1784        assert!(!non_private.all_private());
1785    }
1786
1787    #[test]
1788    fn decode_origin_igp() {
1789        // flags=0x40 (transitive), type=1, len=1, value=0 (IGP)
1790        let buf = [0x40, 0x01, 0x01, 0x00];
1791        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1792        assert_eq!(attrs.len(), 1);
1793        assert_eq!(attrs[0], PathAttribute::Origin(Origin::Igp));
1794    }
1795
1796    #[test]
1797    fn decode_origin_egp() {
1798        let buf = [0x40, 0x01, 0x01, 0x01];
1799        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1800        assert_eq!(attrs[0], PathAttribute::Origin(Origin::Egp));
1801    }
1802
1803    #[test]
1804    fn decode_origin_invalid_value() {
1805        // ORIGIN with value 5 — not a valid Origin (only 0-2 are defined)
1806        let buf = [0x40, 0x01, 0x01, 0x05];
1807        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
1808        match &err {
1809            DecodeError::UpdateAttributeError { subcode, .. } => {
1810                assert_eq!(*subcode, update_subcode::INVALID_ORIGIN);
1811            }
1812            other => panic!("expected UpdateAttributeError, got: {other:?}"),
1813        }
1814    }
1815
1816    #[test]
1817    fn decode_next_hop() {
1818        // flags=0x40, type=3, len=4, value=10.0.0.1
1819        let buf = [0x40, 0x03, 0x04, 10, 0, 0, 1];
1820        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1821        assert_eq!(attrs[0], PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)));
1822    }
1823
1824    #[test]
1825    fn decode_med() {
1826        // flags=0x80 (optional), type=4, len=4, value=100
1827        let buf = [0x80, 0x04, 0x04, 0, 0, 0, 100];
1828        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1829        assert_eq!(attrs[0], PathAttribute::Med(100));
1830    }
1831
1832    #[test]
1833    fn decode_local_pref() {
1834        // flags=0x40, type=5, len=4, value=200
1835        let buf = [0x40, 0x05, 0x04, 0, 0, 0, 200];
1836        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1837        assert_eq!(attrs[0], PathAttribute::LocalPref(200));
1838    }
1839
1840    #[test]
1841    fn decode_as_path_4byte() {
1842        // flags=0x40, type=2, len=10
1843        // segment: type=2 (AS_SEQUENCE), count=2, ASNs: 65001, 65002 (4 bytes each)
1844        let buf = [
1845            0x40, 0x02, 0x0A, // header
1846            0x02, 0x02, // AS_SEQUENCE, 2 ASNs
1847            0x00, 0x00, 0xFD, 0xE9, // 65001
1848            0x00, 0x00, 0xFD, 0xEA, // 65002
1849        ];
1850        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1851        assert_eq!(
1852            attrs[0],
1853            PathAttribute::AsPath(AsPath {
1854                segments: vec![AsPathSegment::AsSequence(vec![65001, 65002])]
1855            })
1856        );
1857    }
1858
1859    #[test]
1860    fn decode_as_path_2byte() {
1861        // flags=0x40, type=2, len=6
1862        // segment: type=2 (AS_SEQUENCE), count=2, ASNs: 100, 200 (2 bytes each)
1863        let buf = [
1864            0x40, 0x02, 0x06, // header
1865            0x02, 0x02, // AS_SEQUENCE, 2 ASNs
1866            0x00, 0x64, // 100
1867            0x00, 0xC8, // 200
1868        ];
1869        let attrs = decode_path_attributes(&buf, false, &[]).unwrap();
1870        assert_eq!(
1871            attrs[0],
1872            PathAttribute::AsPath(AsPath {
1873                segments: vec![AsPathSegment::AsSequence(vec![100, 200])]
1874            })
1875        );
1876    }
1877
1878    #[test]
1879    fn decode_unknown_attribute_preserved() {
1880        // flags=0xC0 (optional+transitive), type=99, len=3, data=[1,2,3]
1881        let buf = [0xC0, 99, 0x03, 1, 2, 3];
1882        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1883        assert_eq!(
1884            attrs[0],
1885            PathAttribute::Unknown(RawAttribute {
1886                flags: 0xC0,
1887                type_code: 99,
1888                data: Bytes::from_static(&[1, 2, 3]),
1889            })
1890        );
1891    }
1892
1893    #[test]
1894    fn decode_atomic_aggregate_as_unknown() {
1895        // ATOMIC_AGGREGATE: flags=0x40, type=6, len=0
1896        let buf = [0x40, 0x06, 0x00];
1897        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1898        assert!(matches!(attrs[0], PathAttribute::Unknown(_)));
1899    }
1900
1901    #[test]
1902    fn decode_extended_length() {
1903        // flags=0x50 (transitive+extended), type=2, len=0x000A (10)
1904        // Same AS_PATH as the 4-byte test
1905        let buf = [
1906            0x50, 0x02, 0x00, 0x0A, // header with extended length
1907            0x02, 0x02, // AS_SEQUENCE, 2 ASNs
1908            0x00, 0x00, 0xFD, 0xE9, // 65001
1909            0x00, 0x00, 0xFD, 0xEA, // 65002
1910        ];
1911        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1912        assert_eq!(
1913            attrs[0],
1914            PathAttribute::AsPath(AsPath {
1915                segments: vec![AsPathSegment::AsSequence(vec![65001, 65002])]
1916            })
1917        );
1918    }
1919
1920    #[test]
1921    fn decode_multiple_attributes() {
1922        let mut buf = Vec::new();
1923        // ORIGIN IGP
1924        buf.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]);
1925        // NEXT_HOP 10.0.0.1
1926        buf.extend_from_slice(&[0x40, 0x03, 0x04, 10, 0, 0, 1]);
1927        // AS_PATH empty
1928        buf.extend_from_slice(&[0x40, 0x02, 0x00]);
1929
1930        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
1931        assert_eq!(attrs.len(), 3);
1932        assert_eq!(attrs[0], PathAttribute::Origin(Origin::Igp));
1933        assert_eq!(attrs[1], PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)));
1934        assert_eq!(attrs[2], PathAttribute::AsPath(AsPath { segments: vec![] }));
1935    }
1936
1937    #[test]
1938    fn roundtrip_attributes_4byte() {
1939        let attrs = vec![
1940            PathAttribute::Origin(Origin::Igp),
1941            PathAttribute::AsPath(AsPath {
1942                segments: vec![AsPathSegment::AsSequence(vec![65001, 65002])],
1943            }),
1944            PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
1945            PathAttribute::Med(100),
1946            PathAttribute::LocalPref(200),
1947        ];
1948
1949        let mut buf = Vec::new();
1950        encode_path_attributes(&attrs, &mut buf, true, false);
1951        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
1952        assert_eq!(decoded, attrs);
1953    }
1954
1955    #[test]
1956    fn roundtrip_attributes_2byte() {
1957        let attrs = vec![
1958            PathAttribute::Origin(Origin::Egp),
1959            PathAttribute::AsPath(AsPath {
1960                segments: vec![AsPathSegment::AsSequence(vec![100, 200])],
1961            }),
1962            PathAttribute::NextHop(Ipv4Addr::new(172, 16, 0, 1)),
1963        ];
1964
1965        let mut buf = Vec::new();
1966        encode_path_attributes(&attrs, &mut buf, false, false);
1967        let decoded = decode_path_attributes(&buf, false, &[]).unwrap();
1968        assert_eq!(decoded, attrs);
1969    }
1970
1971    #[test]
1972    fn reject_truncated_attribute_header() {
1973        let buf = [0x40]; // only 1 byte
1974        assert!(decode_path_attributes(&buf, true, &[]).is_err());
1975    }
1976
1977    #[test]
1978    fn reject_truncated_attribute_value() {
1979        // ORIGIN claims 1 byte value but nothing follows
1980        let buf = [0x40, 0x01, 0x01];
1981        assert!(decode_path_attributes(&buf, true, &[]).is_err());
1982    }
1983
1984    #[test]
1985    fn reject_bad_origin_length() {
1986        // ORIGIN with 2-byte value
1987        let buf = [0x40, 0x01, 0x02, 0x00, 0x00];
1988        assert!(decode_path_attributes(&buf, true, &[]).is_err());
1989    }
1990
1991    #[test]
1992    fn as_path_with_set_and_sequence() {
1993        // AS_SEQUENCE [65001], AS_SET [65002, 65003]
1994        let attrs = vec![PathAttribute::AsPath(AsPath {
1995            segments: vec![
1996                AsPathSegment::AsSequence(vec![65001]),
1997                AsPathSegment::AsSet(vec![65002, 65003]),
1998            ],
1999        })];
2000
2001        let mut buf = Vec::new();
2002        encode_path_attributes(&attrs, &mut buf, true, false);
2003        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2004        assert_eq!(decoded, attrs);
2005    }
2006
2007    #[test]
2008    fn decode_communities_single() {
2009        // flags=0xC0 (optional+transitive), type=8, len=4, community=65001:100
2010        // 65001 = 0xFDE9, 100 = 0x0064 → u32 = 0xFDE90064
2011        let community: u32 = (65001 << 16) | 0x0064;
2012        let bytes = community.to_be_bytes();
2013        let buf = [0xC0, 0x08, 0x04, bytes[0], bytes[1], bytes[2], bytes[3]];
2014        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2015        assert_eq!(attrs.len(), 1);
2016        assert_eq!(attrs[0], PathAttribute::Communities(vec![community]));
2017    }
2018
2019    #[test]
2020    fn decode_communities_multiple() {
2021        let c1: u32 = (65001 << 16) | 0x0064;
2022        let c2: u32 = (65002 << 16) | 0x00C8;
2023        let b1 = c1.to_be_bytes();
2024        let b2 = c2.to_be_bytes();
2025        let buf = [
2026            0xC0, 0x08, 0x08, b1[0], b1[1], b1[2], b1[3], b2[0], b2[1], b2[2], b2[3],
2027        ];
2028        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2029        assert_eq!(attrs[0], PathAttribute::Communities(vec![c1, c2]));
2030    }
2031
2032    #[test]
2033    fn decode_communities_empty() {
2034        // flags=0xC0, type=8, len=0
2035        let buf = [0xC0, 0x08, 0x00];
2036        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2037        assert_eq!(attrs[0], PathAttribute::Communities(vec![]));
2038    }
2039
2040    #[test]
2041    fn decode_communities_odd_length_rejected() {
2042        // flags=0xC0, type=8, len=3, only 3 bytes (not multiple of 4)
2043        let buf = [0xC0, 0x08, 0x03, 0x01, 0x02, 0x03];
2044        assert!(decode_path_attributes(&buf, true, &[]).is_err());
2045    }
2046
2047    #[test]
2048    fn communities_roundtrip() {
2049        let c1: u32 = (65001 << 16) | 0x0064;
2050        let c2: u32 = (65002 << 16) | 0x00C8;
2051        let attrs = vec![PathAttribute::Communities(vec![c1, c2])];
2052
2053        let mut buf = Vec::new();
2054        encode_path_attributes(&attrs, &mut buf, true, false);
2055        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2056        assert_eq!(decoded, attrs);
2057    }
2058
2059    #[test]
2060    fn communities_type_code_and_flags() {
2061        let attr = PathAttribute::Communities(vec![]);
2062        assert_eq!(attr.type_code(), 8);
2063        assert_eq!(attr.flags(), attr_flags::OPTIONAL | attr_flags::TRANSITIVE);
2064    }
2065
2066    // --- Extended Communities (RFC 4360) tests ---
2067
2068    #[test]
2069    fn decode_extended_communities_single() {
2070        // Route Target 65001:100 — type 0x00, subtype 0x02, AS 65001 (2-octet), value 100
2071        let ec = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2072        let bytes = ec.as_u64().to_be_bytes();
2073        let buf = [
2074            0xC0, 0x10, 0x08, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
2075            bytes[7],
2076        ];
2077        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2078        assert_eq!(attrs.len(), 1);
2079        assert_eq!(attrs[0], PathAttribute::ExtendedCommunities(vec![ec]));
2080    }
2081
2082    #[test]
2083    fn decode_extended_communities_multiple() {
2084        let ec1 = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2085        let ec2 = ExtendedCommunity::new(0x0003_FDEA_0000_00C8);
2086        let b1 = ec1.as_u64().to_be_bytes();
2087        let b2 = ec2.as_u64().to_be_bytes();
2088        let mut buf = vec![0xC0, 0x10, 16]; // flags, type=16, len=16
2089        buf.extend_from_slice(&b1);
2090        buf.extend_from_slice(&b2);
2091        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2092        assert_eq!(attrs[0], PathAttribute::ExtendedCommunities(vec![ec1, ec2]));
2093    }
2094
2095    #[test]
2096    fn decode_extended_communities_empty() {
2097        let buf = [0xC0, 0x10, 0x00];
2098        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2099        assert_eq!(attrs[0], PathAttribute::ExtendedCommunities(vec![]));
2100    }
2101
2102    #[test]
2103    fn decode_extended_communities_bad_length() {
2104        // length 5 is not a multiple of 8
2105        let buf = [0xC0, 0x10, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05];
2106        assert!(decode_path_attributes(&buf, true, &[]).is_err());
2107    }
2108
2109    #[test]
2110    fn extended_communities_roundtrip() {
2111        let ec1 = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2112        let ec2 = ExtendedCommunity::new(0x0003_FDEA_0000_00C8);
2113        let attrs = vec![PathAttribute::ExtendedCommunities(vec![ec1, ec2])];
2114
2115        let mut buf = Vec::new();
2116        encode_path_attributes(&attrs, &mut buf, true, false);
2117        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2118        assert_eq!(decoded, attrs);
2119    }
2120
2121    #[test]
2122    fn extended_communities_type_code_and_flags() {
2123        let attr = PathAttribute::ExtendedCommunities(vec![]);
2124        assert_eq!(attr.type_code(), 16);
2125        assert_eq!(attr.flags(), attr_flags::OPTIONAL | attr_flags::TRANSITIVE);
2126    }
2127
2128    #[test]
2129    fn extended_community_type_subtype() {
2130        // Type 0x00, Sub-type 0x02 (Route Target, 2-octet AS)
2131        let ec = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2132        assert_eq!(ec.type_byte(), 0x00);
2133        assert_eq!(ec.subtype(), 0x02);
2134        assert!(ec.is_transitive());
2135    }
2136
2137    #[test]
2138    fn extended_community_route_target() {
2139        // 2-octet AS RT: type=0x00, subtype=0x02, AS=65001, value=100
2140        let ec = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2141        assert_eq!(ec.route_target(), Some((65001, 100)));
2142        assert_eq!(ec.route_origin(), None);
2143
2144        // 4-octet AS RT: type=0x02, subtype=0x02, AS=65537, value=200
2145        let ec4 = ExtendedCommunity::new(0x0202_0001_0001_00C8);
2146        assert_eq!(ec4.route_target(), Some((65537, 200)));
2147
2148        // IPv4-specific RT: type=0x01, subtype=0x02, IP=192.0.2.1, value=100
2149        // 192.0.2.1 = 0xC0000201
2150        let ec_ipv4 = ExtendedCommunity::new(0x0102_C000_0201_0064);
2151        let (g, l) = ec_ipv4.route_target().unwrap();
2152        assert_eq!(g, 0xC000_0201); // 192.0.2.1 as u32
2153        assert_eq!(l, 100);
2154        // Callers distinguish via type_byte()
2155        assert_eq!(ec_ipv4.type_byte() & 0x3F, 0x01);
2156    }
2157
2158    #[test]
2159    fn extended_community_is_transitive() {
2160        // Type 0x00 → transitive (bit 6 = 0)
2161        let t = ExtendedCommunity::new(0x0002_0000_0000_0000);
2162        assert!(t.is_transitive());
2163
2164        // Type 0x40 → non-transitive (bit 6 = 1)
2165        let nt = ExtendedCommunity::new(0x4002_0000_0000_0000);
2166        assert!(!nt.is_transitive());
2167    }
2168
2169    #[test]
2170    fn extended_community_display() {
2171        let rt = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2172        assert_eq!(rt.to_string(), "RT:65001:100");
2173
2174        let ro = ExtendedCommunity::new(0x0003_FDE9_0000_0064);
2175        assert_eq!(ro.to_string(), "RO:65001:100");
2176
2177        // IPv4-specific RT: type=0x01, subtype=0x02, IP=192.0.2.1, value=100
2178        let target_v4 = ExtendedCommunity::new(0x0102_C000_0201_0064);
2179        assert_eq!(target_v4.to_string(), "RT:192.0.2.1:100");
2180
2181        // IPv4-specific RO
2182        let origin_v4 = ExtendedCommunity::new(0x0103_C000_0201_0064);
2183        assert_eq!(origin_v4.to_string(), "RO:192.0.2.1:100");
2184
2185        // 4-octet AS RT
2186        let rt_as4 = ExtendedCommunity::new(0x0202_0001_0001_00C8);
2187        assert_eq!(rt_as4.to_string(), "RT:65537:200");
2188
2189        // Non-transitive opaque → hex fallback
2190        let opaque = ExtendedCommunity::new(0x4300_1234_5678_9ABC);
2191        assert_eq!(opaque.to_string(), "0x4300123456789abc");
2192    }
2193
2194    #[test]
2195    fn unknown_attribute_roundtrip() {
2196        // Input has flags 0xC0 (optional+transitive). After encoding, the
2197        // Partial bit is OR'd in for transitive unknowns → 0xE0.
2198        let attrs = vec![PathAttribute::Unknown(RawAttribute {
2199            flags: 0xC0,
2200            type_code: 99,
2201            data: Bytes::from_static(&[1, 2, 3, 4, 5]),
2202        })];
2203
2204        let mut buf = Vec::new();
2205        encode_path_attributes(&attrs, &mut buf, true, false);
2206        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2207        assert_eq!(
2208            decoded,
2209            vec![PathAttribute::Unknown(RawAttribute {
2210                flags: 0xE0, // Partial bit set on re-advertisement
2211                type_code: 99,
2212                data: Bytes::from_static(&[1, 2, 3, 4, 5]),
2213            })]
2214        );
2215    }
2216
2217    #[test]
2218    fn origin_with_optional_flag_rejected() {
2219        // ORIGIN with flags 0xC0 (Optional+Transitive) — should be 0x40 (Transitive only)
2220        let buf = [0xC0, 0x01, 0x01, 0x00];
2221        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2222        match &err {
2223            DecodeError::UpdateAttributeError { subcode, .. } => {
2224                assert_eq!(*subcode, update_subcode::ATTRIBUTE_FLAGS_ERROR);
2225            }
2226            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2227        }
2228    }
2229
2230    #[test]
2231    fn med_with_transitive_flag_rejected() {
2232        // MED with flags 0xC0 (Optional+Transitive) — should be 0x80 (Optional only)
2233        let buf = [0xC0, 0x04, 0x04, 0, 0, 0, 100];
2234        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2235        match &err {
2236            DecodeError::UpdateAttributeError { subcode, .. } => {
2237                assert_eq!(*subcode, update_subcode::ATTRIBUTE_FLAGS_ERROR);
2238            }
2239            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2240        }
2241    }
2242
2243    #[test]
2244    fn communities_without_optional_rejected() {
2245        // COMMUNITIES with flags 0x40 (Transitive only) — should be 0xC0 (Optional+Transitive)
2246        let buf = [0x40, 0x08, 0x04, 0, 0, 0, 100];
2247        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2248        match &err {
2249            DecodeError::UpdateAttributeError { subcode, .. } => {
2250                assert_eq!(*subcode, update_subcode::ATTRIBUTE_FLAGS_ERROR);
2251            }
2252            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2253        }
2254    }
2255
2256    #[test]
2257    fn next_hop_length_error_subcode() {
2258        // NEXT_HOP with 3 bytes instead of 4
2259        let buf = [0x40, 0x03, 0x03, 10, 0, 0];
2260        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2261        match &err {
2262            DecodeError::UpdateAttributeError { subcode, .. } => {
2263                assert_eq!(*subcode, update_subcode::ATTRIBUTE_LENGTH_ERROR);
2264            }
2265            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2266        }
2267    }
2268
2269    #[test]
2270    fn invalid_origin_value_subcode() {
2271        // ORIGIN with value 5 → subcode 6 (INVALID_ORIGIN)
2272        let buf = [0x40, 0x01, 0x01, 0x05];
2273        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2274        match &err {
2275            DecodeError::UpdateAttributeError { subcode, .. } => {
2276                assert_eq!(*subcode, update_subcode::INVALID_ORIGIN);
2277            }
2278            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2279        }
2280    }
2281
2282    #[test]
2283    fn as_path_bad_segment_subcode() {
2284        // AS_PATH with unknown segment type 5
2285        let buf = [
2286            0x40, 0x02, 0x06, // AS_PATH header, length 6
2287            0x05, 0x01, // unknown segment type 5, count 1
2288            0x00, 0x00, 0xFD, 0xE9, // ASN 65001
2289        ];
2290        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2291        match &err {
2292            DecodeError::UpdateAttributeError { subcode, .. } => {
2293                assert_eq!(*subcode, update_subcode::MALFORMED_AS_PATH);
2294            }
2295            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2296        }
2297    }
2298
2299    #[test]
2300    fn encode_unknown_transitive_sets_partial() {
2301        let attr = PathAttribute::Unknown(RawAttribute {
2302            flags: attr_flags::OPTIONAL | attr_flags::TRANSITIVE, // 0xC0
2303            type_code: 99,
2304            data: Bytes::from_static(&[1, 2]),
2305        });
2306        let mut buf = Vec::new();
2307        encode_path_attributes(&[attr], &mut buf, true, false);
2308        // First byte is flags — should have PARTIAL bit set
2309        assert_eq!(
2310            buf[0],
2311            attr_flags::OPTIONAL | attr_flags::TRANSITIVE | attr_flags::PARTIAL
2312        );
2313    }
2314
2315    #[test]
2316    fn encode_unknown_wellknown_transitive_no_partial() {
2317        // Well-known transitive (OPTIONAL=0, TRANSITIVE=1) should NOT get PARTIAL
2318        let attr = PathAttribute::Unknown(RawAttribute {
2319            flags: attr_flags::TRANSITIVE, // 0x40, well-known transitive
2320            type_code: 99,
2321            data: Bytes::from_static(&[1, 2]),
2322        });
2323        let mut buf = Vec::new();
2324        encode_path_attributes(&[attr], &mut buf, true, false);
2325        assert_eq!(buf[0], attr_flags::TRANSITIVE);
2326    }
2327
2328    #[test]
2329    fn encode_unknown_nontransitive_no_partial() {
2330        let attr = PathAttribute::Unknown(RawAttribute {
2331            flags: attr_flags::OPTIONAL, // 0x80, no Transitive
2332            type_code: 99,
2333            data: Bytes::from_static(&[1, 2]),
2334        });
2335        let mut buf = Vec::new();
2336        encode_path_attributes(&[attr], &mut buf, true, false);
2337        // First byte is flags — should NOT have PARTIAL bit
2338        assert_eq!(buf[0], attr_flags::OPTIONAL);
2339    }
2340
2341    // --- MP_REACH_NLRI / MP_UNREACH_NLRI tests ---
2342
2343    /// Helper to create a `NlriEntry` with `path_id=0`.
2344    fn nlri(prefix: Prefix) -> NlriEntry {
2345        NlriEntry { path_id: 0, prefix }
2346    }
2347
2348    #[test]
2349    fn mp_reach_nlri_ipv6_roundtrip() {
2350        use crate::capability::{Afi, Safi};
2351        use crate::nlri::{Ipv6Prefix, Prefix};
2352
2353        let mp = MpReachNlri {
2354            afi: Afi::Ipv6,
2355            safi: Safi::Unicast,
2356            next_hop: IpAddr::V6("2001:db8::1".parse().unwrap()),
2357            announced: vec![
2358                nlri(Prefix::V6(Ipv6Prefix::new(
2359                    "2001:db8:1::".parse().unwrap(),
2360                    48,
2361                ))),
2362                nlri(Prefix::V6(Ipv6Prefix::new(
2363                    "2001:db8:2::".parse().unwrap(),
2364                    48,
2365                ))),
2366            ],
2367            flowspec_announced: vec![],
2368            evpn_announced: vec![],
2369        };
2370        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2371
2372        let mut buf = Vec::new();
2373        encode_path_attributes(&attrs, &mut buf, true, false);
2374        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2375        assert_eq!(decoded.len(), 1);
2376        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2377    }
2378
2379    #[test]
2380    fn mp_unreach_nlri_ipv6_roundtrip() {
2381        use crate::capability::{Afi, Safi};
2382        use crate::nlri::{Ipv6Prefix, Prefix};
2383
2384        let mp = MpUnreachNlri {
2385            afi: Afi::Ipv6,
2386            safi: Safi::Unicast,
2387            withdrawn: vec![nlri(Prefix::V6(Ipv6Prefix::new(
2388                "2001:db8:1::".parse().unwrap(),
2389                48,
2390            )))],
2391            flowspec_withdrawn: vec![],
2392            evpn_withdrawn: vec![],
2393        };
2394        let attrs = vec![PathAttribute::MpUnreachNlri(mp.clone())];
2395
2396        let mut buf = Vec::new();
2397        encode_path_attributes(&attrs, &mut buf, true, false);
2398        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2399        assert_eq!(decoded.len(), 1);
2400        assert_eq!(decoded[0], PathAttribute::MpUnreachNlri(mp));
2401    }
2402
2403    #[test]
2404    fn mp_reach_nlri_ipv4_roundtrip() {
2405        use crate::capability::{Afi, Safi};
2406        use crate::nlri::Prefix;
2407
2408        let mp = MpReachNlri {
2409            afi: Afi::Ipv4,
2410            safi: Safi::Unicast,
2411            next_hop: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
2412            announced: vec![nlri(Prefix::V4(crate::nlri::Ipv4Prefix::new(
2413                Ipv4Addr::new(10, 1, 0, 0),
2414                16,
2415            )))],
2416            flowspec_announced: vec![],
2417            evpn_announced: vec![],
2418        };
2419        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2420
2421        let mut buf = Vec::new();
2422        encode_path_attributes(&attrs, &mut buf, true, false);
2423        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2424        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2425    }
2426
2427    #[test]
2428    fn mp_reach_nlri_ipv4_with_ipv6_nexthop_roundtrip() {
2429        use crate::capability::{Afi, Safi};
2430        use crate::nlri::Prefix;
2431
2432        let mp = MpReachNlri {
2433            afi: Afi::Ipv4,
2434            safi: Safi::Unicast,
2435            next_hop: IpAddr::V6("2001:db8::1".parse().unwrap()),
2436            announced: vec![nlri(Prefix::V4(crate::nlri::Ipv4Prefix::new(
2437                Ipv4Addr::new(10, 1, 0, 0),
2438                16,
2439            )))],
2440            flowspec_announced: vec![],
2441            evpn_announced: vec![],
2442        };
2443        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2444
2445        let mut buf = Vec::new();
2446        encode_path_attributes(&attrs, &mut buf, true, false);
2447        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2448        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2449    }
2450
2451    #[test]
2452    fn mp_reach_nlri_type_code_and_flags() {
2453        use crate::capability::{Afi, Safi};
2454
2455        let attr = PathAttribute::MpReachNlri(MpReachNlri {
2456            afi: Afi::Ipv6,
2457            safi: Safi::Unicast,
2458            next_hop: IpAddr::V6(Ipv6Addr::UNSPECIFIED),
2459            announced: vec![],
2460            flowspec_announced: vec![],
2461            evpn_announced: vec![],
2462        });
2463        assert_eq!(attr.type_code(), 14);
2464        // RFC 4760 §3: MP_REACH_NLRI is optional non-transitive
2465        assert_eq!(attr.flags(), attr_flags::OPTIONAL);
2466    }
2467
2468    #[test]
2469    fn mp_unreach_nlri_type_code_and_flags() {
2470        use crate::capability::{Afi, Safi};
2471
2472        let attr = PathAttribute::MpUnreachNlri(MpUnreachNlri {
2473            afi: Afi::Ipv6,
2474            safi: Safi::Unicast,
2475            withdrawn: vec![],
2476            flowspec_withdrawn: vec![],
2477            evpn_withdrawn: vec![],
2478        });
2479        assert_eq!(attr.type_code(), 15);
2480        assert_eq!(attr.flags(), attr_flags::OPTIONAL);
2481    }
2482
2483    #[test]
2484    fn mp_reach_nlri_empty_nlri() {
2485        use crate::capability::{Afi, Safi};
2486
2487        let mp = MpReachNlri {
2488            afi: Afi::Ipv6,
2489            safi: Safi::Unicast,
2490            next_hop: IpAddr::V6("fe80::1".parse().unwrap()),
2491            announced: vec![],
2492            flowspec_announced: vec![],
2493            evpn_announced: vec![],
2494        };
2495        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2496
2497        let mut buf = Vec::new();
2498        encode_path_attributes(&attrs, &mut buf, true, false);
2499        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2500        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2501    }
2502
2503    #[test]
2504    fn mp_reach_nlri_bad_flags_rejected() {
2505        // MP_REACH_NLRI (type 14) with flags 0x40 (Transitive only)
2506        // — should be 0xC0 (Optional+Transitive)
2507        // Build minimal valid value: AFI=2, SAFI=1, NH-Len=16, NH=::1, Reserved=0
2508        let mut value = Vec::new();
2509        value.extend_from_slice(&2u16.to_be_bytes()); // AFI IPv6
2510        value.push(1); // SAFI Unicast
2511        value.push(16); // NH-Len
2512        value.extend_from_slice(&"::1".parse::<Ipv6Addr>().unwrap().octets()); // NH
2513        value.push(0); // Reserved
2514
2515        let mut buf = Vec::new();
2516        buf.push(0x40); // flags: Transitive only (wrong)
2517        buf.push(14); // type: MP_REACH_NLRI
2518        #[expect(clippy::cast_possible_truncation)]
2519        buf.push(value.len() as u8);
2520        buf.extend_from_slice(&value);
2521
2522        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2523        assert!(matches!(
2524            err,
2525            DecodeError::UpdateAttributeError {
2526                subcode: 4, // ATTRIBUTE_FLAGS_ERROR
2527                ..
2528            }
2529        ));
2530    }
2531
2532    // --- MP Add-Path decode tests ---
2533
2534    #[test]
2535    #[expect(clippy::cast_possible_truncation)]
2536    fn mp_reach_nlri_ipv4_addpath_decode() {
2537        use crate::capability::{Afi, Safi};
2538        use crate::nlri::Prefix;
2539
2540        // Build MP_REACH_NLRI with Add-Path-encoded IPv4 NLRI:
2541        // path_id(4) + prefix_len(1) + prefix_bytes
2542        let mut value = Vec::new();
2543        value.extend_from_slice(&1u16.to_be_bytes()); // AFI IPv4
2544        value.push(1); // SAFI Unicast
2545        value.push(4); // NH-Len
2546        value.extend_from_slice(&[10, 0, 0, 1]); // Next Hop
2547        value.push(0); // Reserved
2548        // Add-Path NLRI: path_id=42, 10.1.0.0/16
2549        value.extend_from_slice(&42u32.to_be_bytes());
2550        value.push(16);
2551        value.extend_from_slice(&[10, 1]);
2552
2553        let mut buf = Vec::new();
2554        buf.push(0x90); // flags: Optional + Extended Length
2555        buf.push(14); // type: MP_REACH_NLRI
2556        buf.extend_from_slice(&(value.len() as u16).to_be_bytes());
2557        buf.extend_from_slice(&value);
2558
2559        // With Add-Path for IPv4 unicast → decode path_id
2560        let decoded = decode_path_attributes(&buf, true, &[(Afi::Ipv4, Safi::Unicast)]).unwrap();
2561        let PathAttribute::MpReachNlri(mp) = &decoded[0] else {
2562            panic!("expected MpReachNlri");
2563        };
2564        assert_eq!(mp.announced.len(), 1);
2565        assert_eq!(mp.announced[0].path_id, 42);
2566        assert!(matches!(mp.announced[0].prefix, Prefix::V4(p) if p.len == 16));
2567
2568        // Without Add-Path → plain decoder misinterprets the path_id bytes
2569        // as prefix encoding and rejects the garbled data.
2570        assert!(decode_path_attributes(&buf, true, &[]).is_err());
2571    }
2572
2573    #[test]
2574    #[expect(clippy::cast_possible_truncation)]
2575    fn mp_reach_nlri_ipv6_addpath_decode() {
2576        use crate::capability::{Afi, Safi};
2577        use crate::nlri::{Ipv6Prefix, Prefix};
2578
2579        // Build MP_REACH_NLRI with Add-Path-encoded IPv6 NLRI
2580        let mut value = Vec::new();
2581        value.extend_from_slice(&2u16.to_be_bytes()); // AFI IPv6
2582        value.push(1); // SAFI Unicast
2583        value.push(16); // NH-Len
2584        value.extend_from_slice(&"2001:db8::1".parse::<Ipv6Addr>().unwrap().octets());
2585        value.push(0); // Reserved
2586        // Add-Path NLRI: path_id=99, 2001:db8:1::/48
2587        value.extend_from_slice(&99u32.to_be_bytes());
2588        value.push(48);
2589        value.extend_from_slice(&[0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01]);
2590
2591        let mut buf = Vec::new();
2592        buf.push(0x90); // flags: Optional + Extended Length
2593        buf.push(14); // type: MP_REACH_NLRI
2594        buf.extend_from_slice(&(value.len() as u16).to_be_bytes());
2595        buf.extend_from_slice(&value);
2596
2597        let decoded = decode_path_attributes(&buf, true, &[(Afi::Ipv6, Safi::Unicast)]).unwrap();
2598        let PathAttribute::MpReachNlri(mp) = &decoded[0] else {
2599            panic!("expected MpReachNlri");
2600        };
2601        assert_eq!(mp.announced.len(), 1);
2602        assert_eq!(mp.announced[0].path_id, 99);
2603        assert_eq!(
2604            mp.announced[0].prefix,
2605            Prefix::V6(Ipv6Prefix::new("2001:db8:1::".parse().unwrap(), 48))
2606        );
2607    }
2608
2609    #[test]
2610    #[expect(clippy::cast_possible_truncation)]
2611    fn mp_unreach_nlri_ipv6_addpath_decode() {
2612        use crate::capability::{Afi, Safi};
2613        use crate::nlri::{Ipv6Prefix, Prefix};
2614
2615        // Build MP_UNREACH_NLRI with Add-Path-encoded IPv6 NLRI
2616        let mut value = Vec::new();
2617        value.extend_from_slice(&2u16.to_be_bytes()); // AFI IPv6
2618        value.push(1); // SAFI Unicast
2619        // Add-Path NLRI: path_id=7, 2001:db8:2::/48
2620        value.extend_from_slice(&7u32.to_be_bytes());
2621        value.push(48);
2622        value.extend_from_slice(&[0x20, 0x01, 0x0d, 0xb8, 0x00, 0x02]);
2623
2624        let mut buf = Vec::new();
2625        buf.push(0x90); // flags: Optional + Extended Length
2626        buf.push(15); // type: MP_UNREACH_NLRI
2627        buf.extend_from_slice(&(value.len() as u16).to_be_bytes());
2628        buf.extend_from_slice(&value);
2629
2630        let decoded = decode_path_attributes(&buf, true, &[(Afi::Ipv6, Safi::Unicast)]).unwrap();
2631        let PathAttribute::MpUnreachNlri(mp) = &decoded[0] else {
2632            panic!("expected MpUnreachNlri");
2633        };
2634        assert_eq!(mp.withdrawn.len(), 1);
2635        assert_eq!(mp.withdrawn[0].path_id, 7);
2636        assert_eq!(
2637            mp.withdrawn[0].prefix,
2638            Prefix::V6(Ipv6Prefix::new("2001:db8:2::".parse().unwrap(), 48))
2639        );
2640    }
2641
2642    #[test]
2643    fn mp_reach_addpath_only_applies_to_matching_family() {
2644        use crate::capability::{Afi, Safi};
2645        use crate::nlri::{Ipv6Prefix, Prefix};
2646
2647        // Build plain (non-Add-Path) MP_REACH_NLRI for IPv6
2648        let mp = MpReachNlri {
2649            afi: Afi::Ipv6,
2650            safi: Safi::Unicast,
2651            next_hop: IpAddr::V6("2001:db8::1".parse().unwrap()),
2652            announced: vec![NlriEntry {
2653                path_id: 0,
2654                prefix: Prefix::V6(Ipv6Prefix::new("2001:db8:1::".parse().unwrap(), 48)),
2655            }],
2656            flowspec_announced: vec![],
2657            evpn_announced: vec![],
2658        };
2659        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2660
2661        let mut buf = Vec::new();
2662        encode_path_attributes(&attrs, &mut buf, true, false);
2663
2664        // Add-Path enabled for IPv4 only — IPv6 should still decode as plain
2665        let decoded = decode_path_attributes(&buf, true, &[(Afi::Ipv4, Safi::Unicast)]).unwrap();
2666        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2667    }
2668
2669    // --- ORIGINATOR_ID tests ---
2670
2671    #[test]
2672    fn decode_originator_id() {
2673        // flags=0x80 (optional), type=9, len=4, value=1.2.3.4
2674        let buf = [0x80, 0x09, 0x04, 1, 2, 3, 4];
2675        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2676        assert_eq!(
2677            attrs[0],
2678            PathAttribute::OriginatorId(Ipv4Addr::new(1, 2, 3, 4))
2679        );
2680    }
2681
2682    #[test]
2683    fn originator_id_roundtrip() {
2684        let attr = PathAttribute::OriginatorId(Ipv4Addr::new(10, 0, 0, 1));
2685        let mut buf = Vec::new();
2686        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
2687        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2688        assert_eq!(decoded, vec![attr]);
2689    }
2690
2691    #[test]
2692    fn originator_id_wrong_length() {
2693        // 3 bytes instead of 4
2694        let buf = [0x80, 0x09, 0x03, 1, 2, 3];
2695        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2696        assert!(matches!(
2697            err,
2698            DecodeError::UpdateAttributeError {
2699                subcode: 5, // ATTRIBUTE_LENGTH_ERROR
2700                ..
2701            }
2702        ));
2703    }
2704
2705    #[test]
2706    fn originator_id_wrong_flags() {
2707        // flags=0x40 (transitive) — should be 0x80 (optional)
2708        let buf = [0x40, 0x09, 0x04, 1, 2, 3, 4];
2709        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2710        assert!(matches!(
2711            err,
2712            DecodeError::UpdateAttributeError {
2713                subcode: 4, // ATTRIBUTE_FLAGS_ERROR
2714                ..
2715            }
2716        ));
2717    }
2718
2719    // --- CLUSTER_LIST tests ---
2720
2721    #[test]
2722    fn decode_cluster_list() {
2723        // flags=0x80 (optional), type=10, len=8, two cluster IDs
2724        let buf = [0x80, 0x0A, 0x08, 1, 2, 3, 4, 5, 6, 7, 8];
2725        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2726        assert_eq!(
2727            attrs[0],
2728            PathAttribute::ClusterList(vec![Ipv4Addr::new(1, 2, 3, 4), Ipv4Addr::new(5, 6, 7, 8),])
2729        );
2730    }
2731
2732    #[test]
2733    fn cluster_list_roundtrip() {
2734        let attr = PathAttribute::ClusterList(vec![
2735            Ipv4Addr::new(10, 0, 0, 1),
2736            Ipv4Addr::new(10, 0, 0, 2),
2737        ]);
2738        let mut buf = Vec::new();
2739        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
2740        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2741        assert_eq!(decoded, vec![attr]);
2742    }
2743
2744    #[test]
2745    fn cluster_list_wrong_length() {
2746        // 5 bytes — not a multiple of 4
2747        let buf = [0x80, 0x0A, 0x05, 1, 2, 3, 4, 5];
2748        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2749        assert!(matches!(
2750            err,
2751            DecodeError::UpdateAttributeError {
2752                subcode: 5, // ATTRIBUTE_LENGTH_ERROR
2753                ..
2754            }
2755        ));
2756    }
2757
2758    // -----------------------------------------------------------------------
2759    // Large Communities (RFC 8092)
2760    // -----------------------------------------------------------------------
2761
2762    #[test]
2763    fn large_community_display() {
2764        let lc = LargeCommunity::new(65001, 100, 200);
2765        assert_eq!(lc.to_string(), "65001:100:200");
2766    }
2767
2768    #[test]
2769    fn large_community_type_code_and_flags() {
2770        let attr = PathAttribute::LargeCommunities(vec![LargeCommunity::new(1, 2, 3)]);
2771        assert_eq!(attr.type_code(), attr_type::LARGE_COMMUNITIES);
2772        assert_eq!(attr.flags(), attr_flags::OPTIONAL | attr_flags::TRANSITIVE);
2773    }
2774
2775    #[test]
2776    fn decode_large_community_single() {
2777        // flags=0xC0 (Optional|Transitive), type=32, length=12
2778        let mut buf = vec![0xC0, 32, 12];
2779        buf.extend_from_slice(&65001u32.to_be_bytes());
2780        buf.extend_from_slice(&100u32.to_be_bytes());
2781        buf.extend_from_slice(&200u32.to_be_bytes());
2782        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2783        assert_eq!(attrs.len(), 1);
2784        assert_eq!(
2785            attrs[0],
2786            PathAttribute::LargeCommunities(vec![LargeCommunity::new(65001, 100, 200)])
2787        );
2788    }
2789
2790    #[test]
2791    fn decode_large_community_multiple() {
2792        // Two LCs: 24 bytes total
2793        let mut buf = vec![0xC0, 32, 24];
2794        for (g, l1, l2) in [(65001u32, 100u32, 200u32), (65002, 300, 400)] {
2795            buf.extend_from_slice(&g.to_be_bytes());
2796            buf.extend_from_slice(&l1.to_be_bytes());
2797            buf.extend_from_slice(&l2.to_be_bytes());
2798        }
2799        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2800        assert_eq!(
2801            attrs[0],
2802            PathAttribute::LargeCommunities(vec![
2803                LargeCommunity::new(65001, 100, 200),
2804                LargeCommunity::new(65002, 300, 400),
2805            ])
2806        );
2807    }
2808
2809    #[test]
2810    fn decode_large_community_bad_length() {
2811        // 10 bytes — not a multiple of 12
2812        let buf = [0xC0, 32, 10, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0];
2813        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2814        assert!(matches!(
2815            err,
2816            DecodeError::UpdateAttributeError {
2817                subcode: 5, // ATTRIBUTE_LENGTH_ERROR
2818                ..
2819            }
2820        ));
2821    }
2822
2823    #[test]
2824    fn decode_large_community_empty_rejected() {
2825        // Zero-length LARGE_COMMUNITIES is rejected (must carry at least one community).
2826        let buf = [0xC0, 32, 0];
2827        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2828        assert!(matches!(
2829            err,
2830            DecodeError::UpdateAttributeError {
2831                subcode: 5, // ATTRIBUTE_LENGTH_ERROR
2832                ..
2833            }
2834        ));
2835    }
2836
2837    #[test]
2838    fn large_community_roundtrip() {
2839        let lcs = vec![
2840            LargeCommunity::new(65001, 100, 200),
2841            LargeCommunity::new(0, u32::MAX, 42),
2842        ];
2843        let attr = PathAttribute::LargeCommunities(lcs.clone());
2844        let mut buf = Vec::new();
2845        encode_path_attributes(&[attr], &mut buf, true, false);
2846        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2847        assert_eq!(decoded.len(), 1);
2848        assert_eq!(decoded[0], PathAttribute::LargeCommunities(lcs));
2849    }
2850
2851    #[test]
2852    fn large_community_expected_flags_validated() {
2853        // Wrong flags: TRANSITIVE only (0x40) instead of OPTIONAL|TRANSITIVE (0xC0)
2854        let mut buf = vec![0x40, 32, 12];
2855        buf.extend_from_slice(&1u32.to_be_bytes());
2856        buf.extend_from_slice(&2u32.to_be_bytes());
2857        buf.extend_from_slice(&3u32.to_be_bytes());
2858        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2859        assert!(matches!(
2860            err,
2861            DecodeError::UpdateAttributeError {
2862                subcode: 4, // ATTRIBUTE_FLAGS_ERROR
2863                ..
2864            }
2865        ));
2866    }
2867
2868    // -----------------------------------------------------------------------
2869    // AsPath::to_aspath_string()
2870    // -----------------------------------------------------------------------
2871
2872    #[test]
2873    fn aspath_string_sequence() {
2874        let p = AsPath {
2875            segments: vec![AsPathSegment::AsSequence(vec![65001, 65002, 65003])],
2876        };
2877        assert_eq!(p.to_aspath_string(), "65001 65002 65003");
2878    }
2879
2880    #[test]
2881    fn aspath_string_set() {
2882        let p = AsPath {
2883            segments: vec![AsPathSegment::AsSet(vec![65003, 65004])],
2884        };
2885        assert_eq!(p.to_aspath_string(), "{65003 65004}");
2886    }
2887
2888    #[test]
2889    fn aspath_string_mixed() {
2890        let p = AsPath {
2891            segments: vec![
2892                AsPathSegment::AsSequence(vec![65001, 65002]),
2893                AsPathSegment::AsSet(vec![65003, 65004]),
2894            ],
2895        };
2896        assert_eq!(p.to_aspath_string(), "65001 65002 {65003 65004}");
2897    }
2898
2899    #[test]
2900    fn aspath_string_empty() {
2901        let p = AsPath { segments: vec![] };
2902        assert_eq!(p.to_aspath_string(), "");
2903    }
2904
2905    /// Regression: SAFI 70 (EVPN) is only valid under AFI 25 (L2VPN).
2906    /// Other AFIs with SAFI=Evpn must be rejected explicitly so the
2907    /// unicast NLRI fallthrough never tries to parse the typed EVPN
2908    /// payload as a prefix list.
2909    #[test]
2910    fn mp_reach_nlri_rejects_evpn_safi_with_non_l2vpn_afi() {
2911        // AFI=Ipv4 (1), SAFI=Evpn (70), NH-len=4, NH=192.0.2.1, reserved=0,
2912        // followed by an arbitrary EVPN-shaped byte (route type 3, len 0).
2913        let bytes = vec![
2914            0x00, 0x01, // AFI = Ipv4
2915            70,   // SAFI = Evpn
2916            4, 192, 0, 2, 1, // NH len + NH
2917            0, // reserved
2918            3, 0, // EVPN-style NLRI (route type 3, length 0)
2919        ];
2920        let err = decode_mp_reach_nlri(&bytes, &[]).unwrap_err();
2921        match err {
2922            DecodeError::MalformedField { detail, .. } => {
2923                assert!(detail.contains("SAFI EVPN"), "unexpected detail: {detail}");
2924            }
2925            other => panic!("expected MalformedField, got {other:?}"),
2926        }
2927    }
2928
2929    #[test]
2930    fn mp_unreach_nlri_rejects_evpn_safi_with_non_l2vpn_afi() {
2931        let bytes = vec![
2932            0x00, 0x02, // AFI = Ipv6
2933            70,   // SAFI = Evpn
2934            3, 0, // EVPN-style withdrawal (route type 3, length 0)
2935        ];
2936        let err = decode_mp_unreach_nlri(&bytes, &[]).unwrap_err();
2937        match err {
2938            DecodeError::MalformedField { detail, .. } => {
2939                assert!(detail.contains("SAFI EVPN"), "unexpected detail: {detail}");
2940            }
2941            other => panic!("expected MalformedField, got {other:?}"),
2942        }
2943    }
2944}