Skip to main content

async_snmp/pdu/
mod.rs

1//! SNMP Protocol Data Units (PDUs).
2//!
3//! PDUs represent the different SNMP operations.
4
5use crate::ber::{Decoder, EncodeBuf, tag};
6use crate::error::internal::DecodeErrorKind;
7use crate::error::{Error, ErrorStatus, Result, UNKNOWN_TARGET};
8use crate::oid::Oid;
9use crate::varbind::{VarBind, decode_varbind_list, encode_varbind_list};
10
11/// Clamp a GETBULK `non-repeaters`/`max-repetitions` field to the RFC 3416
12/// Section 4.2.3 range `INTEGER (0..2147483647)`.
13///
14/// `i32::MAX` already equals the upper bound, so only the negative floor needs
15/// enforcing. Both GETBULK encode paths apply it at their choke point:
16/// `Pdu::encode` clamps the overloaded `error_status`/`error_index` for the
17/// SNMPv3 representation (so even a directly-constructed `Pdu` with negative
18/// fields is normalized), and `GetBulkPdu::encode` clamps `non_repeaters`/
19/// `max_repetitions` for the community path. It guarantees neither encoder can
20/// emit a negative value on the wire.
21///
22/// The receive side reuses the same clamp: `Pdu::decode` applies it to the
23/// overloaded GETBULK `error_status`/`error_index` fields so a negative value
24/// from a buggy peer is normalized to 0 rather than rejected (net-snmp behavior).
25const fn clamp_bulk_field(value: i32) -> i32 {
26    if value < 0 { 0 } else { value }
27}
28
29/// PDU type tag.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31#[repr(u8)]
32pub enum PduType {
33    /// GET request - retrieve specific OID values.
34    GetRequest = 0xA0,
35    /// GET-NEXT request - retrieve the next OID in the MIB tree.
36    GetNextRequest = 0xA1,
37    /// Response to a request from an agent.
38    Response = 0xA2,
39    /// SET request - modify OID values.
40    SetRequest = 0xA3,
41    /// `SNMPv1` trap - unsolicited notification from an agent.
42    TrapV1 = 0xA4,
43    /// GET-BULK request - efficient bulk retrieval of table data.
44    GetBulkRequest = 0xA5,
45    /// INFORM request - acknowledged notification.
46    InformRequest = 0xA6,
47    /// SNMPv2c/v3 trap - unsolicited notification from an agent.
48    TrapV2 = 0xA7,
49    /// Report - used in `SNMPv3` for engine discovery and error reporting.
50    Report = 0xA8,
51}
52
53impl PduType {
54    /// Create from tag byte.
55    #[must_use]
56    pub fn from_tag(tag: u8) -> Option<Self> {
57        match tag {
58            0xA0 => Some(Self::GetRequest),
59            0xA1 => Some(Self::GetNextRequest),
60            0xA2 => Some(Self::Response),
61            0xA3 => Some(Self::SetRequest),
62            0xA4 => Some(Self::TrapV1),
63            0xA5 => Some(Self::GetBulkRequest),
64            0xA6 => Some(Self::InformRequest),
65            0xA7 => Some(Self::TrapV2),
66            0xA8 => Some(Self::Report),
67            _ => None,
68        }
69    }
70
71    /// Get the tag byte.
72    #[must_use]
73    pub fn tag(self) -> u8 {
74        self as u8
75    }
76}
77
78impl std::fmt::Display for PduType {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            Self::GetRequest => write!(f, "GetRequest"),
82            Self::GetNextRequest => write!(f, "GetNextRequest"),
83            Self::Response => write!(f, "Response"),
84            Self::SetRequest => write!(f, "SetRequest"),
85            Self::TrapV1 => write!(f, "TrapV1"),
86            Self::GetBulkRequest => write!(f, "GetBulkRequest"),
87            Self::InformRequest => write!(f, "InformRequest"),
88            Self::TrapV2 => write!(f, "TrapV2"),
89            Self::Report => write!(f, "Report"),
90        }
91    }
92}
93
94/// Generic PDU structure for request/response operations.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Pdu {
97    /// PDU type
98    pub pdu_type: PduType,
99    /// Request ID for correlating requests and responses
100    pub request_id: i32,
101    /// Error status (0 for requests, error code for responses)
102    pub error_status: i32,
103    /// Error index (1-based index of problematic varbind)
104    pub error_index: i32,
105    /// Variable bindings
106    pub varbinds: Vec<VarBind>,
107}
108
109impl Pdu {
110    /// Create a new GET request PDU.
111    #[must_use]
112    pub fn get_request(request_id: i32, oids: &[Oid]) -> Self {
113        Self {
114            pdu_type: PduType::GetRequest,
115            request_id,
116            error_status: 0,
117            error_index: 0,
118            varbinds: oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
119        }
120    }
121
122    /// Create a new GETNEXT request PDU.
123    #[must_use]
124    pub fn get_next_request(request_id: i32, oids: &[Oid]) -> Self {
125        Self {
126            pdu_type: PduType::GetNextRequest,
127            request_id,
128            error_status: 0,
129            error_index: 0,
130            varbinds: oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
131        }
132    }
133
134    /// Create a new SET request PDU.
135    #[must_use]
136    pub fn set_request(request_id: i32, varbinds: Vec<VarBind>) -> Self {
137        Self {
138            pdu_type: PduType::SetRequest,
139            request_id,
140            error_status: 0,
141            error_index: 0,
142            varbinds,
143        }
144    }
145
146    /// Create a SNMPv2c/v3 Trap PDU.
147    ///
148    /// Prepends the mandatory varbind prefix per RFC 3416 Section 4.2.6:
149    /// 1. sysUpTime.0 (1.3.6.1.2.1.1.3.0) with `TimeTicks` value
150    /// 2. snmpTrapOID.0 (1.3.6.1.6.3.1.1.4.1.0) with the trap OID
151    ///
152    /// Caller-provided varbinds are appended after the prefix.
153    #[must_use]
154    pub fn trap_v2(request_id: i32, uptime: u32, trap_oid: &Oid, varbinds: Vec<VarBind>) -> Self {
155        let mut all_varbinds = Vec::with_capacity(2 + varbinds.len());
156        all_varbinds.push(VarBind::new(
157            crate::notification::oids::sys_uptime(),
158            crate::value::Value::TimeTicks(uptime),
159        ));
160        all_varbinds.push(VarBind::new(
161            crate::notification::oids::snmp_trap_oid(),
162            crate::value::Value::ObjectIdentifier(trap_oid.clone()),
163        ));
164        all_varbinds.extend(varbinds);
165        Self {
166            pdu_type: PduType::TrapV2,
167            request_id,
168            error_status: 0,
169            error_index: 0,
170            varbinds: all_varbinds,
171        }
172    }
173
174    /// Create an `InformRequest` PDU.
175    ///
176    /// Same varbind structure as `trap_v2` (sysUpTime.0 + snmpTrapOID.0 prefix),
177    /// but uses `InformRequest` PDU type which expects a Response from the receiver.
178    #[must_use]
179    pub fn inform_request(
180        request_id: i32,
181        uptime: u32,
182        trap_oid: &Oid,
183        varbinds: Vec<VarBind>,
184    ) -> Self {
185        let mut all_varbinds = Vec::with_capacity(2 + varbinds.len());
186        all_varbinds.push(VarBind::new(
187            crate::notification::oids::sys_uptime(),
188            crate::value::Value::TimeTicks(uptime),
189        ));
190        all_varbinds.push(VarBind::new(
191            crate::notification::oids::snmp_trap_oid(),
192            crate::value::Value::ObjectIdentifier(trap_oid.clone()),
193        ));
194        all_varbinds.extend(varbinds);
195        Self {
196            pdu_type: PduType::InformRequest,
197            request_id,
198            error_status: 0,
199            error_index: 0,
200            varbinds: all_varbinds,
201        }
202    }
203
204    /// Create a GETBULK request PDU.
205    ///
206    /// Note: For GETBULK, `error_status` holds `non_repeaters` and `error_index` holds `max_repetitions`.
207    /// Both are clamped via `clamp_bulk_field` so this (SNMPv3) encode path cannot emit a negative
208    /// value on the wire.
209    #[must_use]
210    pub fn get_bulk(
211        request_id: i32,
212        non_repeaters: i32,
213        max_repetitions: i32,
214        varbinds: Vec<VarBind>,
215    ) -> Self {
216        Self {
217            pdu_type: PduType::GetBulkRequest,
218            request_id,
219            error_status: clamp_bulk_field(non_repeaters),
220            error_index: clamp_bulk_field(max_repetitions),
221            varbinds,
222        }
223    }
224
225    /// Encode to BER.
226    pub fn encode(&self, buf: &mut EncodeBuf) {
227        // The SNMPv1 Trap PDU has a distinct wire layout and must be encoded via
228        // `TrapV1Pdu::encode`; the generic layout here is not round-trippable for it.
229        debug_assert!(
230            self.pdu_type != PduType::TrapV1,
231            "TrapV1 must be encoded via TrapV1Pdu, not the generic Pdu encoder"
232        );
233        // For GETBULK, error_status/error_index overload as non-repeaters and
234        // max-repetitions (RFC 3416 Section 4.2.3, INTEGER 0..2147483647). Clamp
235        // negatives to 0 at the encode choke point so a directly-constructed
236        // `Pdu { pdu_type: GetBulkRequest, error_status: -1, .. }` cannot emit a
237        // negative value on the wire, regardless of how the fields were set.
238        let (error_status, error_index) = if self.pdu_type == PduType::GetBulkRequest {
239            (
240                clamp_bulk_field(self.error_status),
241                clamp_bulk_field(self.error_index),
242            )
243        } else {
244            (self.error_status, self.error_index)
245        };
246
247        buf.push_constructed(self.pdu_type.tag(), |buf| {
248            encode_varbind_list(buf, &self.varbinds);
249            buf.push_integer(error_index);
250            buf.push_integer(error_status);
251            buf.push_integer(self.request_id);
252        });
253    }
254
255    /// Decode from BER (after tag has been peeked).
256    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
257        let tag = decoder.read_tag()?;
258        let pdu_type = PduType::from_tag(tag).ok_or_else(|| {
259            tracing::debug!(target: "async_snmp::pdu", { offset = decoder.offset(), tag = tag, kind = %DecodeErrorKind::UnknownPduType(tag) }, "decode error");
260            Error::MalformedResponse {
261                target: UNKNOWN_TARGET,
262            }
263            .boxed()
264        })?;
265
266        // The SNMPv1 Trap PDU (tag 0xA4) has a distinct wire layout (RFC 1157
267        // Section 4.1.6) that this generic decoder cannot parse correctly. It is
268        // only ever carried in v1 messages and must be routed through
269        // `TrapV1Pdu::decode`; reaching it here means a v2c/v3 message carried a
270        // v1 Trap tag, which is rejected.
271        if pdu_type == PduType::TrapV1 {
272            tracing::debug!(target: "async_snmp::pdu", { offset = decoder.offset(), tag = tag }, "TrapV1 PDU tag not valid in generic PDU context");
273            return Err(Error::MalformedResponse {
274                target: UNKNOWN_TARGET,
275            }
276            .boxed());
277        }
278
279        let len = decoder.read_length()?;
280        let mut pdu_decoder = decoder.sub_decoder(len)?;
281
282        let request_id = pdu_decoder.read_integer()?;
283        let mut error_status = pdu_decoder.read_integer()?;
284        let mut error_index = pdu_decoder.read_integer()?;
285        let varbinds = decode_varbind_list(&mut pdu_decoder)?;
286
287        // For GETBULK, error_status/error_index overload as non-repeaters and
288        // max-repetitions (RFC 3416 Section 4.2.3, INTEGER 0..2147483647). Clamp
289        // negatives to 0 here so the single decode path normalizes them before the
290        // agent sees them, matching net-snmp (snmp_agent.c) and the agent's own
291        // normalization. The upper bound already equals i32::MAX, so only the
292        // negative floor needs enforcing.
293        if pdu_type == PduType::GetBulkRequest {
294            error_status = clamp_bulk_field(error_status);
295            error_index = clamp_bulk_field(error_index);
296        }
297
298        // For non-GETBULK PDUs, error_index is not validated here. net-snmp
299        // performs no bounds checking on this field (validation code in
300        // snmp_client.c is wrapped in #ifdef TEMPORARILY_DISABLED). RFC 3416
301        // Section 3 annotates it "sometimes ignored" and places no MUST/SHOULD
302        // obligation on receivers. Rejecting out-of-range values would break
303        // compatibility with buggy agents that work fine with net-snmp.
304
305        Ok(Pdu {
306            pdu_type,
307            request_id,
308            error_status,
309            error_index,
310            varbinds,
311        })
312    }
313
314    /// Check if this is an error response.
315    #[must_use]
316    pub fn is_error(&self) -> bool {
317        self.pdu_type == PduType::Response && self.error_status != 0
318    }
319
320    /// Get the error status as an enum.
321    #[must_use]
322    pub fn error_status_enum(&self) -> ErrorStatus {
323        ErrorStatus::from_i32(self.error_status)
324    }
325
326    /// Create a Response PDU from this PDU (for Inform handling).
327    ///
328    /// The response copies the `request_id` and variable bindings,
329    /// sets `error_status` and `error_index` to 0, and changes the PDU type to Response.
330    #[must_use]
331    pub fn to_response(&self) -> Self {
332        Self {
333            pdu_type: PduType::Response,
334            request_id: self.request_id,
335            error_status: 0,
336            error_index: 0,
337            varbinds: self.varbinds.clone(),
338        }
339    }
340
341    /// Create a Response PDU with specific error status.
342    #[must_use]
343    pub fn to_error_response(&self, error_status: ErrorStatus, error_index: i32) -> Self {
344        Self {
345            pdu_type: PduType::Response,
346            request_id: self.request_id,
347            error_status: error_status.as_i32(),
348            error_index,
349            varbinds: self.varbinds.clone(),
350        }
351    }
352
353    /// Convert a v2 notification PDU to a v1 `TrapV1Pdu` (RFC 3584 Section 3.2).
354    ///
355    /// Extracts the v1 fields from the standard v2 notification varbind layout:
356    /// - sysUpTime.0 (first varbind) -> `time_stamp`
357    /// - snmpTrapOID.0 (second varbind) -> `generic_trap`, `specific_trap`, enterprise
358    /// - snmpTrapAddress.0 varbind (if present) -> `agent_addr`
359    /// - snmpTrapEnterprise.0 varbind (if present) -> enterprise (for standard traps)
360    ///
361    /// Per RFC 3584 Section 3.2: if any varbind is Counter64, the trap cannot be
362    /// represented in v1 and `None` is returned.
363    ///
364    /// The `default_addr` parameter provides the `agent_addr` when no
365    /// snmpTrapAddress.0 varbind is present (typically the local IP address,
366    /// or `[0,0,0,0]` if unknown).
367    ///
368    /// Returns `None` if:
369    /// - The PDU has fewer than 2 varbinds
370    /// - The first varbind is not `TimeTicks`
371    /// - The second varbind is not an OID
372    /// - Any varbind contains a Counter64 value
373    #[must_use]
374    pub fn to_v1_trap(&self, default_addr: [u8; 4]) -> Option<TrapV1Pdu> {
375        use crate::notification::oids;
376        use crate::value::Value;
377
378        if self.varbinds.len() < 2 {
379            return None;
380        }
381
382        // Verify OID names per RFC 3416 Section 4.2.6: the first two varbinds
383        // must be sysUpTime.0 and snmpTrapOID.0.
384        if self.varbinds[0].oid != oids::sys_uptime() {
385            return None;
386        }
387        if self.varbinds[1].oid != oids::snmp_trap_oid() {
388            return None;
389        }
390
391        let time_stamp = match &self.varbinds[0].value {
392            Value::TimeTicks(t) => *t,
393            _ => return None,
394        };
395
396        let Value::ObjectIdentifier(trap_oid) = &self.varbinds[1].value else {
397            return None;
398        };
399
400        // Check for Counter64 in any varbind (RFC 3584 Section 3.2, rule 6)
401        for vb in &self.varbinds {
402            if matches!(vb.value, Value::Counter64(_)) {
403                return None;
404            }
405        }
406
407        // Derive generic_trap, specific_trap, and enterprise from snmpTrapOID
408        let snmp_traps_prefix = oids::snmp_traps();
409        let (generic_trap, specific_trap, enterprise) = if trap_oid.starts_with(&snmp_traps_prefix)
410            && trap_oid.len() == snmp_traps_prefix.len() + 1
411            && (1..=6).contains(&trap_oid.arcs()[trap_oid.len() - 1])
412        {
413            // Standard trap: snmpTraps.{generic+1}
414            let last_arc = trap_oid.arcs()[trap_oid.len() - 1];
415            let generic = GenericTrap::from_i32((last_arc - 1) as i32);
416            // For standard traps, enterprise comes from snmpTrapEnterprise.0
417            // varbind if present, otherwise use snmpTraps
418            let enterprise_oid = oids::snmp_trap_enterprise();
419            let ent = self.varbinds[2..]
420                .iter()
421                .find(|vb| vb.oid == enterprise_oid)
422                .and_then(|vb| match &vb.value {
423                    Value::ObjectIdentifier(oid) => Some(oid.clone()),
424                    _ => None,
425                })
426                .unwrap_or_else(|| snmp_traps_prefix.clone());
427            (generic, 0, ent)
428        } else if trap_oid.len() >= 2 {
429            // Enterprise-specific trap. RFC 3584 Section 3.2:
430            // - If next-to-last sub-id is zero: enterprise = OID minus last 2 arcs
431            // - If next-to-last sub-id is non-zero: enterprise = OID minus last arc
432            let arcs = trap_oid.arcs();
433            let specific = i32::try_from(arcs[arcs.len() - 1]).ok()?;
434            let next_to_last = arcs[arcs.len() - 2];
435            let enterprise = if next_to_last == 0 {
436                Oid::from_slice(&arcs[..arcs.len() - 2])
437            } else {
438                Oid::from_slice(&arcs[..arcs.len() - 1])
439            };
440            (GenericTrap::EnterpriseSpecific, specific, enterprise)
441        } else {
442            return None;
443        };
444
445        // Extract agent_addr from snmpTrapAddress.0 varbind if present
446        let trap_address_oid = oids::snmp_trap_address();
447        let agent_addr = self.varbinds[2..]
448            .iter()
449            .find(|vb| vb.oid == trap_address_oid)
450            .and_then(|vb| match &vb.value {
451                Value::IpAddress(addr) => Some(*addr),
452                _ => None,
453            })
454            .unwrap_or(default_addr);
455
456        // RFC 3584 Section 3.2 rule (6): the SNMPv1 varbinds are the SNMPv2
457        // varbinds minus only the sysUpTime.0/snmpTrapOID.0 prefix;
458        // snmpTrapAddress.0 and snmpTrapEnterprise.0 are retained.
459        let varbinds: Vec<VarBind> = self.varbinds[2..].to_vec();
460
461        Some(TrapV1Pdu {
462            enterprise,
463            agent_addr,
464            generic_trap,
465            specific_trap,
466            time_stamp,
467            varbinds,
468        })
469    }
470
471    /// Check if this is a notification PDU (Trap or Inform).
472    #[must_use]
473    pub fn is_notification(&self) -> bool {
474        matches!(
475            self.pdu_type,
476            PduType::TrapV1 | PduType::TrapV2 | PduType::InformRequest
477        )
478    }
479
480    /// Check if this is a confirmed-class PDU (requires response).
481    #[must_use]
482    pub fn is_confirmed(&self) -> bool {
483        matches!(
484            self.pdu_type,
485            PduType::GetRequest
486                | PduType::GetNextRequest
487                | PduType::GetBulkRequest
488                | PduType::SetRequest
489                | PduType::InformRequest
490        )
491    }
492}
493
494/// `SNMPv1` generic trap types (RFC 1157 Section 4.1.6).
495#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
496pub enum GenericTrap {
497    /// coldStart(0) - agent is reinitializing, config may change
498    ColdStart,
499    /// warmStart(1) - agent is reinitializing, config unchanged
500    WarmStart,
501    /// linkDown(2) - communication link failure
502    LinkDown,
503    /// linkUp(3) - communication link came up
504    LinkUp,
505    /// authenticationFailure(4) - improperly authenticated message received
506    AuthenticationFailure,
507    /// egpNeighborLoss(5) - EGP peer marked down
508    EgpNeighborLoss,
509    /// enterpriseSpecific(6) - vendor-specific trap, see `specific_trap` field
510    EnterpriseSpecific,
511    /// An unrecognized generic trap value received on the wire.
512    Unknown(i32),
513}
514
515impl std::fmt::Display for GenericTrap {
516    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517        match self {
518            Self::ColdStart => write!(f, "coldStart"),
519            Self::WarmStart => write!(f, "warmStart"),
520            Self::LinkDown => write!(f, "linkDown"),
521            Self::LinkUp => write!(f, "linkUp"),
522            Self::AuthenticationFailure => write!(f, "authenticationFailure"),
523            Self::EgpNeighborLoss => write!(f, "egpNeighborLoss"),
524            Self::EnterpriseSpecific => write!(f, "enterpriseSpecific"),
525            Self::Unknown(v) => write!(f, "unknown({v})"),
526        }
527    }
528}
529
530impl GenericTrap {
531    /// Create from integer value.
532    #[must_use]
533    pub fn from_i32(v: i32) -> Self {
534        match v {
535            0 => Self::ColdStart,
536            1 => Self::WarmStart,
537            2 => Self::LinkDown,
538            3 => Self::LinkUp,
539            4 => Self::AuthenticationFailure,
540            5 => Self::EgpNeighborLoss,
541            6 => Self::EnterpriseSpecific,
542            _ => Self::Unknown(v),
543        }
544    }
545
546    /// Get the integer value.
547    #[must_use]
548    pub fn as_i32(self) -> i32 {
549        match self {
550            Self::ColdStart => 0,
551            Self::WarmStart => 1,
552            Self::LinkDown => 2,
553            Self::LinkUp => 3,
554            Self::AuthenticationFailure => 4,
555            Self::EgpNeighborLoss => 5,
556            Self::EnterpriseSpecific => 6,
557            Self::Unknown(v) => v,
558        }
559    }
560}
561
562/// `SNMPv1` Trap PDU (RFC 1157 Section 4.1.6).
563///
564/// This PDU type has a completely different structure from other PDUs.
565/// It is only used in `SNMPv1` and is replaced by SNMPv2-Trap in v2c/v3.
566#[derive(Debug, Clone, PartialEq, Eq)]
567pub struct TrapV1Pdu {
568    /// Enterprise OID (sysObjectID of the entity generating the trap)
569    pub enterprise: Oid,
570    /// Agent address (IP address of the agent generating the trap)
571    pub agent_addr: [u8; 4],
572    /// Generic trap type
573    pub generic_trap: GenericTrap,
574    /// Specific trap code (meaningful when `generic_trap` is enterpriseSpecific)
575    pub specific_trap: i32,
576    /// Time since the network entity was last (re)initialized (in hundredths of seconds)
577    pub time_stamp: u32,
578    /// Variable bindings containing "interesting" information
579    pub varbinds: Vec<VarBind>,
580}
581
582impl TrapV1Pdu {
583    /// Create a new `SNMPv1` Trap PDU.
584    #[must_use]
585    pub fn new(
586        enterprise: Oid,
587        agent_addr: [u8; 4],
588        generic_trap: GenericTrap,
589        specific_trap: i32,
590        time_stamp: u32,
591        varbinds: Vec<VarBind>,
592    ) -> Self {
593        Self {
594            enterprise,
595            agent_addr,
596            generic_trap,
597            specific_trap,
598            time_stamp,
599            varbinds,
600        }
601    }
602
603    /// Check if this is an enterprise-specific trap.
604    #[must_use]
605    pub fn is_enterprise_specific(&self) -> bool {
606        self.generic_trap == GenericTrap::EnterpriseSpecific
607    }
608
609    /// Convert to `SNMPv2` trap OID (RFC 3584 Section 3).
610    ///
611    /// RFC 3584 defines how to translate `SNMPv1` trap information to `SNMPv2`
612    /// snmpTrapOID.0 format:
613    ///
614    /// - For generic traps 0-5 (coldStart through egpNeighborLoss):
615    ///   The trap OID is `snmpTraps.{generic_trap + 1}` (1.3.6.1.6.3.1.1.5.{1-6})
616    ///
617    /// - For enterprise-specific traps (`generic_trap` = 6):
618    ///   The trap OID is `enterprise.0.specific_trap`
619    ///
620    /// # Errors
621    ///
622    /// Returns [`Error::InvalidOid`] if:
623    /// - `generic_trap` is `Unknown` with a negative value (undefined per RFC 1157)
624    /// - `generic_trap` is `Unknown` with value `i32::MAX` (would overflow when adding 1)
625    /// - `specific_trap < 0` for enterprise-specific traps (OID arcs must be non-negative)
626    ///
627    /// # Example
628    ///
629    /// ```rust
630    /// use async_snmp::pdu::{TrapV1Pdu, GenericTrap};
631    /// use async_snmp::oid;
632    ///
633    /// // Generic trap (linkDown = 2) -> snmpTraps.3
634    /// let trap = TrapV1Pdu::new(
635    ///     oid!(1, 3, 6, 1, 4, 1, 9999),
636    ///     [192, 168, 1, 1],
637    ///     GenericTrap::LinkDown,
638    ///     0,
639    ///     12345,
640    ///     vec![],
641    /// );
642    /// assert_eq!(trap.v2_trap_oid().unwrap(), oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3));
643    ///
644    /// // Enterprise-specific trap -> enterprise.0.specific_trap
645    /// let trap = TrapV1Pdu::new(
646    ///     oid!(1, 3, 6, 1, 4, 1, 9999),
647    ///     [192, 168, 1, 1],
648    ///     GenericTrap::EnterpriseSpecific,
649    ///     42,
650    ///     12345,
651    ///     vec![],
652    /// );
653    /// assert_eq!(trap.v2_trap_oid().unwrap(), oid!(1, 3, 6, 1, 4, 1, 9999, 0, 42));
654    /// ```
655    pub fn v2_trap_oid(&self) -> crate::Result<Oid> {
656        if self.is_enterprise_specific() {
657            if self.specific_trap < 0 {
658                return Err(Error::InvalidOid("specific_trap cannot be negative".into()).boxed());
659            }
660            let mut arcs: Vec<u32> = self.enterprise.arcs().to_vec();
661            arcs.push(0);
662            arcs.push(self.specific_trap as u32);
663            Ok(Oid::new(arcs))
664        } else {
665            let raw = self.generic_trap.as_i32();
666            if raw < 0 {
667                return Err(Error::InvalidOid("generic_trap cannot be negative".into()).boxed());
668            }
669            if raw == i32::MAX {
670                return Err(Error::InvalidOid("generic_trap overflow".into()).boxed());
671            }
672            let trap_num = raw + 1;
673            Ok(crate::oid!(1, 3, 6, 1, 6, 3, 1, 1, 5).child(trap_num as u32))
674        }
675    }
676
677    /// Convert to a v2 notification PDU (RFC 3584 Section 3.1).
678    ///
679    /// Performs the originator (non-proxy) conversion: only the mandatory
680    /// sysUpTime.0 and snmpTrapOID.0 prefix followed by the original varbinds.
681    /// Per RFC 3584 Section 3.1(4), the additional proxy varbinds
682    /// (snmpTrapAddress.0, snmpTrapCommunity.0, snmpTrapEnterprise.0) are only
683    /// appended when a proxy forwards a received trap.
684    ///
685    /// The `request_id` is set to 0; callers should assign their own.
686    ///
687    /// # Errors
688    ///
689    /// Returns an error if the trap OID cannot be computed (see [`Self::v2_trap_oid`]).
690    pub fn to_v2_pdu(&self) -> crate::Result<Pdu> {
691        use crate::notification::oids;
692        use crate::value::Value;
693
694        let trap_oid = self.v2_trap_oid()?;
695
696        let mut varbinds = Vec::with_capacity(2 + self.varbinds.len());
697        varbinds.push(VarBind::new(
698            oids::sys_uptime(),
699            Value::TimeTicks(self.time_stamp),
700        ));
701        varbinds.push(VarBind::new(
702            oids::snmp_trap_oid(),
703            Value::ObjectIdentifier(trap_oid),
704        ));
705        varbinds.extend_from_slice(&self.varbinds);
706
707        Ok(Pdu {
708            pdu_type: PduType::TrapV2,
709            request_id: 0,
710            error_status: 0,
711            error_index: 0,
712            varbinds,
713        })
714    }
715
716    /// Encode to BER.
717    pub fn encode(&self, buf: &mut EncodeBuf) {
718        buf.push_constructed(tag::pdu::TRAP_V1, |buf| {
719            encode_varbind_list(buf, &self.varbinds);
720            buf.push_unsigned32(tag::application::TIMETICKS, self.time_stamp);
721            buf.push_integer(self.specific_trap);
722            buf.push_integer(self.generic_trap.as_i32());
723            // NetworkAddress is APPLICATION 0 IMPLICIT IpAddress
724            // IpAddress is APPLICATION 0 IMPLICIT OCTET STRING (SIZE (4))
725            buf.push_bytes(&self.agent_addr);
726            buf.push_length(4);
727            buf.push_tag(tag::application::IP_ADDRESS);
728            buf.push_oid(&self.enterprise);
729        });
730    }
731
732    /// Decode from BER (after tag has been peeked).
733    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
734        let mut pdu = decoder.read_constructed(tag::pdu::TRAP_V1)?;
735
736        // enterprise OBJECT IDENTIFIER
737        let enterprise = pdu.read_oid()?;
738
739        // agent-addr NetworkAddress (IpAddress)
740        let agent_tag = pdu.read_tag()?;
741        if agent_tag != tag::application::IP_ADDRESS {
742            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), expected = 0x40_u8, actual = agent_tag, kind = %DecodeErrorKind::UnexpectedTag {
743                    expected: 0x40,
744                    actual: agent_tag,
745                } }, "decode error");
746            return Err(Error::MalformedResponse {
747                target: UNKNOWN_TARGET,
748            }
749            .boxed());
750        }
751        let agent_len = pdu.read_length()?;
752        if agent_len != 4 {
753            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), length = agent_len, kind = %DecodeErrorKind::InvalidIpAddressLength { length: agent_len } }, "decode error");
754            return Err(Error::MalformedResponse {
755                target: UNKNOWN_TARGET,
756            }
757            .boxed());
758        }
759        let agent_bytes = pdu.read_bytes(4)?;
760        let agent_addr = [
761            agent_bytes[0],
762            agent_bytes[1],
763            agent_bytes[2],
764            agent_bytes[3],
765        ];
766
767        // generic-trap INTEGER
768        let generic_trap = GenericTrap::from_i32(pdu.read_integer()?);
769
770        // specific-trap INTEGER
771        let specific_trap = pdu.read_integer()?;
772
773        // time-stamp TimeTicks
774        let ts_tag = pdu.read_tag()?;
775        if ts_tag != tag::application::TIMETICKS {
776            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), expected = 0x43_u8, actual = ts_tag, kind = %DecodeErrorKind::UnexpectedTag {
777                    expected: 0x43,
778                    actual: ts_tag,
779                } }, "decode error");
780            return Err(Error::MalformedResponse {
781                target: UNKNOWN_TARGET,
782            }
783            .boxed());
784        }
785        let ts_len = pdu.read_length()?;
786        let time_stamp = pdu.read_unsigned32_value(ts_len)?;
787
788        // variable-bindings
789        let varbinds = decode_varbind_list(&mut pdu)?;
790
791        Ok(TrapV1Pdu {
792            enterprise,
793            agent_addr,
794            generic_trap,
795            specific_trap,
796            time_stamp,
797            varbinds,
798        })
799    }
800}
801
802/// GETBULK request PDU.
803#[derive(Debug, Clone)]
804pub struct GetBulkPdu {
805    /// Request ID
806    pub request_id: i32,
807    /// Number of non-repeating OIDs
808    pub non_repeaters: i32,
809    /// Maximum repetitions for repeating OIDs
810    pub max_repetitions: i32,
811    /// Variable bindings
812    pub varbinds: Vec<VarBind>,
813}
814
815impl GetBulkPdu {
816    /// Create a new GETBULK request.
817    #[must_use]
818    pub fn new(request_id: i32, non_repeaters: i32, max_repetitions: i32, oids: &[Oid]) -> Self {
819        Self {
820            request_id,
821            non_repeaters,
822            max_repetitions,
823            varbinds: oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
824        }
825    }
826
827    /// Encode to BER.
828    pub fn encode(&self, buf: &mut EncodeBuf) {
829        buf.push_constructed(tag::pdu::GET_BULK_REQUEST, |buf| {
830            encode_varbind_list(buf, &self.varbinds);
831            // Clamp the RFC 3416 (0..2147483647) fields via the shared
832            // choke point so neither GETBULK encode path emits a negative.
833            buf.push_integer(clamp_bulk_field(self.max_repetitions));
834            buf.push_integer(clamp_bulk_field(self.non_repeaters));
835            buf.push_integer(self.request_id);
836        });
837    }
838
839    /// Decode from BER.
840    ///
841    /// Delegates to `Pdu::decode` so GETBULK requests share a single decode and
842    /// normalization path. `Pdu::decode` clamps negative non-repeaters and
843    /// max-repetitions to 0 per RFC 3416 Section 4.2.3 (net-snmp-compatible),
844    /// after which the overloaded `error_status`/`error_index` fields map back
845    /// onto the typed `GetBulkPdu` shape.
846    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
847        let pdu = Pdu::decode(decoder)?;
848
849        if pdu.pdu_type != PduType::GetBulkRequest {
850            tracing::debug!(target: "async_snmp::pdu", { pdu_type = ?pdu.pdu_type }, "expected GETBULK PDU");
851            return Err(Error::MalformedResponse {
852                target: UNKNOWN_TARGET,
853            }
854            .boxed());
855        }
856
857        Ok(GetBulkPdu {
858            request_id: pdu.request_id,
859            non_repeaters: pdu.error_status,
860            max_repetitions: pdu.error_index,
861            varbinds: pdu.varbinds,
862        })
863    }
864}
865
866#[cfg(test)]
867mod tests {
868    use super::*;
869    use crate::oid;
870
871    /// Test helper for encoding PDUs with arbitrary field values.
872    ///
873    /// Unlike `Pdu`, this allows encoding invalid values (negative `error_index`,
874    /// out-of-bounds indices, etc.) for testing decoder validation.
875    struct RawPdu {
876        pdu_type: u8,
877        request_id: i32,
878        error_status: i32,
879        error_index: i32,
880        varbinds: Vec<VarBind>,
881    }
882
883    impl RawPdu {
884        fn response(
885            request_id: i32,
886            error_status: i32,
887            error_index: i32,
888            varbinds: Vec<VarBind>,
889        ) -> Self {
890            Self {
891                pdu_type: PduType::Response.tag(),
892                request_id,
893                error_status,
894                error_index,
895                varbinds,
896            }
897        }
898
899        fn encode(&self) -> bytes::Bytes {
900            let mut buf = EncodeBuf::new();
901            buf.push_constructed(self.pdu_type, |buf| {
902                encode_varbind_list(buf, &self.varbinds);
903                buf.push_integer(self.error_index);
904                buf.push_integer(self.error_status);
905                buf.push_integer(self.request_id);
906            });
907            buf.finish()
908        }
909    }
910
911    /// Test helper for encoding GETBULK PDUs with arbitrary field values.
912    struct RawGetBulkPdu {
913        request_id: i32,
914        non_repeaters: i32,
915        max_repetitions: i32,
916        varbinds: Vec<VarBind>,
917    }
918
919    impl RawGetBulkPdu {
920        fn new(
921            request_id: i32,
922            non_repeaters: i32,
923            max_repetitions: i32,
924            varbinds: Vec<VarBind>,
925        ) -> Self {
926            Self {
927                request_id,
928                non_repeaters,
929                max_repetitions,
930                varbinds,
931            }
932        }
933
934        fn encode(&self) -> bytes::Bytes {
935            let mut buf = EncodeBuf::new();
936            buf.push_constructed(tag::pdu::GET_BULK_REQUEST, |buf| {
937                encode_varbind_list(buf, &self.varbinds);
938                buf.push_integer(self.max_repetitions);
939                buf.push_integer(self.non_repeaters);
940                buf.push_integer(self.request_id);
941            });
942            buf.finish()
943        }
944    }
945
946    #[test]
947    fn test_get_request_roundtrip() {
948        let pdu = Pdu::get_request(12345, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
949
950        let mut buf = EncodeBuf::new();
951        pdu.encode(&mut buf);
952        let bytes = buf.finish();
953
954        let mut decoder = Decoder::new(bytes);
955        let decoded = Pdu::decode(&mut decoder).unwrap();
956
957        assert_eq!(decoded.pdu_type, PduType::GetRequest);
958        assert_eq!(decoded.request_id, 12345);
959        assert_eq!(decoded.varbinds.len(), 1);
960    }
961
962    #[test]
963    fn test_getbulk_roundtrip() {
964        let pdu = GetBulkPdu::new(12345, 0, 10, &[oid!(1, 3, 6, 1, 2, 1, 1)]);
965
966        let mut buf = EncodeBuf::new();
967        pdu.encode(&mut buf);
968        let bytes = buf.finish();
969
970        let mut decoder = Decoder::new(bytes);
971        let decoded = GetBulkPdu::decode(&mut decoder).unwrap();
972
973        assert_eq!(decoded.request_id, 12345);
974        assert_eq!(decoded.non_repeaters, 0);
975        assert_eq!(decoded.max_repetitions, 10);
976    }
977
978    #[test]
979    fn test_trap_v1_roundtrip() {
980        use crate::value::Value;
981        use crate::varbind::VarBind;
982
983        let trap = TrapV1Pdu::new(
984            oid!(1, 3, 6, 1, 4, 1, 9999), // enterprise OID
985            [192, 168, 1, 1],             // agent address
986            GenericTrap::LinkDown,
987            0,
988            1234_5678, // time stamp
989            vec![VarBind::new(
990                oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1),
991                Value::Integer(1),
992            )],
993        );
994
995        let mut buf = EncodeBuf::new();
996        trap.encode(&mut buf);
997        let bytes = buf.finish();
998
999        let mut decoder = Decoder::new(bytes);
1000        let decoded = TrapV1Pdu::decode(&mut decoder).unwrap();
1001
1002        assert_eq!(decoded.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999));
1003        assert_eq!(decoded.agent_addr, [192, 168, 1, 1]);
1004        assert_eq!(decoded.generic_trap, GenericTrap::LinkDown);
1005        assert_eq!(decoded.specific_trap, 0);
1006        assert_eq!(decoded.time_stamp, 1234_5678);
1007        assert_eq!(decoded.varbinds.len(), 1);
1008    }
1009
1010    #[test]
1011    fn test_generic_pdu_decode_rejects_trap_v1_tag() {
1012        // The v1 Trap PDU (tag 0xA4) has a distinct wire layout and must only be
1013        // decoded via TrapV1Pdu. The generic Pdu decoder must reject the tag so a
1014        // v1 Trap cannot slip into a v2c/v3 (generic PDU) context.
1015        let trap = TrapV1Pdu::new(
1016            oid!(1, 3, 6, 1, 4, 1, 9999),
1017            [192, 168, 1, 1],
1018            GenericTrap::LinkDown,
1019            0,
1020            12345,
1021            vec![],
1022        );
1023        let mut buf = EncodeBuf::new();
1024        trap.encode(&mut buf);
1025        let bytes = buf.finish();
1026
1027        let mut decoder = Decoder::new(bytes);
1028        let result = Pdu::decode(&mut decoder);
1029        assert!(
1030            result.is_err(),
1031            "generic Pdu::decode must reject TrapV1 tag, got {result:?}"
1032        );
1033    }
1034
1035    #[test]
1036    fn test_trap_v1_enterprise_specific() {
1037        let trap = TrapV1Pdu::new(
1038            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
1039            [10, 0, 0, 1],
1040            GenericTrap::EnterpriseSpecific,
1041            42, // specific trap number
1042            100,
1043            vec![],
1044        );
1045
1046        assert!(trap.is_enterprise_specific());
1047        assert_eq!(trap.generic_trap, GenericTrap::EnterpriseSpecific);
1048
1049        let mut buf = EncodeBuf::new();
1050        trap.encode(&mut buf);
1051        let bytes = buf.finish();
1052
1053        let mut decoder = Decoder::new(bytes);
1054        let decoded = TrapV1Pdu::decode(&mut decoder).unwrap();
1055
1056        assert_eq!(decoded.specific_trap, 42);
1057    }
1058
1059    #[test]
1060    fn test_trap_v1_v2_trap_oid_generic_traps() {
1061        // Test all generic trap types translate to correct snmpTraps.X OIDs
1062        // RFC 3584 Section 3: snmpTraps.{generic_trap + 1}
1063
1064        let test_cases = [
1065            (GenericTrap::ColdStart, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)),
1066            (GenericTrap::WarmStart, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)),
1067            (GenericTrap::LinkDown, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)),
1068            (GenericTrap::LinkUp, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)),
1069            (
1070                GenericTrap::AuthenticationFailure,
1071                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5),
1072            ),
1073            (
1074                GenericTrap::EgpNeighborLoss,
1075                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6),
1076            ),
1077        ];
1078
1079        for (generic_trap, expected_oid) in test_cases {
1080            let trap = TrapV1Pdu::new(
1081                oid!(1, 3, 6, 1, 4, 1, 9999),
1082                [192, 168, 1, 1],
1083                generic_trap,
1084                0,
1085                12345,
1086                vec![],
1087            );
1088            assert_eq!(
1089                trap.v2_trap_oid().unwrap(),
1090                expected_oid,
1091                "Failed for {generic_trap:?}"
1092            );
1093        }
1094    }
1095
1096    #[test]
1097    fn test_trap_v1_v2_trap_oid_enterprise_specific() {
1098        // RFC 3584 Section 3: enterprise.0.specific_trap
1099        let trap = TrapV1Pdu::new(
1100            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
1101            [192, 168, 1, 1],
1102            GenericTrap::EnterpriseSpecific,
1103            42,
1104            12345,
1105            vec![],
1106        );
1107
1108        // Expected: 1.3.6.1.4.1.9999.1.2.0.42
1109        assert_eq!(
1110            trap.v2_trap_oid().unwrap(),
1111            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
1112        );
1113    }
1114
1115    #[test]
1116    fn test_trap_v1_v2_trap_oid_enterprise_specific_zero() {
1117        // Edge case: specific_trap = 0
1118        let trap = TrapV1Pdu::new(
1119            oid!(1, 3, 6, 1, 4, 1, 1234),
1120            [10, 0, 0, 1],
1121            GenericTrap::EnterpriseSpecific,
1122            0,
1123            100,
1124            vec![],
1125        );
1126
1127        // Expected: 1.3.6.1.4.1.1234.0.0
1128        assert_eq!(
1129            trap.v2_trap_oid().unwrap(),
1130            oid!(1, 3, 6, 1, 4, 1, 1234, 0, 0)
1131        );
1132    }
1133
1134    #[test]
1135    fn test_pdu_to_response() {
1136        use crate::value::Value;
1137        use crate::varbind::VarBind;
1138
1139        let inform = Pdu {
1140            pdu_type: PduType::InformRequest,
1141            request_id: 99999,
1142            error_status: 0,
1143            error_index: 0,
1144            varbinds: vec![
1145                VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), Value::TimeTicks(12345)),
1146                VarBind::new(
1147                    oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0),
1148                    Value::ObjectIdentifier(oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)),
1149                ),
1150            ],
1151        };
1152
1153        let response = inform.to_response();
1154
1155        assert_eq!(response.pdu_type, PduType::Response);
1156        assert_eq!(response.request_id, 99999);
1157        assert_eq!(response.error_status, 0);
1158        assert_eq!(response.error_index, 0);
1159        assert_eq!(response.varbinds.len(), 2);
1160    }
1161
1162    #[test]
1163    fn test_pdu_is_confirmed() {
1164        let get = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
1165        assert!(get.is_confirmed());
1166
1167        let inform = Pdu {
1168            pdu_type: PduType::InformRequest,
1169            request_id: 1,
1170            error_status: 0,
1171            error_index: 0,
1172            varbinds: vec![],
1173        };
1174        assert!(inform.is_confirmed());
1175
1176        let trap = Pdu {
1177            pdu_type: PduType::TrapV2,
1178            request_id: 1,
1179            error_status: 0,
1180            error_index: 0,
1181            varbinds: vec![],
1182        };
1183        assert!(!trap.is_confirmed());
1184        assert!(trap.is_notification());
1185    }
1186
1187    #[test]
1188    fn test_decode_accepts_negative_error_index() {
1189        // net-snmp does not validate error_index at parse time; validation code
1190        // that once existed in snmp_client.c is wrapped in #ifdef TEMPORARILY_DISABLED
1191        // and is never compiled. Buggy agents that send negative error_index values
1192        // are accepted by net-snmp and must be accepted here too, or users will
1193        // report "works with net-snmp but not your library".
1194        let raw = RawPdu::response(1, 0, -1, vec![VarBind::null(oid!(1, 3, 6, 1))]);
1195        let encoded = raw.encode();
1196
1197        let mut decoder = Decoder::new(encoded);
1198        let result = Pdu::decode(&mut decoder);
1199
1200        assert!(
1201            result.is_ok(),
1202            "negative error_index must be accepted to match net-snmp behavior, got {:?}",
1203            result.err()
1204        );
1205        assert_eq!(result.unwrap().error_index, -1);
1206    }
1207
1208    #[test]
1209    fn test_decode_accepts_error_index_beyond_varbinds() {
1210        // net-snmp does not bounds-check error_index against the varbind list length.
1211        // RFC 3416 Section 3 defines error-index as INTEGER (0..max-bindings) and
1212        // annotates it "sometimes ignored"; it places no MUST/SHOULD obligation on
1213        // receivers to reject out-of-range values. Buggy agents that send an
1214        // error_index larger than the varbind count are accepted by net-snmp.
1215        let raw = RawPdu::response(1, 5, 5, vec![VarBind::null(oid!(1, 3, 6, 1))]);
1216        let encoded = raw.encode();
1217
1218        let mut decoder = Decoder::new(encoded);
1219        let result = Pdu::decode(&mut decoder);
1220
1221        assert!(
1222            result.is_ok(),
1223            "error_index beyond varbind count must be accepted to match net-snmp behavior, got {:?}",
1224            result.err()
1225        );
1226        assert_eq!(result.unwrap().error_index, 5);
1227    }
1228
1229    #[test]
1230    fn test_decode_accepts_valid_error_index_zero() {
1231        // error_index=0 with no error is valid
1232        let raw = RawPdu::response(1, 0, 0, vec![VarBind::null(oid!(1, 3, 6, 1))]);
1233        let encoded = raw.encode();
1234
1235        let mut decoder = Decoder::new(encoded);
1236        let decoded = Pdu::decode(&mut decoder);
1237        assert!(decoded.is_ok(), "error_index=0 should be valid");
1238    }
1239
1240    #[test]
1241    fn test_decode_accepts_error_index_within_bounds() {
1242        // error_index=1 with 1 varbind is valid (1-based indexing)
1243        let raw = RawPdu::response(1, 5, 1, vec![VarBind::null(oid!(1, 3, 6, 1))]);
1244        let encoded = raw.encode();
1245
1246        let mut decoder = Decoder::new(encoded);
1247        let result = Pdu::decode(&mut decoder);
1248        assert!(
1249            result.is_ok(),
1250            "error_index=1 with 1 varbind should be valid"
1251        );
1252    }
1253
1254    #[test]
1255    fn test_decode_clamps_negative_non_repeaters() {
1256        // RFC 3416 Section 4.2.3: non-repeaters is INTEGER (0..2147483647).
1257        // A negative value from a buggy peer is normalized to 0 (net-snmp
1258        // snmp_agent.c behavior) rather than rejected.
1259        let raw = RawGetBulkPdu::new(1, -1, 10, vec![VarBind::null(oid!(1, 3, 6, 1))]);
1260        let encoded = raw.encode();
1261
1262        let mut decoder = Decoder::new(encoded);
1263        let decoded = GetBulkPdu::decode(&mut decoder).expect("negative non_repeaters clamps to 0");
1264        assert_eq!(decoded.non_repeaters, 0);
1265        assert_eq!(decoded.max_repetitions, 10);
1266    }
1267
1268    #[test]
1269    fn test_decode_clamps_negative_max_repetitions() {
1270        let raw = RawGetBulkPdu::new(1, 0, -5, vec![VarBind::null(oid!(1, 3, 6, 1))]);
1271        let encoded = raw.encode();
1272
1273        let mut decoder = Decoder::new(encoded);
1274        let decoded =
1275            GetBulkPdu::decode(&mut decoder).expect("negative max_repetitions clamps to 0");
1276        assert_eq!(decoded.non_repeaters, 0);
1277        assert_eq!(decoded.max_repetitions, 0);
1278    }
1279
1280    #[test]
1281    fn test_pdu_decode_getbulk_clamps_negative_non_repeaters_repro() {
1282        // Regression (audit F06): production GETBULK decode goes through the
1283        // generic Pdu::decode path (community.rs / v3.rs), which must clamp the
1284        // overloaded non-repeaters/max-repetitions to 0 for negatives.
1285        // Repro packet: GETBULK, request_id=1, non_repeaters=-1 (0xff),
1286        // max_repetitions=1, empty varbinds.
1287        let packet = [
1288            0xa5, 0x0b, 0x02, 0x01, 0x01, 0x02, 0x01, 0xff, 0x02, 0x01, 0x01, 0x30, 0x00,
1289        ];
1290        let mut decoder = Decoder::new(bytes::Bytes::copy_from_slice(&packet));
1291        let pdu = Pdu::decode(&mut decoder).expect("repro GETBULK packet must decode");
1292        assert_eq!(pdu.pdu_type, PduType::GetBulkRequest);
1293        assert_eq!(pdu.request_id, 1);
1294        // error_status = non_repeaters (was -1, clamped to 0)
1295        assert_eq!(pdu.error_status, 0);
1296        // error_index = max_repetitions
1297        assert_eq!(pdu.error_index, 1);
1298    }
1299
1300    #[test]
1301    fn test_decode_accepts_valid_getbulk_params() {
1302        let raw = RawGetBulkPdu::new(1, 0, 10, vec![VarBind::null(oid!(1, 3, 6, 1))]);
1303        let encoded = raw.encode();
1304
1305        let mut decoder = Decoder::new(encoded);
1306        let result = GetBulkPdu::decode(&mut decoder);
1307        assert!(result.is_ok(), "valid GETBULK params should be accepted");
1308
1309        let pdu = result.unwrap();
1310        assert_eq!(pdu.non_repeaters, 0);
1311        assert_eq!(pdu.max_repetitions, 10);
1312    }
1313
1314    #[test]
1315    fn test_encode_clamps_negative_non_repeaters_and_max_repetitions() {
1316        // RFC 3416 Section 4.2.3: non-repeaters and max-repetitions are
1317        // INTEGER (0..2147483647). GetBulkPdu::encode clamps negative values
1318        // to 0 before writing, so even a PDU built with negative fields
1319        // (e.g. via a raw i32 passed to Client::get_bulk) round-trips through
1320        // decode instead of producing a malformed request on the wire.
1321        let pdu = GetBulkPdu::new(1, -1, -5, &[oid!(1, 3, 6, 1)]);
1322
1323        let mut buf = EncodeBuf::new();
1324        pdu.encode(&mut buf);
1325        let bytes = buf.finish();
1326
1327        let mut decoder = Decoder::new(bytes);
1328        let result = GetBulkPdu::decode(&mut decoder);
1329        assert!(
1330            result.is_ok(),
1331            "encoded negative non_repeaters/max_repetitions should decode after clamping, got {result:?}"
1332        );
1333        let decoded = result.unwrap();
1334        assert_eq!(decoded.non_repeaters, 0);
1335        assert_eq!(decoded.max_repetitions, 0);
1336    }
1337
1338    #[test]
1339    fn test_encode_leaves_non_negative_non_repeaters_and_max_repetitions_unchanged() {
1340        let pdu = GetBulkPdu::new(1, 0, 10, &[oid!(1, 3, 6, 1)]);
1341
1342        let mut buf = EncodeBuf::new();
1343        pdu.encode(&mut buf);
1344        let bytes = buf.finish();
1345
1346        let mut decoder = Decoder::new(bytes);
1347        let decoded = GetBulkPdu::decode(&mut decoder).unwrap();
1348        assert_eq!(decoded.non_repeaters, 0);
1349        assert_eq!(decoded.max_repetitions, 10);
1350    }
1351
1352    #[test]
1353    fn test_generic_pdu_get_bulk_clamps_negative_fields() {
1354        // The SNMPv3 encode path builds a generic Pdu via Pdu::get_bulk (which
1355        // overloads error_status/error_index for non_repeaters/max_repetitions)
1356        // and serializes it with Pdu::encode, bypassing GetBulkPdu::encode. That
1357        // path must apply the same RFC 3416 (0..max-bindings) clamp so a negative
1358        // passed to Client::get_bulk on a v3 client never reaches the wire.
1359        let pdu = Pdu::get_bulk(1, -1, -5, vec![VarBind::null(oid!(1, 3, 6, 1))]);
1360        assert_eq!(pdu.error_status, 0);
1361        assert_eq!(pdu.error_index, 0);
1362
1363        let mut buf = EncodeBuf::new();
1364        pdu.encode(&mut buf);
1365        let bytes = buf.finish();
1366
1367        let mut decoder = Decoder::new(bytes);
1368        let decoded = GetBulkPdu::decode(&mut decoder)
1369            .expect("clamped generic GETBULK encode should decode as valid");
1370        assert_eq!(decoded.non_repeaters, 0);
1371        assert_eq!(decoded.max_repetitions, 0);
1372    }
1373
1374    #[test]
1375    fn test_directly_constructed_getbulk_pdu_encode_clamps_negative_fields() {
1376        // The pub fields let a caller build a GETBULK Pdu directly, bypassing
1377        // Pdu::get_bulk's constructor-side clamp. Pdu::encode must still clamp the
1378        // overloaded non-repeaters/max-repetitions (error_status/error_index) to 0
1379        // so a directly-constructed PDU cannot emit a negative on the wire.
1380        let pdu = Pdu {
1381            pdu_type: PduType::GetBulkRequest,
1382            request_id: 1,
1383            error_status: -1,
1384            error_index: -5,
1385            varbinds: vec![VarBind::null(oid!(1, 3, 6, 1))],
1386        };
1387
1388        let mut buf = EncodeBuf::new();
1389        pdu.encode(&mut buf);
1390        let bytes = buf.finish();
1391
1392        // Read the raw wire integers directly rather than via GetBulkPdu::decode,
1393        // which would re-clamp on the decode side (F06) and mask a broken encoder.
1394        let mut decoder = Decoder::new(bytes);
1395        let tag = decoder.read_tag().expect("read pdu tag");
1396        assert_eq!(tag, PduType::GetBulkRequest.tag());
1397        let len = decoder.read_length().expect("read pdu length");
1398        let mut inner = decoder.sub_decoder(len).expect("pdu sub-decoder");
1399        let _request_id = inner.read_integer().expect("read request_id");
1400        let non_repeaters = inner.read_integer().expect("read non_repeaters");
1401        let max_repetitions = inner.read_integer().expect("read max_repetitions");
1402        assert_eq!(non_repeaters, 0);
1403        assert_eq!(max_repetitions, 0);
1404    }
1405
1406    #[test]
1407    fn test_pdu_decode_getbulk_with_large_max_repetitions() {
1408        // GETBULK PDU with max_repetitions (25) > varbinds.len() (1)
1409        // This is the normal case for GETBULK requests.
1410        // The generic Pdu::decode must not reject this as an invalid error_index.
1411        let raw = RawGetBulkPdu::new(12345, 0, 25, vec![VarBind::null(oid!(1, 3, 6, 1, 2, 1, 1))]);
1412        let encoded = raw.encode();
1413
1414        let mut decoder = Decoder::new(encoded);
1415        let result = Pdu::decode(&mut decoder);
1416        assert!(
1417            result.is_ok(),
1418            "Pdu::decode should accept GETBULK with max_repetitions > varbinds.len(), got {:?}",
1419            result.err()
1420        );
1421
1422        let pdu = result.unwrap();
1423        assert_eq!(pdu.pdu_type, PduType::GetBulkRequest);
1424        assert_eq!(pdu.request_id, 12345);
1425        // For GETBULK: error_status = non_repeaters, error_index = max_repetitions
1426        assert_eq!(pdu.error_status, 0);
1427        assert_eq!(pdu.error_index, 25);
1428        assert_eq!(pdu.varbinds.len(), 1);
1429    }
1430
1431    #[test]
1432    fn test_getbulk_request_is_not_treated_as_error() {
1433        let pdu = Pdu::get_bulk(
1434            12345,
1435            2,
1436            10,
1437            vec![
1438                VarBind::null(oid!(1, 3, 6, 1, 2, 1, 1)),
1439                VarBind::null(oid!(1, 3, 6, 1, 2, 1, 2)),
1440            ],
1441        );
1442
1443        assert!(!pdu.is_error());
1444    }
1445
1446    #[test]
1447    fn test_response_with_error_status_is_treated_as_error() {
1448        let pdu = Pdu {
1449            pdu_type: PduType::Response,
1450            request_id: 12345,
1451            error_status: ErrorStatus::TooBig.as_i32(),
1452            error_index: 1,
1453            varbinds: vec![VarBind::null(oid!(1, 3, 6, 1, 2, 1, 1))],
1454        };
1455
1456        assert!(pdu.is_error());
1457    }
1458
1459    #[test]
1460    fn pdu_type_hash() {
1461        use std::collections::HashSet;
1462        let mut set = HashSet::new();
1463        set.insert(PduType::GetRequest);
1464        set.insert(PduType::GetNextRequest);
1465        assert_eq!(set.len(), 2);
1466        assert!(set.contains(&PduType::GetRequest));
1467    }
1468
1469    // =========================================================================
1470    // V1 <-> V2 PDU conversion tests
1471    // =========================================================================
1472
1473    #[test]
1474    fn test_v1_to_v2_generic_trap() {
1475        use crate::value::Value;
1476        use crate::varbind::VarBind;
1477
1478        let trap = TrapV1Pdu::new(
1479            oid!(1, 3, 6, 1, 4, 1, 9999),
1480            [192, 168, 1, 1],
1481            GenericTrap::LinkDown,
1482            0,
1483            12345,
1484            vec![VarBind::new(
1485                oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1),
1486                Value::Integer(1),
1487            )],
1488        );
1489
1490        let pdu = trap.to_v2_pdu().unwrap();
1491
1492        assert_eq!(pdu.pdu_type, PduType::TrapV2);
1493        assert_eq!(pdu.request_id, 0);
1494        // sysUpTime.0 + snmpTrapOID.0 + 1 original varbind (no proxy varbinds)
1495        assert_eq!(pdu.varbinds.len(), 3);
1496
1497        // First varbind: sysUpTime.0
1498        assert_eq!(pdu.varbinds[0].oid, oid!(1, 3, 6, 1, 2, 1, 1, 3, 0));
1499        assert_eq!(pdu.varbinds[0].value, Value::TimeTicks(12345));
1500
1501        // Second varbind: snmpTrapOID.0 = snmpTraps.3 (linkDown)
1502        assert_eq!(pdu.varbinds[1].oid, oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0));
1503        assert_eq!(
1504            pdu.varbinds[1].value,
1505            Value::ObjectIdentifier(oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3))
1506        );
1507
1508        // Third: original varbind
1509        assert_eq!(pdu.varbinds[2].oid, oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1));
1510    }
1511
1512    #[test]
1513    fn test_v1_to_v2_no_proxy_varbinds() {
1514        let trap = TrapV1Pdu::new(
1515            oid!(1, 3, 6, 1, 4, 1, 9999),
1516            [192, 168, 1, 1],
1517            GenericTrap::ColdStart,
1518            0,
1519            100,
1520            vec![],
1521        );
1522
1523        let pdu = trap.to_v2_pdu().unwrap();
1524        // Only sysUpTime.0 and snmpTrapOID.0 - no proxy varbinds even with
1525        // non-zero agent_addr (RFC 3584 Section 3.1(4))
1526        assert_eq!(pdu.varbinds.len(), 2);
1527    }
1528
1529    #[test]
1530    fn test_v1_to_v2_enterprise_specific() {
1531        use crate::value::Value;
1532
1533        let trap = TrapV1Pdu::new(
1534            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
1535            [10, 0, 0, 1],
1536            GenericTrap::EnterpriseSpecific,
1537            42,
1538            5000,
1539            vec![],
1540        );
1541
1542        let pdu = trap.to_v2_pdu().unwrap();
1543
1544        // snmpTrapOID.0 should be enterprise.0.42
1545        assert_eq!(
1546            pdu.varbinds[1].value,
1547            Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42))
1548        );
1549    }
1550
1551    #[test]
1552    fn test_v2_to_v1_standard_trap() {
1553        use crate::value::Value;
1554        use crate::varbind::VarBind;
1555
1556        let pdu = Pdu::trap_v2(
1557            1,
1558            5000,
1559            &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3), // linkDown
1560            vec![VarBind::new(
1561                oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1),
1562                Value::Integer(1),
1563            )],
1564        );
1565
1566        let trap = pdu.to_v1_trap([10, 0, 0, 1]).unwrap();
1567
1568        assert_eq!(trap.generic_trap, GenericTrap::LinkDown);
1569        assert_eq!(trap.specific_trap, 0);
1570        assert_eq!(trap.time_stamp, 5000);
1571        assert_eq!(trap.agent_addr, [10, 0, 0, 1]);
1572        // Enterprise defaults to snmpTraps when no snmpTrapEnterprise.0 varbind
1573        assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5));
1574        assert_eq!(trap.varbinds.len(), 1);
1575    }
1576
1577    #[test]
1578    fn test_v2_to_v1_enterprise_specific_trap() {
1579        let pdu = Pdu::trap_v2(1, 100, &oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42), vec![]);
1580
1581        let trap = pdu.to_v1_trap([0, 0, 0, 0]).unwrap();
1582
1583        assert_eq!(trap.generic_trap, GenericTrap::EnterpriseSpecific);
1584        assert_eq!(trap.specific_trap, 42);
1585        assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2));
1586        assert_eq!(trap.time_stamp, 100);
1587    }
1588
1589    #[test]
1590    fn test_v2_to_v1_enterprise_specific_nonzero_penultimate() {
1591        // RFC 3584 Section 3.2: when next-to-last sub-id is non-zero,
1592        // enterprise is snmpTrapOID with only the last sub-id removed.
1593        let pdu = Pdu::trap_v2(1, 200, &oid!(1, 3, 6, 1, 4, 1, 9999, 1, 42), vec![]);
1594
1595        let trap = pdu.to_v1_trap([0, 0, 0, 0]).unwrap();
1596
1597        assert_eq!(trap.generic_trap, GenericTrap::EnterpriseSpecific);
1598        assert_eq!(trap.specific_trap, 42);
1599        // Next-to-last arc is 1 (non-zero), so only last arc stripped
1600        assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999, 1));
1601        assert_eq!(trap.time_stamp, 200);
1602    }
1603
1604    #[test]
1605    fn test_v2_to_v1_snmp_traps_arc_out_of_range() {
1606        // RFC 3584 Section 3.2 rules (1), (3), (4): snmpTraps.x with x=0 or
1607        // x>6 is not a standard trap, so enterprise = OID minus last arc
1608        // (next-to-last arc 5 is non-zero), generic = 6, specific = last arc.
1609        for arc in [0u32, 7, 9] {
1610            let pdu = Pdu::trap_v2(1, 100, &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, arc), vec![]);
1611
1612            let trap = pdu.to_v1_trap([0, 0, 0, 0]).unwrap();
1613
1614            assert_eq!(trap.generic_trap, GenericTrap::EnterpriseSpecific);
1615            assert_eq!(trap.specific_trap, i32::try_from(arc).unwrap());
1616            assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5));
1617        }
1618    }
1619
1620    #[test]
1621    fn test_v2_to_v1_extracts_trap_address() {
1622        use crate::notification::oids;
1623        use crate::value::Value;
1624        use crate::varbind::VarBind;
1625
1626        let pdu = Pdu::trap_v2(
1627            1,
1628            0,
1629            &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1), // coldStart
1630            vec![VarBind::new(
1631                oids::snmp_trap_address(),
1632                Value::IpAddress([192, 168, 1, 1]),
1633            )],
1634        );
1635
1636        let trap = pdu.to_v1_trap([0, 0, 0, 0]).unwrap();
1637        assert_eq!(trap.agent_addr, [192, 168, 1, 1]);
1638        // RFC 3584 Section 3.2 rule (6): only sysUpTime.0 and snmpTrapOID.0
1639        // are excluded; snmpTrapAddress.0 is retained in the varbinds
1640        assert_eq!(trap.varbinds.len(), 1);
1641        assert_eq!(trap.varbinds[0].oid, oids::snmp_trap_address());
1642    }
1643
1644    #[test]
1645    fn test_v2_to_v1_extracts_trap_enterprise() {
1646        use crate::notification::oids;
1647        use crate::value::Value;
1648        use crate::varbind::VarBind;
1649
1650        let pdu = Pdu::trap_v2(
1651            1,
1652            0,
1653            &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1), // coldStart
1654            vec![VarBind::new(
1655                oids::snmp_trap_enterprise(),
1656                Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 9999)),
1657            )],
1658        );
1659
1660        let trap = pdu.to_v1_trap([0, 0, 0, 0]).unwrap();
1661        // Standard trap should use the enterprise from snmpTrapEnterprise.0
1662        assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999));
1663        // RFC 3584 Section 3.2 rule (6): only sysUpTime.0 and snmpTrapOID.0
1664        // are excluded; snmpTrapEnterprise.0 is retained in the varbinds
1665        assert_eq!(trap.varbinds.len(), 1);
1666        assert_eq!(trap.varbinds[0].oid, oids::snmp_trap_enterprise());
1667    }
1668
1669    #[test]
1670    fn test_v2_to_v1_counter64_dropped() {
1671        use crate::value::Value;
1672        use crate::varbind::VarBind;
1673
1674        let pdu = Pdu::trap_v2(
1675            1,
1676            0,
1677            &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1),
1678            vec![VarBind::new(
1679                oid!(1, 3, 6, 1, 2, 1, 1, 1, 0),
1680                Value::Counter64(12345),
1681            )],
1682        );
1683
1684        // Counter64 in any varbind means the trap cannot be represented in V1
1685        assert!(pdu.to_v1_trap([0, 0, 0, 0]).is_none());
1686    }
1687
1688    #[test]
1689    fn test_v2_to_v1_too_few_varbinds() {
1690        let pdu = Pdu {
1691            pdu_type: PduType::TrapV2,
1692            request_id: 1,
1693            error_status: 0,
1694            error_index: 0,
1695            varbinds: vec![],
1696        };
1697
1698        assert!(pdu.to_v1_trap([0, 0, 0, 0]).is_none());
1699    }
1700
1701    #[test]
1702    fn test_v1_v2_roundtrip_enterprise_specific() {
1703        use crate::value::Value;
1704        use crate::varbind::VarBind;
1705
1706        // Enterprise-specific traps preserve enterprise and specific_trap
1707        // through the OID encoding (enterprise.0.specific_trap), so these
1708        // fields survive a non-proxy v1->v2->v1 roundtrip. agent_addr is
1709        // lost (comes from default_addr on the v1 side).
1710        let original = TrapV1Pdu::new(
1711            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
1712            [192, 168, 1, 1],
1713            GenericTrap::EnterpriseSpecific,
1714            42,
1715            12345,
1716            vec![VarBind::new(
1717                oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1),
1718                Value::Integer(1),
1719            )],
1720        );
1721
1722        let v2 = original.to_v2_pdu().unwrap();
1723        let restored = v2.to_v1_trap([0, 0, 0, 0]).unwrap();
1724
1725        assert_eq!(restored.enterprise, original.enterprise);
1726        assert_eq!(restored.generic_trap, original.generic_trap);
1727        assert_eq!(restored.specific_trap, original.specific_trap);
1728        assert_eq!(restored.time_stamp, original.time_stamp);
1729        assert_eq!(restored.varbinds.len(), original.varbinds.len());
1730        assert_eq!(restored.varbinds[0].oid, original.varbinds[0].oid);
1731        // agent_addr not preserved without proxy varbinds
1732        assert_eq!(restored.agent_addr, [0, 0, 0, 0]);
1733    }
1734
1735    #[test]
1736    fn test_v1_v2_roundtrip_standard_trap() {
1737        // Standard trap roundtrip preserves generic_trap and time_stamp.
1738        // Without proxy varbinds, enterprise falls back to snmpTraps and
1739        // agent_addr comes from default_addr.
1740        let original = TrapV1Pdu::new(
1741            oid!(1, 3, 6, 1, 4, 1, 9999),
1742            [10, 0, 0, 1],
1743            GenericTrap::WarmStart,
1744            0,
1745            500,
1746            vec![],
1747        );
1748
1749        let v2 = original.to_v2_pdu().unwrap();
1750        let restored = v2.to_v1_trap([10, 0, 0, 1]).unwrap();
1751
1752        assert_eq!(restored.generic_trap, GenericTrap::WarmStart);
1753        assert_eq!(restored.specific_trap, 0);
1754        assert_eq!(restored.time_stamp, 500);
1755        assert_eq!(restored.agent_addr, [10, 0, 0, 1]); // from default_addr
1756        // Enterprise falls back to snmpTraps without snmpTrapEnterprise.0
1757        assert_eq!(restored.enterprise, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5));
1758    }
1759
1760    #[test]
1761    fn test_v2_to_v1_all_generic_traps() {
1762        // Verify all 6 standard traps roundtrip correctly
1763        let traps = [
1764            (oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1), GenericTrap::ColdStart),
1765            (oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2), GenericTrap::WarmStart),
1766            (oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3), GenericTrap::LinkDown),
1767            (oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4), GenericTrap::LinkUp),
1768            (
1769                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5),
1770                GenericTrap::AuthenticationFailure,
1771            ),
1772            (
1773                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6),
1774                GenericTrap::EgpNeighborLoss,
1775            ),
1776        ];
1777
1778        for (trap_oid, expected_generic) in traps {
1779            let pdu = Pdu::trap_v2(1, 100, &trap_oid, vec![]);
1780            let v1 = pdu.to_v1_trap([0, 0, 0, 0]).unwrap();
1781            assert_eq!(v1.generic_trap, expected_generic, "Failed for {trap_oid:?}");
1782            assert_eq!(v1.specific_trap, 0);
1783        }
1784    }
1785}