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