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