Skip to main content

rustbgpd_wire/
attribute.rs

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