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)]
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    pub fn from_tag(tag: u8) -> Option<Self> {
38        match tag {
39            0xA0 => Some(Self::GetRequest),
40            0xA1 => Some(Self::GetNextRequest),
41            0xA2 => Some(Self::Response),
42            0xA3 => Some(Self::SetRequest),
43            0xA4 => Some(Self::TrapV1),
44            0xA5 => Some(Self::GetBulkRequest),
45            0xA6 => Some(Self::InformRequest),
46            0xA7 => Some(Self::TrapV2),
47            0xA8 => Some(Self::Report),
48            _ => None,
49        }
50    }
51
52    /// Get the tag byte.
53    pub fn tag(self) -> u8 {
54        self as u8
55    }
56}
57
58impl std::fmt::Display for PduType {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self {
61            Self::GetRequest => write!(f, "GetRequest"),
62            Self::GetNextRequest => write!(f, "GetNextRequest"),
63            Self::Response => write!(f, "Response"),
64            Self::SetRequest => write!(f, "SetRequest"),
65            Self::TrapV1 => write!(f, "TrapV1"),
66            Self::GetBulkRequest => write!(f, "GetBulkRequest"),
67            Self::InformRequest => write!(f, "InformRequest"),
68            Self::TrapV2 => write!(f, "TrapV2"),
69            Self::Report => write!(f, "Report"),
70        }
71    }
72}
73
74/// Generic PDU structure for request/response operations.
75#[derive(Debug, Clone)]
76pub struct Pdu {
77    /// PDU type
78    pub pdu_type: PduType,
79    /// Request ID for correlating requests and responses
80    pub request_id: i32,
81    /// Error status (0 for requests, error code for responses)
82    pub error_status: i32,
83    /// Error index (1-based index of problematic varbind)
84    pub error_index: i32,
85    /// Variable bindings
86    pub varbinds: Vec<VarBind>,
87}
88
89impl Pdu {
90    /// Create a new GET request PDU.
91    pub fn get_request(request_id: i32, oids: &[Oid]) -> Self {
92        Self {
93            pdu_type: PduType::GetRequest,
94            request_id,
95            error_status: 0,
96            error_index: 0,
97            varbinds: oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
98        }
99    }
100
101    /// Create a new GETNEXT request PDU.
102    pub fn get_next_request(request_id: i32, oids: &[Oid]) -> Self {
103        Self {
104            pdu_type: PduType::GetNextRequest,
105            request_id,
106            error_status: 0,
107            error_index: 0,
108            varbinds: oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
109        }
110    }
111
112    /// Create a new SET request PDU.
113    pub fn set_request(request_id: i32, varbinds: Vec<VarBind>) -> Self {
114        Self {
115            pdu_type: PduType::SetRequest,
116            request_id,
117            error_status: 0,
118            error_index: 0,
119            varbinds,
120        }
121    }
122
123    /// Create a GETBULK request PDU.
124    ///
125    /// Note: For GETBULK, error_status holds non_repeaters and error_index holds max_repetitions.
126    pub fn get_bulk(
127        request_id: i32,
128        non_repeaters: i32,
129        max_repetitions: i32,
130        varbinds: Vec<VarBind>,
131    ) -> Self {
132        Self {
133            pdu_type: PduType::GetBulkRequest,
134            request_id,
135            error_status: non_repeaters,
136            error_index: max_repetitions,
137            varbinds,
138        }
139    }
140
141    /// Encode to BER.
142    pub fn encode(&self, buf: &mut EncodeBuf) {
143        buf.push_constructed(self.pdu_type.tag(), |buf| {
144            encode_varbind_list(buf, &self.varbinds);
145            buf.push_integer(self.error_index);
146            buf.push_integer(self.error_status);
147            buf.push_integer(self.request_id);
148        });
149    }
150
151    /// Decode from BER (after tag has been peeked).
152    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
153        let tag = decoder.read_tag()?;
154        let pdu_type = PduType::from_tag(tag).ok_or_else(|| {
155            tracing::debug!(target: "async_snmp::pdu", { offset = decoder.offset(), tag = tag, kind = %DecodeErrorKind::UnknownPduType(tag) }, "decode error");
156            Error::MalformedResponse {
157                target: UNKNOWN_TARGET,
158            }
159            .boxed()
160        })?;
161
162        let len = decoder.read_length()?;
163        let mut pdu_decoder = decoder.sub_decoder(len)?;
164
165        let request_id = pdu_decoder.read_integer()?;
166        let error_status = pdu_decoder.read_integer()?;
167        let error_index = pdu_decoder.read_integer()?;
168        let varbinds = decode_varbind_list(&mut pdu_decoder)?;
169
170        // Validate error_index bounds per RFC 3416 Section 3.
171        // error_index is 1-based: 0 means no error, 1..=len points to specific varbind.
172        // Note: For GETBULK, error_status holds non_repeaters and error_index holds
173        // max_repetitions, so these validations don't apply.
174        if pdu_type != PduType::GetBulkRequest {
175            if error_index < 0 {
176                tracing::debug!(target: "async_snmp::pdu", { offset = pdu_decoder.offset(), error_index = error_index, kind = %DecodeErrorKind::NegativeErrorIndex { value: error_index } }, "decode error");
177                return Err(Error::MalformedResponse {
178                    target: UNKNOWN_TARGET,
179                }
180                .boxed());
181            }
182            if error_index > 0 && (error_index as usize) > varbinds.len() {
183                tracing::debug!(target: "async_snmp::pdu", { offset = pdu_decoder.offset(), error_index = error_index, varbind_count = varbinds.len(), kind = %DecodeErrorKind::ErrorIndexOutOfBounds {
184                        index: error_index,
185                        varbind_count: varbinds.len(),
186                    } }, "decode error");
187                return Err(Error::MalformedResponse {
188                    target: UNKNOWN_TARGET,
189                }
190                .boxed());
191            }
192        }
193
194        Ok(Pdu {
195            pdu_type,
196            request_id,
197            error_status,
198            error_index,
199            varbinds,
200        })
201    }
202
203    /// Check if this is an error response.
204    pub fn is_error(&self) -> bool {
205        self.error_status != 0
206    }
207
208    /// Get the error status as an enum.
209    pub fn error_status_enum(&self) -> ErrorStatus {
210        ErrorStatus::from_i32(self.error_status)
211    }
212
213    /// Create a Response PDU from this PDU (for Inform handling).
214    ///
215    /// The response copies the request_id and variable bindings,
216    /// sets error_status and error_index to 0, and changes the PDU type to Response.
217    pub fn to_response(&self) -> Self {
218        Self {
219            pdu_type: PduType::Response,
220            request_id: self.request_id,
221            error_status: 0,
222            error_index: 0,
223            varbinds: self.varbinds.clone(),
224        }
225    }
226
227    /// Create a Response PDU with specific error status.
228    pub fn to_error_response(&self, error_status: ErrorStatus, error_index: i32) -> Self {
229        Self {
230            pdu_type: PduType::Response,
231            request_id: self.request_id,
232            error_status: error_status.as_i32(),
233            error_index,
234            varbinds: self.varbinds.clone(),
235        }
236    }
237
238    /// Check if this is a notification PDU (Trap or Inform).
239    pub fn is_notification(&self) -> bool {
240        matches!(
241            self.pdu_type,
242            PduType::TrapV1 | PduType::TrapV2 | PduType::InformRequest
243        )
244    }
245
246    /// Check if this is a confirmed-class PDU (requires response).
247    pub fn is_confirmed(&self) -> bool {
248        matches!(
249            self.pdu_type,
250            PduType::GetRequest
251                | PduType::GetNextRequest
252                | PduType::GetBulkRequest
253                | PduType::SetRequest
254                | PduType::InformRequest
255        )
256    }
257}
258
259/// SNMPv1 generic trap types (RFC 1157 Section 4.1.6).
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261#[repr(i32)]
262pub enum GenericTrap {
263    /// coldStart(0) - agent is reinitializing, config may change
264    ColdStart = 0,
265    /// warmStart(1) - agent is reinitializing, config unchanged
266    WarmStart = 1,
267    /// linkDown(2) - communication link failure
268    LinkDown = 2,
269    /// linkUp(3) - communication link came up
270    LinkUp = 3,
271    /// authenticationFailure(4) - improperly authenticated message received
272    AuthenticationFailure = 4,
273    /// egpNeighborLoss(5) - EGP peer marked down
274    EgpNeighborLoss = 5,
275    /// enterpriseSpecific(6) - vendor-specific trap, see specific_trap field
276    EnterpriseSpecific = 6,
277}
278
279impl GenericTrap {
280    /// Create from integer value.
281    pub fn from_i32(v: i32) -> Option<Self> {
282        match v {
283            0 => Some(Self::ColdStart),
284            1 => Some(Self::WarmStart),
285            2 => Some(Self::LinkDown),
286            3 => Some(Self::LinkUp),
287            4 => Some(Self::AuthenticationFailure),
288            5 => Some(Self::EgpNeighborLoss),
289            6 => Some(Self::EnterpriseSpecific),
290            _ => None,
291        }
292    }
293
294    /// Get the integer value.
295    pub fn as_i32(self) -> i32 {
296        self as i32
297    }
298}
299
300/// SNMPv1 Trap PDU (RFC 1157 Section 4.1.6).
301///
302/// This PDU type has a completely different structure from other PDUs.
303/// It is only used in SNMPv1 and is replaced by SNMPv2-Trap in v2c/v3.
304#[derive(Debug, Clone)]
305pub struct TrapV1Pdu {
306    /// Enterprise OID (sysObjectID of the entity generating the trap)
307    pub enterprise: Oid,
308    /// Agent address (IP address of the agent generating the trap)
309    pub agent_addr: [u8; 4],
310    /// Generic trap type
311    pub generic_trap: i32,
312    /// Specific trap code (meaningful when generic_trap is enterpriseSpecific)
313    pub specific_trap: i32,
314    /// Time since the network entity was last (re)initialized (in hundredths of seconds)
315    pub time_stamp: u32,
316    /// Variable bindings containing "interesting" information
317    pub varbinds: Vec<VarBind>,
318}
319
320impl TrapV1Pdu {
321    /// Create a new SNMPv1 Trap PDU.
322    pub fn new(
323        enterprise: Oid,
324        agent_addr: [u8; 4],
325        generic_trap: GenericTrap,
326        specific_trap: i32,
327        time_stamp: u32,
328        varbinds: Vec<VarBind>,
329    ) -> Self {
330        Self {
331            enterprise,
332            agent_addr,
333            generic_trap: generic_trap.as_i32(),
334            specific_trap,
335            time_stamp,
336            varbinds,
337        }
338    }
339
340    /// Get the generic trap type as an enum.
341    pub fn generic_trap_enum(&self) -> Option<GenericTrap> {
342        GenericTrap::from_i32(self.generic_trap)
343    }
344
345    /// Check if this is an enterprise-specific trap.
346    pub fn is_enterprise_specific(&self) -> bool {
347        self.generic_trap == GenericTrap::EnterpriseSpecific as i32
348    }
349
350    /// Convert to SNMPv2 trap OID (RFC 3584 Section 3).
351    ///
352    /// RFC 3584 defines how to translate SNMPv1 trap information to SNMPv2
353    /// snmpTrapOID.0 format:
354    ///
355    /// - For generic traps 0-5 (coldStart through egpNeighborLoss):
356    ///   The trap OID is `snmpTraps.{generic_trap + 1}` (1.3.6.1.6.3.1.1.5.{1-6})
357    ///
358    /// - For enterprise-specific traps (generic_trap = 6):
359    ///   The trap OID is `enterprise.0.specific_trap`
360    ///
361    /// # Errors
362    ///
363    /// Returns [`Error::InvalidOid`] if:
364    /// - `generic_trap < 0` (undefined per RFC 1157)
365    /// - `generic_trap == i32::MAX` (would overflow when adding 1)
366    /// - `specific_trap < 0` for enterprise-specific traps (OID arcs must be non-negative)
367    ///
368    /// # Example
369    ///
370    /// ```rust
371    /// use async_snmp::pdu::{TrapV1Pdu, GenericTrap};
372    /// use async_snmp::oid;
373    ///
374    /// // Generic trap (linkDown = 2) -> snmpTraps.3
375    /// let trap = TrapV1Pdu::new(
376    ///     oid!(1, 3, 6, 1, 4, 1, 9999),
377    ///     [192, 168, 1, 1],
378    ///     GenericTrap::LinkDown,
379    ///     0,
380    ///     12345,
381    ///     vec![],
382    /// );
383    /// assert_eq!(trap.v2_trap_oid().unwrap(), oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3));
384    ///
385    /// // Enterprise-specific trap -> enterprise.0.specific_trap
386    /// let trap = TrapV1Pdu::new(
387    ///     oid!(1, 3, 6, 1, 4, 1, 9999),
388    ///     [192, 168, 1, 1],
389    ///     GenericTrap::EnterpriseSpecific,
390    ///     42,
391    ///     12345,
392    ///     vec![],
393    /// );
394    /// assert_eq!(trap.v2_trap_oid().unwrap(), oid!(1, 3, 6, 1, 4, 1, 9999, 0, 42));
395    /// ```
396    pub fn v2_trap_oid(&self) -> crate::Result<Oid> {
397        if self.is_enterprise_specific() {
398            if self.specific_trap < 0 {
399                return Err(Error::InvalidOid("specific_trap cannot be negative".into()).boxed());
400            }
401            let mut arcs: Vec<u32> = self.enterprise.arcs().to_vec();
402            arcs.push(0);
403            arcs.push(self.specific_trap as u32);
404            Ok(Oid::new(arcs))
405        } else {
406            if self.generic_trap < 0 {
407                return Err(Error::InvalidOid("generic_trap cannot be negative".into()).boxed());
408            }
409            if self.generic_trap == i32::MAX {
410                return Err(Error::InvalidOid("generic_trap overflow".into()).boxed());
411            }
412            let trap_num = self.generic_trap + 1;
413            Ok(crate::oid!(1, 3, 6, 1, 6, 3, 1, 1, 5).child(trap_num as u32))
414        }
415    }
416
417    /// Encode to BER.
418    pub fn encode(&self, buf: &mut EncodeBuf) {
419        buf.push_constructed(tag::pdu::TRAP_V1, |buf| {
420            encode_varbind_list(buf, &self.varbinds);
421            buf.push_unsigned32(tag::application::TIMETICKS, self.time_stamp);
422            buf.push_integer(self.specific_trap);
423            buf.push_integer(self.generic_trap);
424            // NetworkAddress is APPLICATION 0 IMPLICIT IpAddress
425            // IpAddress is APPLICATION 0 IMPLICIT OCTET STRING (SIZE (4))
426            buf.push_bytes(&self.agent_addr);
427            buf.push_length(4);
428            buf.push_tag(tag::application::IP_ADDRESS);
429            buf.push_oid(&self.enterprise);
430        });
431    }
432
433    /// Decode from BER (after tag has been peeked).
434    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
435        let mut pdu = decoder.read_constructed(tag::pdu::TRAP_V1)?;
436
437        // enterprise OBJECT IDENTIFIER
438        let enterprise = pdu.read_oid()?;
439
440        // agent-addr NetworkAddress (IpAddress)
441        let agent_tag = pdu.read_tag()?;
442        if agent_tag != tag::application::IP_ADDRESS {
443            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), expected = 0x40_u8, actual = agent_tag, kind = %DecodeErrorKind::UnexpectedTag {
444                    expected: 0x40,
445                    actual: agent_tag,
446                } }, "decode error");
447            return Err(Error::MalformedResponse {
448                target: UNKNOWN_TARGET,
449            }
450            .boxed());
451        }
452        let agent_len = pdu.read_length()?;
453        if agent_len != 4 {
454            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), length = agent_len, kind = %DecodeErrorKind::InvalidIpAddressLength { length: agent_len } }, "decode error");
455            return Err(Error::MalformedResponse {
456                target: UNKNOWN_TARGET,
457            }
458            .boxed());
459        }
460        let agent_bytes = pdu.read_bytes(4)?;
461        let agent_addr = [
462            agent_bytes[0],
463            agent_bytes[1],
464            agent_bytes[2],
465            agent_bytes[3],
466        ];
467
468        // generic-trap INTEGER
469        let generic_trap = pdu.read_integer()?;
470
471        // specific-trap INTEGER
472        let specific_trap = pdu.read_integer()?;
473
474        // time-stamp TimeTicks
475        let ts_tag = pdu.read_tag()?;
476        if ts_tag != tag::application::TIMETICKS {
477            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), expected = 0x43_u8, actual = ts_tag, kind = %DecodeErrorKind::UnexpectedTag {
478                    expected: 0x43,
479                    actual: ts_tag,
480                } }, "decode error");
481            return Err(Error::MalformedResponse {
482                target: UNKNOWN_TARGET,
483            }
484            .boxed());
485        }
486        let ts_len = pdu.read_length()?;
487        let time_stamp = pdu.read_unsigned32_value(ts_len)?;
488
489        // variable-bindings
490        let varbinds = decode_varbind_list(&mut pdu)?;
491
492        Ok(TrapV1Pdu {
493            enterprise,
494            agent_addr,
495            generic_trap,
496            specific_trap,
497            time_stamp,
498            varbinds,
499        })
500    }
501}
502
503/// GETBULK request PDU.
504#[derive(Debug, Clone)]
505pub struct GetBulkPdu {
506    /// Request ID
507    pub request_id: i32,
508    /// Number of non-repeating OIDs
509    pub non_repeaters: i32,
510    /// Maximum repetitions for repeating OIDs
511    pub max_repetitions: i32,
512    /// Variable bindings
513    pub varbinds: Vec<VarBind>,
514}
515
516impl GetBulkPdu {
517    /// Create a new GETBULK request.
518    pub fn new(request_id: i32, non_repeaters: i32, max_repetitions: i32, oids: &[Oid]) -> Self {
519        Self {
520            request_id,
521            non_repeaters,
522            max_repetitions,
523            varbinds: oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
524        }
525    }
526
527    /// Encode to BER.
528    pub fn encode(&self, buf: &mut EncodeBuf) {
529        buf.push_constructed(tag::pdu::GET_BULK_REQUEST, |buf| {
530            encode_varbind_list(buf, &self.varbinds);
531            buf.push_integer(self.max_repetitions);
532            buf.push_integer(self.non_repeaters);
533            buf.push_integer(self.request_id);
534        });
535    }
536
537    /// Decode from BER.
538    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
539        let mut pdu = decoder.read_constructed(tag::pdu::GET_BULK_REQUEST)?;
540
541        let request_id = pdu.read_integer()?;
542        let non_repeaters = pdu.read_integer()?;
543        let max_repetitions = pdu.read_integer()?;
544        let varbinds = decode_varbind_list(&mut pdu)?;
545
546        // Validate non_repeaters and max_repetitions per RFC 3416 Section 4.2.3.
547        if non_repeaters < 0 {
548            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), non_repeaters = non_repeaters, kind = %DecodeErrorKind::NegativeNonRepeaters {
549                    value: non_repeaters,
550                } }, "decode error");
551            return Err(Error::MalformedResponse {
552                target: UNKNOWN_TARGET,
553            }
554            .boxed());
555        }
556        if max_repetitions < 0 {
557            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), max_repetitions = max_repetitions, kind = %DecodeErrorKind::NegativeMaxRepetitions {
558                    value: max_repetitions,
559                } }, "decode error");
560            return Err(Error::MalformedResponse {
561                target: UNKNOWN_TARGET,
562            }
563            .boxed());
564        }
565
566        Ok(GetBulkPdu {
567            request_id,
568            non_repeaters,
569            max_repetitions,
570            varbinds,
571        })
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use crate::oid;
579
580    /// Test helper for encoding PDUs with arbitrary field values.
581    ///
582    /// Unlike `Pdu`, this allows encoding invalid values (negative error_index,
583    /// out-of-bounds indices, etc.) for testing decoder validation.
584    struct RawPdu {
585        pdu_type: u8,
586        request_id: i32,
587        error_status: i32,
588        error_index: i32,
589        varbinds: Vec<VarBind>,
590    }
591
592    impl RawPdu {
593        fn response(
594            request_id: i32,
595            error_status: i32,
596            error_index: i32,
597            varbinds: Vec<VarBind>,
598        ) -> Self {
599            Self {
600                pdu_type: PduType::Response.tag(),
601                request_id,
602                error_status,
603                error_index,
604                varbinds,
605            }
606        }
607
608        fn encode(&self) -> bytes::Bytes {
609            let mut buf = EncodeBuf::new();
610            buf.push_constructed(self.pdu_type, |buf| {
611                encode_varbind_list(buf, &self.varbinds);
612                buf.push_integer(self.error_index);
613                buf.push_integer(self.error_status);
614                buf.push_integer(self.request_id);
615            });
616            buf.finish()
617        }
618    }
619
620    /// Test helper for encoding GETBULK PDUs with arbitrary field values.
621    struct RawGetBulkPdu {
622        request_id: i32,
623        non_repeaters: i32,
624        max_repetitions: i32,
625        varbinds: Vec<VarBind>,
626    }
627
628    impl RawGetBulkPdu {
629        fn new(
630            request_id: i32,
631            non_repeaters: i32,
632            max_repetitions: i32,
633            varbinds: Vec<VarBind>,
634        ) -> Self {
635            Self {
636                request_id,
637                non_repeaters,
638                max_repetitions,
639                varbinds,
640            }
641        }
642
643        fn encode(&self) -> bytes::Bytes {
644            let mut buf = EncodeBuf::new();
645            buf.push_constructed(tag::pdu::GET_BULK_REQUEST, |buf| {
646                encode_varbind_list(buf, &self.varbinds);
647                buf.push_integer(self.max_repetitions);
648                buf.push_integer(self.non_repeaters);
649                buf.push_integer(self.request_id);
650            });
651            buf.finish()
652        }
653    }
654
655    #[test]
656    fn test_get_request_roundtrip() {
657        let pdu = Pdu::get_request(12345, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
658
659        let mut buf = EncodeBuf::new();
660        pdu.encode(&mut buf);
661        let bytes = buf.finish();
662
663        let mut decoder = Decoder::new(bytes);
664        let decoded = Pdu::decode(&mut decoder).unwrap();
665
666        assert_eq!(decoded.pdu_type, PduType::GetRequest);
667        assert_eq!(decoded.request_id, 12345);
668        assert_eq!(decoded.varbinds.len(), 1);
669    }
670
671    #[test]
672    fn test_getbulk_roundtrip() {
673        let pdu = GetBulkPdu::new(12345, 0, 10, &[oid!(1, 3, 6, 1, 2, 1, 1)]);
674
675        let mut buf = EncodeBuf::new();
676        pdu.encode(&mut buf);
677        let bytes = buf.finish();
678
679        let mut decoder = Decoder::new(bytes);
680        let decoded = GetBulkPdu::decode(&mut decoder).unwrap();
681
682        assert_eq!(decoded.request_id, 12345);
683        assert_eq!(decoded.non_repeaters, 0);
684        assert_eq!(decoded.max_repetitions, 10);
685    }
686
687    #[test]
688    fn test_trap_v1_roundtrip() {
689        use crate::value::Value;
690        use crate::varbind::VarBind;
691
692        let trap = TrapV1Pdu::new(
693            oid!(1, 3, 6, 1, 4, 1, 9999), // enterprise OID
694            [192, 168, 1, 1],             // agent address
695            GenericTrap::LinkDown,
696            0,
697            12345678, // time stamp
698            vec![VarBind::new(
699                oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1),
700                Value::Integer(1),
701            )],
702        );
703
704        let mut buf = EncodeBuf::new();
705        trap.encode(&mut buf);
706        let bytes = buf.finish();
707
708        let mut decoder = Decoder::new(bytes);
709        let decoded = TrapV1Pdu::decode(&mut decoder).unwrap();
710
711        assert_eq!(decoded.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999));
712        assert_eq!(decoded.agent_addr, [192, 168, 1, 1]);
713        assert_eq!(decoded.generic_trap, GenericTrap::LinkDown as i32);
714        assert_eq!(decoded.specific_trap, 0);
715        assert_eq!(decoded.time_stamp, 12345678);
716        assert_eq!(decoded.varbinds.len(), 1);
717    }
718
719    #[test]
720    fn test_trap_v1_enterprise_specific() {
721        let trap = TrapV1Pdu::new(
722            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
723            [10, 0, 0, 1],
724            GenericTrap::EnterpriseSpecific,
725            42, // specific trap number
726            100,
727            vec![],
728        );
729
730        assert!(trap.is_enterprise_specific());
731        assert_eq!(
732            trap.generic_trap_enum(),
733            Some(GenericTrap::EnterpriseSpecific)
734        );
735
736        let mut buf = EncodeBuf::new();
737        trap.encode(&mut buf);
738        let bytes = buf.finish();
739
740        let mut decoder = Decoder::new(bytes);
741        let decoded = TrapV1Pdu::decode(&mut decoder).unwrap();
742
743        assert_eq!(decoded.specific_trap, 42);
744    }
745
746    #[test]
747    fn test_trap_v1_v2_trap_oid_generic_traps() {
748        // Test all generic trap types translate to correct snmpTraps.X OIDs
749        // RFC 3584 Section 3: snmpTraps.{generic_trap + 1}
750
751        let test_cases = [
752            (GenericTrap::ColdStart, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)),
753            (GenericTrap::WarmStart, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)),
754            (GenericTrap::LinkDown, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)),
755            (GenericTrap::LinkUp, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)),
756            (
757                GenericTrap::AuthenticationFailure,
758                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5),
759            ),
760            (
761                GenericTrap::EgpNeighborLoss,
762                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6),
763            ),
764        ];
765
766        for (generic_trap, expected_oid) in test_cases {
767            let trap = TrapV1Pdu::new(
768                oid!(1, 3, 6, 1, 4, 1, 9999),
769                [192, 168, 1, 1],
770                generic_trap,
771                0,
772                12345,
773                vec![],
774            );
775            assert_eq!(
776                trap.v2_trap_oid().unwrap(),
777                expected_oid,
778                "Failed for {:?}",
779                generic_trap
780            );
781        }
782    }
783
784    #[test]
785    fn test_trap_v1_v2_trap_oid_enterprise_specific() {
786        // RFC 3584 Section 3: enterprise.0.specific_trap
787        let trap = TrapV1Pdu::new(
788            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
789            [192, 168, 1, 1],
790            GenericTrap::EnterpriseSpecific,
791            42,
792            12345,
793            vec![],
794        );
795
796        // Expected: 1.3.6.1.4.1.9999.1.2.0.42
797        assert_eq!(
798            trap.v2_trap_oid().unwrap(),
799            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
800        );
801    }
802
803    #[test]
804    fn test_trap_v1_v2_trap_oid_enterprise_specific_zero() {
805        // Edge case: specific_trap = 0
806        let trap = TrapV1Pdu::new(
807            oid!(1, 3, 6, 1, 4, 1, 1234),
808            [10, 0, 0, 1],
809            GenericTrap::EnterpriseSpecific,
810            0,
811            100,
812            vec![],
813        );
814
815        // Expected: 1.3.6.1.4.1.1234.0.0
816        assert_eq!(
817            trap.v2_trap_oid().unwrap(),
818            oid!(1, 3, 6, 1, 4, 1, 1234, 0, 0)
819        );
820    }
821
822    #[test]
823    fn test_pdu_to_response() {
824        use crate::value::Value;
825        use crate::varbind::VarBind;
826
827        let inform = Pdu {
828            pdu_type: PduType::InformRequest,
829            request_id: 99999,
830            error_status: 0,
831            error_index: 0,
832            varbinds: vec![
833                VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), Value::TimeTicks(12345)),
834                VarBind::new(
835                    oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0),
836                    Value::ObjectIdentifier(oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)),
837                ),
838            ],
839        };
840
841        let response = inform.to_response();
842
843        assert_eq!(response.pdu_type, PduType::Response);
844        assert_eq!(response.request_id, 99999);
845        assert_eq!(response.error_status, 0);
846        assert_eq!(response.error_index, 0);
847        assert_eq!(response.varbinds.len(), 2);
848    }
849
850    #[test]
851    fn test_pdu_is_confirmed() {
852        let get = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
853        assert!(get.is_confirmed());
854
855        let inform = Pdu {
856            pdu_type: PduType::InformRequest,
857            request_id: 1,
858            error_status: 0,
859            error_index: 0,
860            varbinds: vec![],
861        };
862        assert!(inform.is_confirmed());
863
864        let trap = Pdu {
865            pdu_type: PduType::TrapV2,
866            request_id: 1,
867            error_status: 0,
868            error_index: 0,
869            varbinds: vec![],
870        };
871        assert!(!trap.is_confirmed());
872        assert!(trap.is_notification());
873    }
874
875    #[test]
876    fn test_decode_rejects_negative_error_index() {
877        // Response PDU with negative error_index (-1)
878        let raw = RawPdu::response(1, 0, -1, vec![VarBind::null(oid!(1, 3, 6, 1))]);
879        let encoded = raw.encode();
880
881        let mut decoder = Decoder::new(encoded);
882        let result = Pdu::decode(&mut decoder);
883
884        assert!(result.is_err(), "should reject negative error_index");
885        let err = result.unwrap_err();
886        assert!(
887            matches!(&*err, crate::error::Error::MalformedResponse { .. }),
888            "expected MalformedResponse, got {:?}",
889            err
890        );
891    }
892
893    #[test]
894    fn test_decode_rejects_error_index_beyond_varbinds() {
895        // Response PDU with error_index=5 but only 1 varbind
896        let raw = RawPdu::response(1, 5, 5, vec![VarBind::null(oid!(1, 3, 6, 1))]);
897        let encoded = raw.encode();
898
899        let mut decoder = Decoder::new(encoded);
900        let result = Pdu::decode(&mut decoder);
901
902        assert!(result.is_err(), "should reject error_index beyond varbinds");
903        let err = result.unwrap_err();
904        assert!(
905            matches!(&*err, crate::error::Error::MalformedResponse { .. }),
906            "expected MalformedResponse, got {:?}",
907            err
908        );
909    }
910
911    #[test]
912    fn test_decode_accepts_valid_error_index_zero() {
913        // error_index=0 with no error is valid
914        let raw = RawPdu::response(1, 0, 0, vec![VarBind::null(oid!(1, 3, 6, 1))]);
915        let encoded = raw.encode();
916
917        let mut decoder = Decoder::new(encoded);
918        let decoded = Pdu::decode(&mut decoder);
919        assert!(decoded.is_ok(), "error_index=0 should be valid");
920    }
921
922    #[test]
923    fn test_decode_accepts_error_index_within_bounds() {
924        // error_index=1 with 1 varbind is valid (1-based indexing)
925        let raw = RawPdu::response(1, 5, 1, vec![VarBind::null(oid!(1, 3, 6, 1))]);
926        let encoded = raw.encode();
927
928        let mut decoder = Decoder::new(encoded);
929        let result = Pdu::decode(&mut decoder);
930        assert!(
931            result.is_ok(),
932            "error_index=1 with 1 varbind should be valid"
933        );
934    }
935
936    #[test]
937    fn test_decode_rejects_negative_non_repeaters() {
938        let raw = RawGetBulkPdu::new(1, -1, 10, vec![VarBind::null(oid!(1, 3, 6, 1))]);
939        let encoded = raw.encode();
940
941        let mut decoder = Decoder::new(encoded);
942        let result = GetBulkPdu::decode(&mut decoder);
943
944        assert!(result.is_err(), "should reject negative non_repeaters");
945        let err = result.unwrap_err();
946        assert!(
947            matches!(&*err, crate::error::Error::MalformedResponse { .. }),
948            "expected MalformedResponse, got {:?}",
949            err
950        );
951    }
952
953    #[test]
954    fn test_decode_rejects_negative_max_repetitions() {
955        let raw = RawGetBulkPdu::new(1, 0, -5, vec![VarBind::null(oid!(1, 3, 6, 1))]);
956        let encoded = raw.encode();
957
958        let mut decoder = Decoder::new(encoded);
959        let result = GetBulkPdu::decode(&mut decoder);
960
961        assert!(result.is_err(), "should reject negative max_repetitions");
962        let err = result.unwrap_err();
963        assert!(
964            matches!(&*err, crate::error::Error::MalformedResponse { .. }),
965            "expected MalformedResponse, got {:?}",
966            err
967        );
968    }
969
970    #[test]
971    fn test_decode_accepts_valid_getbulk_params() {
972        let raw = RawGetBulkPdu::new(1, 0, 10, vec![VarBind::null(oid!(1, 3, 6, 1))]);
973        let encoded = raw.encode();
974
975        let mut decoder = Decoder::new(encoded);
976        let result = GetBulkPdu::decode(&mut decoder);
977        assert!(result.is_ok(), "valid GETBULK params should be accepted");
978
979        let pdu = result.unwrap();
980        assert_eq!(pdu.non_repeaters, 0);
981        assert_eq!(pdu.max_repetitions, 10);
982    }
983
984    #[test]
985    fn test_pdu_decode_getbulk_with_large_max_repetitions() {
986        // GETBULK PDU with max_repetitions (25) > varbinds.len() (1)
987        // This is the normal case for GETBULK requests.
988        // The generic Pdu::decode must not reject this as an invalid error_index.
989        let raw = RawGetBulkPdu::new(12345, 0, 25, vec![VarBind::null(oid!(1, 3, 6, 1, 2, 1, 1))]);
990        let encoded = raw.encode();
991
992        let mut decoder = Decoder::new(encoded);
993        let result = Pdu::decode(&mut decoder);
994        assert!(
995            result.is_ok(),
996            "Pdu::decode should accept GETBULK with max_repetitions > varbinds.len(), got {:?}",
997            result.err()
998        );
999
1000        let pdu = result.unwrap();
1001        assert_eq!(pdu.pdu_type, PduType::GetBulkRequest);
1002        assert_eq!(pdu.request_id, 12345);
1003        // For GETBULK: error_status = non_repeaters, error_index = max_repetitions
1004        assert_eq!(pdu.error_status, 0);
1005        assert_eq!(pdu.error_index, 25);
1006        assert_eq!(pdu.varbinds.len(), 1);
1007    }
1008}