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