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};
8use crate::oid::Oid;
9use crate::value::Value;
10use crate::varbind::{VarBind, decode_varbind_list, encode_varbind_list};
11use crate::version::Version;
12
13fn invalid_outbound(reason: impl Into<Box<str>>) -> Box<Error> {
14    Error::InvalidMessage(reason.into()).boxed()
15}
16
17/// The protocol role of an outbound PDU.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub(crate) enum PduDirection {
20    Request,
21    Response,
22    Notification,
23}
24
25pub(crate) fn pdu_type_valid_for_version(pdu_type: PduType, version: Version) -> bool {
26    match version {
27        Version::V1 => matches!(
28            pdu_type,
29            PduType::GetRequest | PduType::GetNextRequest | PduType::Response | PduType::SetRequest
30        ),
31        Version::V2c => matches!(
32            pdu_type,
33            PduType::GetRequest
34                | PduType::GetNextRequest
35                | PduType::Response
36                | PduType::SetRequest
37                | PduType::GetBulkRequest
38                | PduType::InformRequest
39                | PduType::TrapV2
40        ),
41        Version::V3 => pdu_type != PduType::TrapV1,
42    }
43}
44
45fn validate_outbound_values(
46    version: Version,
47    direction: PduDirection,
48    varbinds: &[VarBind],
49) -> Result<()> {
50    for varbind in varbinds {
51        varbind.oid.validate_for_wire()?;
52        let value = &varbind.value;
53        if let Value::ObjectIdentifier(oid) = value {
54            oid.validate_for_wire()?;
55        }
56        if matches!(value, Value::Unknown { .. }) {
57            return Err(invalid_outbound(
58                "Value::Unknown cannot be encoded by structured encoders",
59            ));
60        }
61        if matches!(value, Value::UInteger32(_) | Value::Nsap(_)) {
62            return Err(invalid_outbound(
63                "historic receive-only value type cannot be encoded",
64            ));
65        }
66        if version == Version::V1
67            && matches!(
68                value,
69                Value::Counter64(_)
70                    | Value::NoSuchObject
71                    | Value::NoSuchInstance
72                    | Value::EndOfMibView
73            )
74        {
75            return Err(invalid_outbound("value type is not valid in SNMPv1"));
76        }
77        if direction != PduDirection::Response && value.is_exception() {
78            return Err(invalid_outbound(
79                "exception values are only valid in response PDUs",
80            ));
81        }
82    }
83    Ok(())
84}
85
86/// PDU type tag.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88#[repr(u8)]
89pub enum PduType {
90    /// GET request - retrieve specific OID values.
91    GetRequest = 0xA0,
92    /// GET-NEXT request - retrieve the next OID in the MIB tree.
93    GetNextRequest = 0xA1,
94    /// Response to a request from an agent.
95    Response = 0xA2,
96    /// SET request - modify OID values.
97    SetRequest = 0xA3,
98    /// `SNMPv1` trap - unsolicited notification from an agent.
99    TrapV1 = 0xA4,
100    /// GET-BULK request - efficient bulk retrieval of table data.
101    GetBulkRequest = 0xA5,
102    /// INFORM request - acknowledged notification.
103    InformRequest = 0xA6,
104    /// SNMPv2c/v3 trap - unsolicited notification from an agent.
105    TrapV2 = 0xA7,
106    /// Report - used in `SNMPv3` for engine discovery and error reporting.
107    Report = 0xA8,
108}
109
110impl PduType {
111    /// Create from tag byte.
112    #[must_use]
113    pub fn from_tag(tag: u8) -> Option<Self> {
114        match tag {
115            0xA0 => Some(Self::GetRequest),
116            0xA1 => Some(Self::GetNextRequest),
117            0xA2 => Some(Self::Response),
118            0xA3 => Some(Self::SetRequest),
119            0xA4 => Some(Self::TrapV1),
120            0xA5 => Some(Self::GetBulkRequest),
121            0xA6 => Some(Self::InformRequest),
122            0xA7 => Some(Self::TrapV2),
123            0xA8 => Some(Self::Report),
124            _ => None,
125        }
126    }
127
128    /// Returns the tag byte.
129    #[must_use]
130    pub fn tag(self) -> u8 {
131        self as u8
132    }
133}
134
135impl std::fmt::Display for PduType {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match self {
138            Self::GetRequest => write!(f, "GetRequest"),
139            Self::GetNextRequest => write!(f, "GetNextRequest"),
140            Self::Response => write!(f, "Response"),
141            Self::SetRequest => write!(f, "SetRequest"),
142            Self::TrapV1 => write!(f, "TrapV1"),
143            Self::GetBulkRequest => write!(f, "GetBulkRequest"),
144            Self::InformRequest => write!(f, "InformRequest"),
145            Self::TrapV2 => write!(f, "TrapV2"),
146            Self::Report => write!(f, "Report"),
147        }
148    }
149}
150
151/// PDU tags whose body uses the standard request/error/varbind layout.
152///
153/// GETBULK is deliberately absent because its two integer fields have different
154/// meanings. The SNMPv1 Trap body remains envelope-specific.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
156pub enum StandardPduType {
157    /// GET request.
158    GetRequest,
159    /// GET-NEXT request.
160    GetNextRequest,
161    /// Response.
162    Response,
163    /// SET request.
164    SetRequest,
165    /// INFORM request.
166    InformRequest,
167    /// SNMPv2 trap.
168    TrapV2,
169    /// SNMPv3 report.
170    Report,
171}
172
173impl StandardPduType {
174    /// Return the corresponding wire PDU type.
175    #[must_use]
176    pub fn pdu_type(self) -> PduType {
177        match self {
178            Self::GetRequest => PduType::GetRequest,
179            Self::GetNextRequest => PduType::GetNextRequest,
180            Self::Response => PduType::Response,
181            Self::SetRequest => PduType::SetRequest,
182            Self::InformRequest => PduType::InformRequest,
183            Self::TrapV2 => PduType::TrapV2,
184            Self::Report => PduType::Report,
185        }
186    }
187}
188
189impl TryFrom<PduType> for StandardPduType {
190    type Error = PduType;
191
192    fn try_from(value: PduType) -> std::result::Result<Self, Self::Error> {
193        match value {
194            PduType::GetRequest => Ok(Self::GetRequest),
195            PduType::GetNextRequest => Ok(Self::GetNextRequest),
196            PduType::Response => Ok(Self::Response),
197            PduType::SetRequest => Ok(Self::SetRequest),
198            PduType::InformRequest => Ok(Self::InformRequest),
199            PduType::TrapV2 => Ok(Self::TrapV2),
200            PduType::Report => Ok(Self::Report),
201            PduType::TrapV1 | PduType::GetBulkRequest => Err(value),
202        }
203    }
204}
205
206/// The typed body of a standard or GETBULK PDU.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum PduBody {
209    /// Standard request/response body fields.
210    Standard {
211        /// PDU tag, excluding GETBULK and SNMPv1 Trap.
212        pdu_type: StandardPduType,
213        /// Error status (zero for requests).
214        error_status: i32,
215        /// One-based error index, or zero.
216        error_index: i32,
217    },
218    /// GETBULK-specific body fields.
219    ///
220    /// Encoding rejects either field above the RFC 3416 `Integer32` maximum.
221    /// Use [`GetBulkPdu::new`] for checked outbound construction.
222    GetBulk {
223        /// Number of non-repeating OIDs.
224        non_repeaters: u32,
225        /// Maximum repetitions for repeating OIDs.
226        max_repetitions: u32,
227    },
228}
229
230/// Largest valid value for either GETBULK parameter.
231pub(crate) const MAX_GET_BULK_VALUE: u32 = i32::MAX as u32;
232
233/// Canonical PDU representation for standard and GETBULK operations.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct Pdu {
236    /// Request ID for correlating requests and responses.
237    pub(crate) request_id: i32,
238    /// Typed fields that determine the PDU tag and integer meanings.
239    pub(crate) body: PduBody,
240    /// Variable bindings.
241    pub(crate) varbinds: Vec<VarBind>,
242}
243
244/// A known error-status value suitable for an outbound Response PDU.
245///
246/// Unlike [`ErrorStatus`], this type has no `Unknown` variant. The relationship
247/// between the status, error index, SNMP version, and variable-binding count is
248/// checked by [`ResponsePdu::new`].
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
250pub enum OutboundErrorStatus {
251    /// No error occurred.
252    NoError,
253    /// The response would exceed a message-size limit.
254    TooBig,
255    /// An object name was unavailable (SNMPv1 only).
256    NoSuchName,
257    /// A SET value was invalid (SNMPv1 only).
258    BadValue,
259    /// A SET targeted a read-only object (SNMPv1 only).
260    ReadOnly,
261    /// An unspecified processing error occurred.
262    GenErr,
263    /// Access to the object was denied.
264    NoAccess,
265    /// A SET value had the wrong ASN.1 type.
266    WrongType,
267    /// A SET value had the wrong length.
268    WrongLength,
269    /// A SET value had the wrong encoding.
270    WrongEncoding,
271    /// A SET value was otherwise invalid.
272    WrongValue,
273    /// Creation of the object was not permitted.
274    NoCreation,
275    /// The requested value was inconsistent.
276    InconsistentValue,
277    /// Required resources were unavailable.
278    ResourceUnavailable,
279    /// The SET commit phase failed.
280    CommitFailed,
281    /// The SET undo phase failed.
282    UndoFailed,
283    /// Authorization failed.
284    AuthorizationError,
285    /// The object is not writable.
286    NotWritable,
287    /// Object creation was inconsistent.
288    InconsistentName,
289}
290
291impl OutboundErrorStatus {
292    /// Return the wire `error-status` value.
293    #[must_use]
294    pub const fn as_i32(self) -> i32 {
295        match self {
296            Self::NoError => 0,
297            Self::TooBig => 1,
298            Self::NoSuchName => 2,
299            Self::BadValue => 3,
300            Self::ReadOnly => 4,
301            Self::GenErr => 5,
302            Self::NoAccess => 6,
303            Self::WrongType => 7,
304            Self::WrongLength => 8,
305            Self::WrongEncoding => 9,
306            Self::WrongValue => 10,
307            Self::NoCreation => 11,
308            Self::InconsistentValue => 12,
309            Self::ResourceUnavailable => 13,
310            Self::CommitFailed => 14,
311            Self::UndoFailed => 15,
312            Self::AuthorizationError => 16,
313            Self::NotWritable => 17,
314            Self::InconsistentName => 18,
315        }
316    }
317}
318
319impl TryFrom<ErrorStatus> for OutboundErrorStatus {
320    type Error = ErrorStatus;
321
322    fn try_from(value: ErrorStatus) -> std::result::Result<Self, Self::Error> {
323        match value {
324            ErrorStatus::NoError => Ok(Self::NoError),
325            ErrorStatus::TooBig => Ok(Self::TooBig),
326            ErrorStatus::NoSuchName => Ok(Self::NoSuchName),
327            ErrorStatus::BadValue => Ok(Self::BadValue),
328            ErrorStatus::ReadOnly => Ok(Self::ReadOnly),
329            ErrorStatus::GenErr => Ok(Self::GenErr),
330            ErrorStatus::NoAccess => Ok(Self::NoAccess),
331            ErrorStatus::WrongType => Ok(Self::WrongType),
332            ErrorStatus::WrongLength => Ok(Self::WrongLength),
333            ErrorStatus::WrongEncoding => Ok(Self::WrongEncoding),
334            ErrorStatus::WrongValue => Ok(Self::WrongValue),
335            ErrorStatus::NoCreation => Ok(Self::NoCreation),
336            ErrorStatus::InconsistentValue => Ok(Self::InconsistentValue),
337            ErrorStatus::ResourceUnavailable => Ok(Self::ResourceUnavailable),
338            ErrorStatus::CommitFailed => Ok(Self::CommitFailed),
339            ErrorStatus::UndoFailed => Ok(Self::UndoFailed),
340            ErrorStatus::AuthorizationError => Ok(Self::AuthorizationError),
341            ErrorStatus::NotWritable => Ok(Self::NotWritable),
342            ErrorStatus::InconsistentName => Ok(Self::InconsistentName),
343            ErrorStatus::Unknown(_) => Err(value),
344        }
345    }
346}
347
348/// A one-based variable-binding index for an outbound error response.
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
350pub struct ErrorIndex(std::num::NonZeroU32);
351
352impl ErrorIndex {
353    /// Construct a one-based index and check it against a variable-binding list.
354    ///
355    /// # Errors
356    ///
357    /// Returns [`Error::InvalidMessage`] for zero, a value above the wire
358    /// `Integer32` maximum, or an index beyond `varbind_count`.
359    pub fn new(index: u32, varbind_count: usize) -> Result<Self> {
360        let index = std::num::NonZeroU32::new(index)
361            .ok_or_else(|| invalid_outbound("error_index must be nonzero"))?;
362        if index.get() > i32::MAX as u32 {
363            return Err(invalid_outbound("error_index exceeds i32::MAX"));
364        }
365        if usize::try_from(index.get()).map_or(true, |index| index > varbind_count) {
366            return Err(invalid_outbound(
367                "error_index does not identify a variable binding",
368            ));
369        }
370        Ok(Self(index))
371    }
372
373    /// Return the one-based wire value.
374    #[must_use]
375    pub const fn get(self) -> u32 {
376        self.0.get()
377    }
378}
379
380/// A validated outbound request using the standard PDU layout.
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct RequestPdu {
383    version: Version,
384    pdu: Pdu,
385}
386
387/// A validated outbound GETBULK request.
388#[derive(Debug, Clone, PartialEq, Eq)]
389pub struct GetBulkPdu {
390    version: Version,
391    pdu: Pdu,
392}
393
394/// A validated outbound Response or SNMPv3 Report PDU.
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct ResponsePdu {
397    version: Version,
398    pdu: Pdu,
399}
400
401/// A validated outbound InformRequest or SNMPv2-Trap PDU.
402#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct NotificationPdu {
404    version: Version,
405    pdu: Pdu,
406}
407
408/// A validated outbound SNMPv1 Trap PDU.
409#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct TrapV1Notification {
411    pdu: TrapV1Pdu,
412}
413
414/// Any validated outbound standard-layout PDU.
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub enum OutboundPdu {
417    /// GET, GETNEXT, or SET request.
418    Request(RequestPdu),
419    /// GETBULK request.
420    GetBulk(GetBulkPdu),
421    /// Response or Report PDU.
422    Response(ResponsePdu),
423    /// InformRequest or SNMPv2-Trap PDU.
424    Notification(NotificationPdu),
425}
426
427impl Pdu {
428    /// Construct a permissive raw PDU without applying outbound invariants.
429    ///
430    /// This is intended for protocol tooling and tests that need to model
431    /// received peer data. Convert it to a role-specific outbound type, or use
432    /// a fallible message constructor, before sending it.
433    #[must_use]
434    pub fn from_raw_parts(request_id: i32, body: PduBody, varbinds: Vec<VarBind>) -> Self {
435        Self {
436            request_id,
437            body,
438            varbinds,
439        }
440    }
441
442    /// Construct a standard-layout PDU.
443    #[must_use]
444    pub(crate) fn standard(
445        pdu_type: StandardPduType,
446        request_id: i32,
447        error_status: i32,
448        error_index: i32,
449        varbinds: Vec<VarBind>,
450    ) -> Self {
451        Self {
452            request_id,
453            body: PduBody::Standard {
454                pdu_type,
455                error_status,
456                error_index,
457            },
458            varbinds,
459        }
460    }
461
462    /// Construct a response PDU.
463    #[must_use]
464    pub(crate) fn response(
465        request_id: i32,
466        error_status: i32,
467        error_index: i32,
468        varbinds: Vec<VarBind>,
469    ) -> Self {
470        Self::standard(
471            StandardPduType::Response,
472            request_id,
473            error_status,
474            error_index,
475            varbinds,
476        )
477    }
478
479    /// Create a GET request PDU.
480    #[must_use]
481    pub(crate) fn get_request(request_id: i32, oids: &[Oid]) -> Self {
482        Self::standard(
483            StandardPduType::GetRequest,
484            request_id,
485            0,
486            0,
487            oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
488        )
489    }
490
491    /// Create a GETNEXT request PDU.
492    #[cfg(test)]
493    #[must_use]
494    pub(crate) fn get_next_request(request_id: i32, oids: &[Oid]) -> Self {
495        Self::standard(
496            StandardPduType::GetNextRequest,
497            request_id,
498            0,
499            0,
500            oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
501        )
502    }
503
504    /// Create a SET request PDU.
505    #[cfg(test)]
506    #[must_use]
507    pub(crate) fn set_request(request_id: i32, varbinds: Vec<VarBind>) -> Self {
508        Self::standard(StandardPduType::SetRequest, request_id, 0, 0, varbinds)
509    }
510
511    /// Create a SNMPv2c/v3 Trap PDU.
512    ///
513    /// Prepends the mandatory varbind prefix per RFC 3416 Section 4.2.6:
514    /// 1. sysUpTime.0 (1.3.6.1.2.1.1.3.0) with `TimeTicks` value
515    /// 2. snmpTrapOID.0 (1.3.6.1.6.3.1.1.4.1.0) with the trap OID
516    ///
517    /// Caller-provided varbinds are appended after the prefix.
518    #[must_use]
519    pub(crate) fn trap_v2(
520        request_id: i32,
521        uptime: u32,
522        trap_oid: &Oid,
523        varbinds: Vec<VarBind>,
524    ) -> Self {
525        let mut all_varbinds = Vec::with_capacity(2 + varbinds.len());
526        all_varbinds.push(VarBind::new(
527            crate::notification::oids::sys_uptime(),
528            crate::value::Value::TimeTicks(uptime),
529        ));
530        all_varbinds.push(VarBind::new(
531            crate::notification::oids::snmp_trap_oid(),
532            crate::value::Value::ObjectIdentifier(trap_oid.clone()),
533        ));
534        all_varbinds.extend(varbinds);
535        Self::standard(StandardPduType::TrapV2, request_id, 0, 0, all_varbinds)
536    }
537
538    /// Create an `InformRequest` PDU.
539    ///
540    /// Same varbind structure as `trap_v2` (sysUpTime.0 + snmpTrapOID.0 prefix),
541    /// but uses `InformRequest` PDU type which expects a Response from the receiver.
542    #[must_use]
543    pub(crate) fn inform_request(
544        request_id: i32,
545        uptime: u32,
546        trap_oid: &Oid,
547        varbinds: Vec<VarBind>,
548    ) -> Self {
549        let mut all_varbinds = Vec::with_capacity(2 + varbinds.len());
550        all_varbinds.push(VarBind::new(
551            crate::notification::oids::sys_uptime(),
552            crate::value::Value::TimeTicks(uptime),
553        ));
554        all_varbinds.push(VarBind::new(
555            crate::notification::oids::snmp_trap_oid(),
556            crate::value::Value::ObjectIdentifier(trap_oid.clone()),
557        ));
558        all_varbinds.extend(varbinds);
559        Self::standard(
560            StandardPduType::InformRequest,
561            request_id,
562            0,
563            0,
564            all_varbinds,
565        )
566    }
567
568    /// Create a GETBULK request PDU.
569    ///
570    /// # Errors
571    ///
572    /// Returns [`Error::InvalidMessage`] when
573    /// either GETBULK parameter exceeds the RFC 3416 `Integer32` maximum.
574    pub(crate) fn get_bulk(
575        request_id: i32,
576        non_repeaters: u32,
577        max_repetitions: u32,
578        varbinds: Vec<VarBind>,
579    ) -> Result<Self> {
580        Self::checked_get_bulk_fields(non_repeaters, max_repetitions)?;
581        Ok(Self {
582            request_id,
583            body: PduBody::GetBulk {
584                non_repeaters,
585                max_repetitions,
586            },
587            varbinds,
588        })
589    }
590
591    pub(crate) fn checked_get_bulk_fields(non_repeaters: u32, max_repetitions: u32) -> Result<()> {
592        Self::get_bulk_fields_for_wire(non_repeaters, max_repetitions)?;
593        Ok(())
594    }
595
596    fn get_bulk_fields_for_wire(non_repeaters: u32, max_repetitions: u32) -> Result<(i32, i32)> {
597        let non_repeaters = i32::try_from(non_repeaters)
598            .map_err(|_| invalid_outbound("GETBULK non_repeaters exceeds i32::MAX"))?;
599        let max_repetitions = i32::try_from(max_repetitions)
600            .map_err(|_| invalid_outbound("GETBULK max_repetitions exceeds i32::MAX"))?;
601        Ok((non_repeaters, max_repetitions))
602    }
603
604    pub(crate) fn outbound_direction(&self) -> PduDirection {
605        match self.pdu_type() {
606            PduType::Response | PduType::Report => PduDirection::Response,
607            PduType::TrapV1 | PduType::TrapV2 => PduDirection::Notification,
608            PduType::GetRequest
609            | PduType::GetNextRequest
610            | PduType::SetRequest
611            | PduType::GetBulkRequest
612            | PduType::InformRequest => PduDirection::Request,
613        }
614    }
615
616    #[cfg(test)]
617    fn inferred_encode_version(&self) -> Version {
618        match self.pdu_type() {
619            PduType::Report => Version::V3,
620            _ => Version::V2c,
621        }
622    }
623
624    pub(crate) fn validate_outbound(
625        &self,
626        version: Version,
627        direction: PduDirection,
628    ) -> Result<()> {
629        let pdu_type = self.pdu_type();
630        if !pdu_type_valid_for_version(pdu_type, version) {
631            return Err(invalid_outbound(format!(
632                "{pdu_type} PDU is not valid for {version:?}"
633            )));
634        }
635        if self.outbound_direction() != direction {
636            return Err(invalid_outbound(format!(
637                "{pdu_type} PDU is not valid in the {direction:?} direction"
638            )));
639        }
640        validate_outbound_values(version, direction, &self.varbinds)?;
641
642        match self.body {
643            PduBody::GetBulk {
644                non_repeaters,
645                max_repetitions,
646            } => {
647                Self::get_bulk_fields_for_wire(non_repeaters, max_repetitions)?;
648            }
649            PduBody::Standard {
650                pdu_type: StandardPduType::Response,
651                error_status,
652                error_index,
653            } => {
654                let maximum_status = if version == Version::V1 { 5 } else { 18 };
655                if !(0..=maximum_status).contains(&error_status) {
656                    return Err(invalid_outbound(
657                        "Response error_status is not valid for the SNMP version",
658                    ));
659                }
660                if error_index < 0
661                    || usize::try_from(error_index)
662                        .ok()
663                        .is_none_or(|index| index > self.varbinds.len())
664                {
665                    return Err(invalid_outbound(
666                        "Response error_index does not identify a variable binding",
667                    ));
668                }
669                if matches!(error_status, 0 | 1 | 15 | 16) {
670                    if error_index != 0 {
671                        return Err(invalid_outbound(
672                            "noError, tooBig, undoFailed, and authorizationError Responses require error_index zero",
673                        ));
674                    }
675                } else if error_index == 0 {
676                    return Err(invalid_outbound(
677                        "this Response error_status requires a nonzero error_index",
678                    ));
679                }
680                if version != Version::V1
681                    && error_status == ErrorStatus::TooBig.as_i32()
682                    && !self.varbinds.is_empty()
683                {
684                    return Err(invalid_outbound(
685                        "SNMPv2c and SNMPv3 tooBig Responses require an empty variable-binding list",
686                    ));
687                }
688            }
689            PduBody::Standard {
690                error_status,
691                error_index,
692                ..
693            } => {
694                if error_status != 0 || error_index != 0 {
695                    return Err(invalid_outbound(
696                        "request and notification PDUs require zero error fields",
697                    ));
698                }
699            }
700        }
701
702        match pdu_type {
703            PduType::GetRequest | PduType::GetNextRequest | PduType::GetBulkRequest => {}
704            PduType::SetRequest => {
705                if self
706                    .varbinds
707                    .iter()
708                    .any(|varbind| varbind.value == Value::Null || varbind.value.is_exception())
709                {
710                    return Err(invalid_outbound(
711                        "SET request variable bindings require concrete values",
712                    ));
713                }
714            }
715            PduType::InformRequest | PduType::TrapV2 => {
716                let [uptime, trap_oid, rest @ ..] = self.varbinds.as_slice() else {
717                    return Err(invalid_outbound(
718                        "notification PDU requires sysUpTime.0 and snmpTrapOID.0",
719                    ));
720                };
721                if uptime.oid != crate::notification::oids::sys_uptime()
722                    || !matches!(uptime.value, Value::TimeTicks(_))
723                    || trap_oid.oid != crate::notification::oids::snmp_trap_oid()
724                    || !matches!(trap_oid.value, Value::ObjectIdentifier(_))
725                    || rest
726                        .iter()
727                        .any(|varbind| varbind.value == Value::Null || varbind.value.is_exception())
728                {
729                    return Err(invalid_outbound(
730                        "notification PDU has an invalid mandatory prefix or value",
731                    ));
732                }
733            }
734            PduType::Report => {
735                if !matches!(
736                    self.varbinds.as_slice(),
737                    [VarBind {
738                        value: Value::Counter32(_),
739                        ..
740                    }]
741                ) {
742                    return Err(invalid_outbound(
743                        "Report PDU requires exactly one Counter32 variable binding",
744                    ));
745                }
746            }
747            PduType::Response | PduType::TrapV1 => {}
748        }
749
750        Ok(())
751    }
752
753    pub(crate) fn encode_for(
754        &self,
755        buf: &mut EncodeBuf,
756        version: Version,
757        direction: PduDirection,
758    ) -> Result<()> {
759        self.validate_outbound(version, direction)?;
760        self.encode_validated(buf)
761    }
762
763    fn encode_validated(&self, buf: &mut EncodeBuf) -> Result<()> {
764        let (pdu_type, first_field, second_field) = match self.body {
765            PduBody::Standard {
766                pdu_type,
767                error_status,
768                error_index,
769            } => (pdu_type.pdu_type(), error_status, error_index),
770            PduBody::GetBulk {
771                non_repeaters,
772                max_repetitions,
773            } => {
774                let (non_repeaters, max_repetitions) =
775                    Self::get_bulk_fields_for_wire(non_repeaters, max_repetitions)?;
776                (PduType::GetBulkRequest, non_repeaters, max_repetitions)
777            }
778        };
779
780        buf.push_constructed(pdu_type.tag(), |buf| {
781            encode_varbind_list(buf, &self.varbinds)?;
782            buf.push_integer(second_field);
783            buf.push_integer(first_field);
784            buf.push_integer(self.request_id);
785            Ok(())
786        })
787    }
788
789    /// Encode a structurally valid PDU to BER.
790    ///
791    /// Version-specific validation is repeated when the PDU is placed in a
792    /// community or SNMPv3 message envelope.
793    #[cfg(test)]
794    pub(crate) fn encode(&self, buf: &mut EncodeBuf) -> Result<()> {
795        self.encode_for(
796            buf,
797            self.inferred_encode_version(),
798            self.outbound_direction(),
799        )
800    }
801
802    /// Decode from BER (after tag has been peeked).
803    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
804        let tag_offset = decoder.local_offset();
805        let tag = decoder.read_tag()?;
806        let pdu_type = PduType::from_tag(tag).ok_or_else(|| {
807            tracing::debug!(target: "async_snmp::pdu", { offset = decoder.offset(), tag = tag, kind = %DecodeErrorKind::UnknownPduType(tag) }, "decode error");
808            decoder.malformed_at(tag_offset, DecodeErrorKind::UnknownPduType(tag))
809        })?;
810
811        // The SNMPv1 Trap PDU (tag 0xA4) has a distinct wire layout (RFC 1157
812        // Section 4.1.6) that this generic decoder cannot parse correctly. It is
813        // only ever carried in v1 messages and must be routed through
814        // `TrapV1Pdu::decode`; reaching it here means a v2c/v3 message carried a
815        // v1 Trap tag, which is rejected.
816        if pdu_type == PduType::TrapV1 {
817            tracing::debug!(target: "async_snmp::pdu", { offset = decoder.offset(), tag = tag }, "TrapV1 PDU tag not valid in generic PDU context");
818            return Err(decoder.malformed_at(tag_offset, DecodeErrorKind::UnknownPduType(tag)));
819        }
820
821        let len = decoder.read_length()?;
822        let config = decoder.decode_config();
823        let mut pdu_decoder = decoder.sub_decoder(len)?;
824
825        // These are protocol Integer32 fields, so decode the complete width
826        // without consulting generic value-truncation compatibility.
827        let request_id = pdu_decoder.read_bounded_integer(i32::MIN, i32::MAX)?;
828        let mut first_field = pdu_decoder.read_bounded_integer(i32::MIN, i32::MAX)?;
829        let mut second_field = pdu_decoder.read_bounded_integer(i32::MIN, i32::MAX)?;
830        let varbinds = decode_varbind_list(&mut pdu_decoder)?;
831        if !pdu_decoder.is_empty() {
832            return Err(pdu_decoder.malformed(DecodeErrorKind::TrailingData {
833                remaining: pdu_decoder.remaining(),
834            }));
835        }
836
837        let body = if pdu_type == PduType::GetBulkRequest {
838            for (field, value) in [
839                ("non_repeaters", &mut first_field),
840                ("max_repetitions", &mut second_field),
841            ] {
842                if *value < 0 {
843                    if !config.normalize_negative_get_bulk_fields {
844                        return Err(pdu_decoder.malformed(DecodeErrorKind::InvalidValue));
845                    }
846                    tracing::warn!(target: "async_snmp::pdu", anomaly = "negative_get_bulk_field", direction = "decode", field, value = *value, normalized = 0, "normalized negative GETBULK field");
847                    pdu_decoder.record_anomaly(crate::DecodeAnomaly::NegativeGetBulkField {
848                        field: match field {
849                            "non_repeaters" => crate::GetBulkField::NonRepeaters,
850                            "max_repetitions" => crate::GetBulkField::MaxRepetitions,
851                            _ => unreachable!("fixed GETBULK field name"),
852                        },
853                        original: *value,
854                        canonical: 0,
855                    });
856                    *value = 0;
857                }
858            }
859            let non_repeaters = u32::try_from(first_field)
860                .map_err(|_| pdu_decoder.malformed(DecodeErrorKind::InvalidValue))?;
861            let max_repetitions = u32::try_from(second_field)
862                .map_err(|_| pdu_decoder.malformed(DecodeErrorKind::InvalidValue))?;
863            PduBody::GetBulk {
864                non_repeaters,
865                max_repetitions,
866            }
867        } else {
868            let standard_type = StandardPduType::try_from(pdu_type).map_err(|_| {
869                pdu_decoder.malformed(DecodeErrorKind::UnknownPduType(pdu_type.tag()))
870            })?;
871            PduBody::Standard {
872                pdu_type: standard_type,
873                error_status: first_field,
874                error_index: second_field,
875            }
876        };
877
878        // For standard PDUs, error_index is not validated here. net-snmp
879        // performs no bounds checking on this field.
880        Ok(Pdu {
881            request_id,
882            body,
883            varbinds,
884        })
885    }
886
887    /// Return the wire PDU type.
888    #[must_use]
889    pub fn pdu_type(&self) -> PduType {
890        match self.body {
891            PduBody::Standard { pdu_type, .. } => pdu_type.pdu_type(),
892            PduBody::GetBulk { .. } => PduType::GetBulkRequest,
893        }
894    }
895
896    /// Return the request identifier.
897    #[must_use]
898    pub const fn request_id(&self) -> i32 {
899        self.request_id
900    }
901
902    /// Return the permissive decoded body fields.
903    #[must_use]
904    pub const fn raw_body(&self) -> &PduBody {
905        &self.body
906    }
907
908    /// Return the variable bindings in wire order.
909    #[must_use]
910    pub fn varbinds(&self) -> &[VarBind] {
911        &self.varbinds
912    }
913
914    /// Consume the decoded/raw PDU and return its variable bindings.
915    #[must_use]
916    pub fn into_varbinds(self) -> Vec<VarBind> {
917        self.varbinds
918    }
919
920    /// Consume this decoded/raw PDU into its invariant-bearing parts.
921    #[must_use]
922    pub fn into_raw_parts(self) -> (i32, PduBody, Vec<VarBind>) {
923        (self.request_id, self.body, self.varbinds)
924    }
925
926    pub(crate) fn set_request_id(&mut self, request_id: i32) {
927        self.request_id = request_id;
928    }
929
930    /// Return standard response fields, if this is a standard-layout PDU.
931    #[must_use]
932    pub fn error_fields(&self) -> Option<(i32, i32)> {
933        match self.body {
934            PduBody::Standard {
935                error_status,
936                error_index,
937                ..
938            } => Some((error_status, error_index)),
939            PduBody::GetBulk { .. } => None,
940        }
941    }
942
943    /// Return the unsigned GETBULK fields, if this is a GETBULK PDU.
944    #[must_use]
945    pub fn get_bulk_fields(&self) -> Option<(u32, u32)> {
946        match self.body {
947            PduBody::GetBulk {
948                non_repeaters,
949                max_repetitions,
950            } => Some((non_repeaters, max_repetitions)),
951            PduBody::Standard { .. } => None,
952        }
953    }
954
955    /// Return the standard error status, or zero for GETBULK.
956    #[must_use]
957    pub fn error_status(&self) -> i32 {
958        self.error_fields().map_or(0, |fields| fields.0)
959    }
960
961    /// Return the standard error index, or zero for GETBULK.
962    #[must_use]
963    pub fn error_index(&self) -> i32 {
964        self.error_fields().map_or(0, |fields| fields.1)
965    }
966
967    /// Set the tag of a standard-layout PDU.
968    ///
969    /// Returns false for a GETBULK PDU.
970    #[cfg(test)]
971    pub(crate) fn set_standard_pdu_type(&mut self, value: StandardPduType) -> bool {
972        match &mut self.body {
973            PduBody::Standard { pdu_type, .. } => {
974                *pdu_type = value;
975                true
976            }
977            PduBody::GetBulk { .. } => false,
978        }
979    }
980
981    /// Set the standard error status.
982    ///
983    /// Returns false for a GETBULK PDU.
984    #[cfg(test)]
985    pub(crate) fn set_error_status(&mut self, value: i32) -> bool {
986        match &mut self.body {
987            PduBody::Standard { error_status, .. } => {
988                *error_status = value;
989                true
990            }
991            PduBody::GetBulk { .. } => false,
992        }
993    }
994
995    /// Set the standard error index.
996    ///
997    /// Returns false for a GETBULK PDU.
998    #[cfg(test)]
999    pub(crate) fn set_error_index(&mut self, value: i32) -> bool {
1000        match &mut self.body {
1001            PduBody::Standard { error_index, .. } => {
1002                *error_index = value;
1003                true
1004            }
1005            PduBody::GetBulk { .. } => false,
1006        }
1007    }
1008
1009    /// Check if this is an error response.
1010    #[must_use]
1011    pub fn is_error(&self) -> bool {
1012        self.pdu_type() == PduType::Response && self.error_status() != 0
1013    }
1014
1015    /// Returns the error status as an enum.
1016    #[must_use]
1017    pub fn error_status_enum(&self) -> ErrorStatus {
1018        ErrorStatus::from_i32(self.error_status())
1019    }
1020
1021    /// Create a validated successful Response PDU from this decoded/raw PDU.
1022    ///
1023    /// The response copies the `request_id` and variable bindings,
1024    /// sets `error_status` and `error_index` to 0, and changes the PDU type to Response.
1025    pub(crate) fn to_response(&self, version: Version) -> Result<Self> {
1026        Ok(ResponsePdu::success(version, self.request_id, self.varbinds.clone())?.into_raw())
1027    }
1028
1029    /// Create a validated Response PDU with a specific known error status.
1030    #[cfg(any(test, feature = "agent"))]
1031    pub(crate) fn to_error_response(
1032        &self,
1033        version: Version,
1034        error_status: ErrorStatus,
1035        error_index: usize,
1036    ) -> Result<Self> {
1037        let error_status = OutboundErrorStatus::try_from(error_status).map_err(|status| {
1038            invalid_outbound(format!(
1039                "unknown error-status {} is receive-only",
1040                status.as_i32()
1041            ))
1042        })?;
1043        let error_index = if error_index == 0 {
1044            None
1045        } else {
1046            let error_index = u32::try_from(error_index)
1047                .map_err(|_| invalid_outbound("error_index exceeds u32::MAX"))?;
1048            Some(ErrorIndex::new(error_index, self.varbinds.len())?)
1049        };
1050        Ok(ResponsePdu::new(
1051            version,
1052            self.request_id,
1053            error_status,
1054            error_index,
1055            self.varbinds.clone(),
1056        )?
1057        .into_raw())
1058    }
1059
1060    /// Check if this is a notification PDU (Trap or Inform).
1061    #[must_use]
1062    pub fn is_notification(&self) -> bool {
1063        matches!(
1064            self.pdu_type(),
1065            PduType::TrapV1 | PduType::TrapV2 | PduType::InformRequest
1066        )
1067    }
1068
1069    /// Check if this is a confirmed-class PDU (requires response).
1070    #[must_use]
1071    pub fn is_confirmed(&self) -> bool {
1072        matches!(
1073            self.pdu_type(),
1074            PduType::GetRequest
1075                | PduType::GetNextRequest
1076                | PduType::GetBulkRequest
1077                | PduType::SetRequest
1078                | PduType::InformRequest
1079        )
1080    }
1081}
1082
1083fn validate_no_receive_only_values(version: Version, varbinds: &[VarBind]) -> Result<()> {
1084    validate_outbound_values(version, PduDirection::Response, varbinds)
1085}
1086
1087fn validate_request_values(kind: StandardPduType, varbinds: &[VarBind]) -> Result<()> {
1088    if kind == StandardPduType::SetRequest
1089        && varbinds
1090            .iter()
1091            .any(|varbind| varbind.value == Value::Null || varbind.value.is_exception())
1092    {
1093        return Err(invalid_outbound(
1094            "SET request variable bindings require concrete values",
1095        ));
1096    }
1097    Ok(())
1098}
1099
1100impl RequestPdu {
1101    fn new(
1102        version: Version,
1103        kind: StandardPduType,
1104        request_id: i32,
1105        varbinds: Vec<VarBind>,
1106    ) -> Result<Self> {
1107        if !matches!(
1108            kind,
1109            StandardPduType::GetRequest
1110                | StandardPduType::GetNextRequest
1111                | StandardPduType::SetRequest
1112        ) {
1113            return Err(invalid_outbound("PDU type is not an ordinary request"));
1114        }
1115        validate_outbound_values(version, PduDirection::Request, &varbinds)?;
1116        validate_request_values(kind, &varbinds)?;
1117        let pdu = Pdu::standard(kind, request_id, 0, 0, varbinds);
1118        pdu.validate_outbound(version, PduDirection::Request)?;
1119        Ok(Self { version, pdu })
1120    }
1121
1122    /// Construct a GET request whose variable bindings contain Null values.
1123    pub fn get(version: Version, request_id: i32, oids: &[Oid]) -> Result<Self> {
1124        Self::new(
1125            version,
1126            StandardPduType::GetRequest,
1127            request_id,
1128            oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
1129        )
1130    }
1131
1132    /// Construct a GETNEXT request whose variable bindings contain Null values.
1133    pub fn get_next(version: Version, request_id: i32, oids: &[Oid]) -> Result<Self> {
1134        Self::new(
1135            version,
1136            StandardPduType::GetNextRequest,
1137            request_id,
1138            oids.iter().map(|oid| VarBind::null(oid.clone())).collect(),
1139        )
1140    }
1141
1142    /// Construct a SET request with concrete values.
1143    pub fn set(version: Version, request_id: i32, varbinds: Vec<VarBind>) -> Result<Self> {
1144        Self::new(version, StandardPduType::SetRequest, request_id, varbinds)
1145    }
1146
1147    /// Validate a decoded/raw PDU as an ordinary outbound request.
1148    pub fn try_from_raw(version: Version, pdu: Pdu) -> Result<Self> {
1149        let kind = match &pdu.body {
1150            PduBody::Standard { pdu_type, .. } => *pdu_type,
1151            PduBody::GetBulk { .. } => {
1152                return Err(invalid_outbound("GETBULK requires GetBulkPdu"));
1153            }
1154        };
1155        validate_request_values(kind, &pdu.varbinds)?;
1156        pdu.validate_outbound(version, PduDirection::Request)?;
1157        if !matches!(
1158            kind,
1159            StandardPduType::GetRequest
1160                | StandardPduType::GetNextRequest
1161                | StandardPduType::SetRequest
1162        ) {
1163            return Err(invalid_outbound("PDU type is not an ordinary request"));
1164        }
1165        Ok(Self { version, pdu })
1166    }
1167
1168    /// Encode the bare request PDU.
1169    pub fn encode(&self, buf: &mut EncodeBuf) -> Result<()> {
1170        self.pdu
1171            .encode_for(buf, self.version, PduDirection::Request)
1172    }
1173
1174    /// Return the SNMP version against which this request was validated.
1175    #[must_use]
1176    pub const fn version(&self) -> Version {
1177        self.version
1178    }
1179
1180    /// Return the request identifier.
1181    #[must_use]
1182    pub const fn request_id(&self) -> i32 {
1183        self.pdu.request_id
1184    }
1185
1186    /// Return whether this is GET, GETNEXT, or SET.
1187    #[must_use]
1188    pub fn pdu_type(&self) -> PduType {
1189        self.pdu.pdu_type()
1190    }
1191
1192    /// Return the request variable bindings.
1193    #[must_use]
1194    pub fn varbinds(&self) -> &[VarBind] {
1195        &self.pdu.varbinds
1196    }
1197
1198    /// Return the decoded/raw representation used on the wire.
1199    #[must_use]
1200    pub const fn as_raw(&self) -> &Pdu {
1201        &self.pdu
1202    }
1203
1204    /// Consume this request into its decoded/raw representation.
1205    #[must_use]
1206    pub fn into_raw(self) -> Pdu {
1207        self.pdu
1208    }
1209
1210    /// Change the request identifier without affecting any other invariant.
1211    pub fn set_request_id(&mut self, request_id: i32) {
1212        self.pdu.request_id = request_id;
1213    }
1214}
1215
1216impl GetBulkPdu {
1217    /// Construct a GETBULK request.
1218    pub fn new(
1219        version: Version,
1220        request_id: i32,
1221        non_repeaters: u32,
1222        max_repetitions: u32,
1223        varbinds: Vec<VarBind>,
1224    ) -> Result<Self> {
1225        if version == Version::V1 {
1226            return Err(invalid_outbound("GETBULK is not valid in SNMPv1"));
1227        }
1228        validate_outbound_values(version, PduDirection::Request, &varbinds)?;
1229        let pdu = Pdu::get_bulk(request_id, non_repeaters, max_repetitions, varbinds)?;
1230        pdu.validate_outbound(version, PduDirection::Request)?;
1231        Ok(Self { version, pdu })
1232    }
1233
1234    /// Validate a decoded/raw PDU as an outbound GETBULK request.
1235    pub fn try_from_raw(version: Version, pdu: Pdu) -> Result<Self> {
1236        if pdu.pdu_type() != PduType::GetBulkRequest {
1237            return Err(invalid_outbound("PDU is not GETBULK"));
1238        }
1239        pdu.validate_outbound(version, PduDirection::Request)?;
1240        Ok(Self { version, pdu })
1241    }
1242
1243    /// Encode the bare GETBULK PDU.
1244    pub fn encode(&self, buf: &mut EncodeBuf) -> Result<()> {
1245        self.pdu
1246            .encode_for(buf, self.version, PduDirection::Request)
1247    }
1248
1249    /// Return the SNMP version against which this request was validated.
1250    #[must_use]
1251    pub const fn version(&self) -> Version {
1252        self.version
1253    }
1254
1255    /// Return the request identifier.
1256    #[must_use]
1257    pub const fn request_id(&self) -> i32 {
1258        self.pdu.request_id
1259    }
1260
1261    /// Return `(non_repeaters, max_repetitions)`.
1262    #[must_use]
1263    pub fn parameters(&self) -> (u32, u32) {
1264        self.pdu
1265            .get_bulk_fields()
1266            .expect("GetBulkPdu always has GETBULK fields")
1267    }
1268
1269    /// Return the request variable bindings.
1270    #[must_use]
1271    pub fn varbinds(&self) -> &[VarBind] {
1272        &self.pdu.varbinds
1273    }
1274
1275    /// Return the decoded/raw representation used on the wire.
1276    #[must_use]
1277    pub const fn as_raw(&self) -> &Pdu {
1278        &self.pdu
1279    }
1280
1281    /// Consume this request into its decoded/raw representation.
1282    #[must_use]
1283    pub fn into_raw(self) -> Pdu {
1284        self.pdu
1285    }
1286
1287    /// Change the request identifier without affecting any other invariant.
1288    pub fn set_request_id(&mut self, request_id: i32) {
1289        self.pdu.request_id = request_id;
1290    }
1291}
1292
1293impl ResponsePdu {
1294    /// Construct a validated Response PDU.
1295    ///
1296    /// `error_index` must be `None` for noError, tooBig, undoFailed, and
1297    /// authorizationError. All other errors require a checked one-based index.
1298    pub fn new(
1299        version: Version,
1300        request_id: i32,
1301        status: OutboundErrorStatus,
1302        error_index: Option<ErrorIndex>,
1303        varbinds: Vec<VarBind>,
1304    ) -> Result<Self> {
1305        let status_value = status.as_i32();
1306        if version == Version::V1 && status_value > ErrorStatus::GenErr.as_i32() {
1307            return Err(invalid_outbound(
1308                "error status is not valid in an SNMPv1 response",
1309            ));
1310        }
1311        let requires_no_index = matches!(
1312            status,
1313            OutboundErrorStatus::NoError
1314                | OutboundErrorStatus::TooBig
1315                | OutboundErrorStatus::UndoFailed
1316                | OutboundErrorStatus::AuthorizationError
1317        );
1318        if requires_no_index != error_index.is_none() {
1319            return Err(invalid_outbound(
1320                "error status and error_index combination is invalid",
1321            ));
1322        }
1323        if let Some(index) = error_index
1324            && usize::try_from(index.get()).map_or(true, |index| index > varbinds.len())
1325        {
1326            return Err(invalid_outbound(
1327                "error_index does not identify a variable binding",
1328            ));
1329        }
1330        if version != Version::V1 && status == OutboundErrorStatus::TooBig && !varbinds.is_empty() {
1331            return Err(invalid_outbound(
1332                "SNMPv2c and SNMPv3 tooBig responses require an empty variable-binding list",
1333            ));
1334        }
1335        validate_no_receive_only_values(version, &varbinds)?;
1336        let wire_error_index = match error_index {
1337            Some(index) => i32::try_from(index.get())
1338                .map_err(|_| invalid_outbound("error_index exceeds i32::MAX"))?,
1339            None => 0,
1340        };
1341        let pdu = Pdu::response(request_id, status_value, wire_error_index, varbinds);
1342        pdu.validate_outbound(version, PduDirection::Response)?;
1343        Ok(Self { version, pdu })
1344    }
1345
1346    /// Construct a successful Response PDU.
1347    pub fn success(version: Version, request_id: i32, varbinds: Vec<VarBind>) -> Result<Self> {
1348        Self::new(
1349            version,
1350            request_id,
1351            OutboundErrorStatus::NoError,
1352            None,
1353            varbinds,
1354        )
1355    }
1356
1357    /// Construct a version-correct tooBig Response PDU.
1358    pub fn too_big(version: Version, request_id: i32, varbinds: Vec<VarBind>) -> Result<Self> {
1359        Self::new(
1360            version,
1361            request_id,
1362            OutboundErrorStatus::TooBig,
1363            None,
1364            varbinds,
1365        )
1366    }
1367
1368    /// Construct an SNMPv3 Report PDU with zero error fields.
1369    pub fn report(request_id: i32, varbinds: Vec<VarBind>) -> Result<Self> {
1370        validate_outbound_values(Version::V3, PduDirection::Response, &varbinds)?;
1371        if varbinds
1372            .iter()
1373            .any(|varbind| varbind.value == Value::Null || varbind.value.is_exception())
1374        {
1375            return Err(invalid_outbound(
1376                "Report variable bindings require concrete values",
1377            ));
1378        }
1379        let pdu = Pdu::standard(StandardPduType::Report, request_id, 0, 0, varbinds);
1380        pdu.validate_outbound(Version::V3, PduDirection::Response)?;
1381        Ok(Self {
1382            version: Version::V3,
1383            pdu,
1384        })
1385    }
1386
1387    /// Validate a decoded/raw PDU as an outbound response-class PDU.
1388    pub fn try_from_raw(version: Version, pdu: Pdu) -> Result<Self> {
1389        if !matches!(pdu.pdu_type(), PduType::Response | PduType::Report) {
1390            return Err(invalid_outbound("PDU is not response-class"));
1391        }
1392        if pdu.pdu_type() == PduType::Response {
1393            OutboundErrorStatus::try_from(pdu.error_status_enum()).map_err(|status| {
1394                invalid_outbound(format!(
1395                    "unknown error-status {} is receive-only",
1396                    status.as_i32()
1397                ))
1398            })?;
1399        }
1400        pdu.validate_outbound(version, PduDirection::Response)?;
1401        Ok(Self { version, pdu })
1402    }
1403
1404    /// Encode the bare response-class PDU.
1405    pub fn encode(&self, buf: &mut EncodeBuf) -> Result<()> {
1406        self.pdu
1407            .encode_for(buf, self.version, PduDirection::Response)
1408    }
1409
1410    /// Return the SNMP version against which this response was validated.
1411    #[must_use]
1412    pub const fn version(&self) -> Version {
1413        self.version
1414    }
1415
1416    /// Return the request identifier being answered.
1417    #[must_use]
1418    pub const fn request_id(&self) -> i32 {
1419        self.pdu.request_id
1420    }
1421
1422    /// Return the validated error status.
1423    #[must_use]
1424    pub fn status(&self) -> OutboundErrorStatus {
1425        OutboundErrorStatus::try_from(self.pdu.error_status_enum())
1426            .expect("ResponsePdu never contains an unknown error status")
1427    }
1428
1429    /// Return the checked one-based error index, if the status uses one.
1430    #[must_use]
1431    pub fn error_index(&self) -> Option<ErrorIndex> {
1432        let index = u32::try_from(self.pdu.error_index()).ok()?;
1433        let index = std::num::NonZeroU32::new(index)?;
1434        Some(ErrorIndex(index))
1435    }
1436
1437    /// Return the response variable bindings.
1438    #[must_use]
1439    pub fn varbinds(&self) -> &[VarBind] {
1440        &self.pdu.varbinds
1441    }
1442
1443    /// Return the decoded/raw representation used on the wire.
1444    #[must_use]
1445    pub const fn as_raw(&self) -> &Pdu {
1446        &self.pdu
1447    }
1448
1449    /// Consume this response into its decoded/raw representation.
1450    #[must_use]
1451    pub fn into_raw(self) -> Pdu {
1452        self.pdu
1453    }
1454
1455    /// Change the request identifier without affecting any other invariant.
1456    pub fn set_request_id(&mut self, request_id: i32) {
1457        self.pdu.request_id = request_id;
1458    }
1459}
1460
1461impl NotificationPdu {
1462    fn new(
1463        version: Version,
1464        kind: StandardPduType,
1465        request_id: i32,
1466        uptime: u32,
1467        trap_oid: &Oid,
1468        varbinds: Vec<VarBind>,
1469    ) -> Result<Self> {
1470        if version == Version::V1 {
1471            return Err(invalid_outbound(
1472                "SNMPv2 notification PDUs are not valid in SNMPv1",
1473            ));
1474        }
1475        if !matches!(
1476            kind,
1477            StandardPduType::TrapV2 | StandardPduType::InformRequest
1478        ) {
1479            return Err(invalid_outbound("PDU type is not a notification"));
1480        }
1481        validate_outbound_values(version, PduDirection::Notification, &varbinds)?;
1482        if varbinds
1483            .iter()
1484            .any(|varbind| varbind.value == Value::Null || varbind.value.is_exception())
1485        {
1486            return Err(invalid_outbound(
1487                "notification variable bindings require concrete values",
1488            ));
1489        }
1490        let pdu = match kind {
1491            StandardPduType::TrapV2 => Pdu::trap_v2(request_id, uptime, trap_oid, varbinds),
1492            StandardPduType::InformRequest => {
1493                Pdu::inform_request(request_id, uptime, trap_oid, varbinds)
1494            }
1495            _ => unreachable!("notification kind checked above"),
1496        };
1497        let direction = pdu.outbound_direction();
1498        pdu.validate_outbound(version, direction)?;
1499        Ok(Self { version, pdu })
1500    }
1501
1502    /// Construct an unconfirmed SNMPv2-Trap PDU.
1503    pub fn trap_v2(
1504        version: Version,
1505        request_id: i32,
1506        uptime: u32,
1507        trap_oid: &Oid,
1508        varbinds: Vec<VarBind>,
1509    ) -> Result<Self> {
1510        Self::new(
1511            version,
1512            StandardPduType::TrapV2,
1513            request_id,
1514            uptime,
1515            trap_oid,
1516            varbinds,
1517        )
1518    }
1519
1520    /// Construct a confirmed InformRequest PDU.
1521    pub fn inform(
1522        version: Version,
1523        request_id: i32,
1524        uptime: u32,
1525        trap_oid: &Oid,
1526        varbinds: Vec<VarBind>,
1527    ) -> Result<Self> {
1528        Self::new(
1529            version,
1530            StandardPduType::InformRequest,
1531            request_id,
1532            uptime,
1533            trap_oid,
1534            varbinds,
1535        )
1536    }
1537
1538    /// Validate a decoded/raw PDU as an outbound v2 notification PDU.
1539    pub fn try_from_raw(version: Version, pdu: Pdu) -> Result<Self> {
1540        if !matches!(pdu.pdu_type(), PduType::TrapV2 | PduType::InformRequest) {
1541            return Err(invalid_outbound("PDU is not an SNMPv2 notification"));
1542        }
1543        let direction = pdu.outbound_direction();
1544        pdu.validate_outbound(version, direction)?;
1545        Ok(Self { version, pdu })
1546    }
1547
1548    /// Encode the bare notification PDU.
1549    pub fn encode(&self, buf: &mut EncodeBuf) -> Result<()> {
1550        self.pdu
1551            .encode_for(buf, self.version, self.pdu.outbound_direction())
1552    }
1553
1554    /// Return the SNMP version against which this notification was validated.
1555    #[must_use]
1556    pub const fn version(&self) -> Version {
1557        self.version
1558    }
1559
1560    /// Return the request identifier.
1561    #[must_use]
1562    pub const fn request_id(&self) -> i32 {
1563        self.pdu.request_id
1564    }
1565
1566    /// Return whether this is an InformRequest or unconfirmed TrapV2.
1567    #[must_use]
1568    pub fn pdu_type(&self) -> PduType {
1569        self.pdu.pdu_type()
1570    }
1571
1572    /// Return the complete variable-binding list, including the mandatory prefix.
1573    #[must_use]
1574    pub fn varbinds(&self) -> &[VarBind] {
1575        &self.pdu.varbinds
1576    }
1577
1578    /// Return the notification uptime from the mandatory prefix.
1579    #[must_use]
1580    pub fn uptime(&self) -> u32 {
1581        match self.pdu.varbinds.first().map(|varbind| &varbind.value) {
1582            Some(Value::TimeTicks(value)) => *value,
1583            _ => unreachable!("NotificationPdu always has a TimeTicks prefix"),
1584        }
1585    }
1586
1587    /// Return the trap OID from the mandatory prefix.
1588    #[must_use]
1589    pub fn trap_oid(&self) -> &Oid {
1590        match self.pdu.varbinds.get(1).map(|varbind| &varbind.value) {
1591            Some(Value::ObjectIdentifier(oid)) => oid,
1592            _ => unreachable!("NotificationPdu always has an OBJECT IDENTIFIER prefix"),
1593        }
1594    }
1595
1596    /// Convert this v2/v3 notification to a validated SNMPv1 Trap.
1597    ///
1598    /// Implements RFC 3584 Section 3.2. `default_addr` is used when the
1599    /// notification has no `snmpTrapAddress.0` varbind.
1600    ///
1601    /// # Errors
1602    ///
1603    /// Returns an error when the notification contains a `Counter64`, its trap
1604    /// OID cannot be represented by the v1 fields, or the derived enterprise
1605    /// and copied varbinds do not form an encodable SNMPv1 Trap.
1606    pub fn to_v1_trap(&self, default_addr: [u8; 4]) -> Result<TrapV1Notification> {
1607        use crate::notification::oids;
1608
1609        if self
1610            .pdu
1611            .varbinds
1612            .iter()
1613            .any(|varbind| matches!(varbind.value, Value::Counter64(_)))
1614        {
1615            return Err(invalid_outbound(
1616                "Counter64 notification values cannot be represented in SNMPv1",
1617            ));
1618        }
1619
1620        let trap_oid = self.trap_oid();
1621        let snmp_traps_prefix = oids::snmp_traps();
1622        let (generic_trap, specific_trap, enterprise) = if trap_oid.starts_with(&snmp_traps_prefix)
1623            && trap_oid.len() == snmp_traps_prefix.len() + 1
1624            && (1..=6).contains(&trap_oid.arcs()[trap_oid.len() - 1])
1625        {
1626            let last_arc = trap_oid.arcs()[trap_oid.len() - 1];
1627            let enterprise = self.pdu.varbinds[2..]
1628                .iter()
1629                .find(|varbind| varbind.oid == oids::snmp_trap_enterprise())
1630                .and_then(|varbind| match &varbind.value {
1631                    Value::ObjectIdentifier(oid) => Some(oid.clone()),
1632                    _ => None,
1633                })
1634                .unwrap_or_else(|| snmp_traps_prefix.clone());
1635            (GenericTrap::from_i32((last_arc - 1) as i32), 0, enterprise)
1636        } else if trap_oid.len() >= 2 {
1637            let arcs = trap_oid.arcs();
1638            let specific_trap = i32::try_from(arcs[arcs.len() - 1])
1639                .map_err(|_| invalid_outbound("trap OID specific value exceeds Integer32"))?;
1640            let enterprise = if arcs[arcs.len() - 2] == 0 {
1641                Oid::from_slice(&arcs[..arcs.len() - 2])
1642            } else {
1643                Oid::from_slice(&arcs[..arcs.len() - 1])
1644            };
1645            (GenericTrap::EnterpriseSpecific, specific_trap, enterprise)
1646        } else {
1647            return Err(invalid_outbound(
1648                "trap OID is too short for SNMPv1 conversion",
1649            ));
1650        };
1651
1652        let agent_addr = self.pdu.varbinds[2..]
1653            .iter()
1654            .find(|varbind| varbind.oid == oids::snmp_trap_address())
1655            .and_then(|varbind| match varbind.value {
1656                Value::IpAddress(address) => Some(address),
1657                _ => None,
1658            })
1659            .unwrap_or(default_addr);
1660
1661        TrapV1Notification::new(
1662            enterprise,
1663            agent_addr,
1664            generic_trap,
1665            specific_trap,
1666            self.uptime(),
1667            self.pdu.varbinds[2..].to_vec(),
1668        )
1669    }
1670
1671    /// Return the decoded/raw representation used on the wire.
1672    #[must_use]
1673    pub const fn as_raw(&self) -> &Pdu {
1674        &self.pdu
1675    }
1676
1677    /// Consume this notification into its decoded/raw representation.
1678    #[must_use]
1679    pub fn into_raw(self) -> Pdu {
1680        self.pdu
1681    }
1682
1683    /// Change the request identifier without affecting any other invariant.
1684    pub fn set_request_id(&mut self, request_id: i32) {
1685        self.pdu.request_id = request_id;
1686    }
1687}
1688
1689impl From<RequestPdu> for Pdu {
1690    fn from(value: RequestPdu) -> Self {
1691        value.into_raw()
1692    }
1693}
1694
1695impl From<GetBulkPdu> for Pdu {
1696    fn from(value: GetBulkPdu) -> Self {
1697        value.into_raw()
1698    }
1699}
1700
1701impl From<ResponsePdu> for Pdu {
1702    fn from(value: ResponsePdu) -> Self {
1703        value.into_raw()
1704    }
1705}
1706
1707impl From<NotificationPdu> for Pdu {
1708    fn from(value: NotificationPdu) -> Self {
1709        value.into_raw()
1710    }
1711}
1712
1713impl OutboundPdu {
1714    /// Validate and classify a decoded/raw PDU for outbound use.
1715    pub fn try_from_raw(version: Version, pdu: Pdu) -> Result<Self> {
1716        match pdu.pdu_type() {
1717            PduType::GetRequest | PduType::GetNextRequest | PduType::SetRequest => {
1718                Ok(Self::Request(RequestPdu::try_from_raw(version, pdu)?))
1719            }
1720            PduType::GetBulkRequest => Ok(Self::GetBulk(GetBulkPdu::try_from_raw(version, pdu)?)),
1721            PduType::Response | PduType::Report => {
1722                Ok(Self::Response(ResponsePdu::try_from_raw(version, pdu)?))
1723            }
1724            PduType::InformRequest | PduType::TrapV2 => Ok(Self::Notification(
1725                NotificationPdu::try_from_raw(version, pdu)?,
1726            )),
1727            PduType::TrapV1 => Err(invalid_outbound("SNMPv1 Trap uses TrapV1Notification")),
1728        }
1729    }
1730
1731    /// Return the decoded/raw representation used on the wire.
1732    #[must_use]
1733    pub fn as_raw(&self) -> &Pdu {
1734        match self {
1735            Self::Request(pdu) => pdu.as_raw(),
1736            Self::GetBulk(pdu) => pdu.as_raw(),
1737            Self::Response(pdu) => pdu.as_raw(),
1738            Self::Notification(pdu) => pdu.as_raw(),
1739        }
1740    }
1741
1742    /// Consume the validated PDU into its decoded/raw representation.
1743    #[must_use]
1744    pub fn into_raw(self) -> Pdu {
1745        match self {
1746            Self::Request(pdu) => pdu.into_raw(),
1747            Self::GetBulk(pdu) => pdu.into_raw(),
1748            Self::Response(pdu) => pdu.into_raw(),
1749            Self::Notification(pdu) => pdu.into_raw(),
1750        }
1751    }
1752
1753    /// Encode the bare validated PDU.
1754    pub fn encode(&self, buf: &mut EncodeBuf) -> Result<()> {
1755        match self {
1756            Self::Request(pdu) => pdu.encode(buf),
1757            Self::GetBulk(pdu) => pdu.encode(buf),
1758            Self::Response(pdu) => pdu.encode(buf),
1759            Self::Notification(pdu) => pdu.encode(buf),
1760        }
1761    }
1762}
1763
1764impl From<OutboundPdu> for Pdu {
1765    fn from(value: OutboundPdu) -> Self {
1766        value.into_raw()
1767    }
1768}
1769
1770/// `SNMPv1` generic trap types (RFC 1157 Section 4.1.6).
1771#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1772pub enum GenericTrap {
1773    /// coldStart(0) - agent is reinitializing, config may change
1774    ColdStart,
1775    /// warmStart(1) - agent is reinitializing, config unchanged
1776    WarmStart,
1777    /// linkDown(2) - communication link failure
1778    LinkDown,
1779    /// linkUp(3) - communication link came up
1780    LinkUp,
1781    /// authenticationFailure(4) - improperly authenticated message received
1782    AuthenticationFailure,
1783    /// egpNeighborLoss(5) - EGP peer marked down
1784    EgpNeighborLoss,
1785    /// enterpriseSpecific(6) - vendor-specific trap, see `specific_trap` field
1786    EnterpriseSpecific,
1787    /// An unrecognized generic trap value received on the wire.
1788    Unknown(i32),
1789}
1790
1791impl std::fmt::Display for GenericTrap {
1792    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1793        match self {
1794            Self::ColdStart => write!(f, "coldStart"),
1795            Self::WarmStart => write!(f, "warmStart"),
1796            Self::LinkDown => write!(f, "linkDown"),
1797            Self::LinkUp => write!(f, "linkUp"),
1798            Self::AuthenticationFailure => write!(f, "authenticationFailure"),
1799            Self::EgpNeighborLoss => write!(f, "egpNeighborLoss"),
1800            Self::EnterpriseSpecific => write!(f, "enterpriseSpecific"),
1801            Self::Unknown(v) => write!(f, "unknown({v})"),
1802        }
1803    }
1804}
1805
1806impl GenericTrap {
1807    /// Create from integer value.
1808    #[must_use]
1809    pub fn from_i32(v: i32) -> Self {
1810        match v {
1811            0 => Self::ColdStart,
1812            1 => Self::WarmStart,
1813            2 => Self::LinkDown,
1814            3 => Self::LinkUp,
1815            4 => Self::AuthenticationFailure,
1816            5 => Self::EgpNeighborLoss,
1817            6 => Self::EnterpriseSpecific,
1818            _ => Self::Unknown(v),
1819        }
1820    }
1821
1822    /// Returns the integer value.
1823    #[must_use]
1824    pub fn as_i32(self) -> i32 {
1825        match self {
1826            Self::ColdStart => 0,
1827            Self::WarmStart => 1,
1828            Self::LinkDown => 2,
1829            Self::LinkUp => 3,
1830            Self::AuthenticationFailure => 4,
1831            Self::EgpNeighborLoss => 5,
1832            Self::EnterpriseSpecific => 6,
1833            Self::Unknown(v) => v,
1834        }
1835    }
1836}
1837
1838/// `SNMPv1` Trap PDU (RFC 1157 Section 4.1.6).
1839///
1840/// This PDU type has a completely different structure from other PDUs.
1841/// It is only used in `SNMPv1` and is replaced by SNMPv2-Trap in v2c/v3.
1842#[derive(Debug, Clone, PartialEq, Eq)]
1843pub struct TrapV1Pdu {
1844    /// Enterprise OID (sysObjectID of the entity generating the trap)
1845    pub(crate) enterprise: Oid,
1846    /// Agent address (IP address of the agent generating the trap)
1847    pub(crate) agent_addr: [u8; 4],
1848    /// Generic trap type
1849    pub(crate) generic_trap: GenericTrap,
1850    /// Specific trap code (meaningful when `generic_trap` is enterpriseSpecific)
1851    pub(crate) specific_trap: i32,
1852    /// Time since the network entity was last (re)initialized (in hundredths of seconds)
1853    pub(crate) time_stamp: u32,
1854    /// Variable bindings containing "interesting" information
1855    pub(crate) varbinds: Vec<VarBind>,
1856}
1857
1858impl TrapV1Pdu {
1859    /// Construct a permissive raw SNMPv1 Trap without outbound validation.
1860    #[must_use]
1861    pub fn from_raw_parts(
1862        enterprise: Oid,
1863        agent_addr: [u8; 4],
1864        generic_trap: GenericTrap,
1865        specific_trap: i32,
1866        time_stamp: u32,
1867        varbinds: Vec<VarBind>,
1868    ) -> Self {
1869        Self::new(
1870            enterprise,
1871            agent_addr,
1872            generic_trap,
1873            specific_trap,
1874            time_stamp,
1875            varbinds,
1876        )
1877    }
1878
1879    /// Create an SNMPv1 Trap PDU.
1880    #[must_use]
1881    pub(crate) fn new(
1882        enterprise: Oid,
1883        agent_addr: [u8; 4],
1884        generic_trap: GenericTrap,
1885        specific_trap: i32,
1886        time_stamp: u32,
1887        varbinds: Vec<VarBind>,
1888    ) -> Self {
1889        Self {
1890            enterprise,
1891            agent_addr,
1892            generic_trap,
1893            specific_trap,
1894            time_stamp,
1895            varbinds,
1896        }
1897    }
1898
1899    /// Return the enterprise OID.
1900    #[must_use]
1901    pub fn enterprise(&self) -> &Oid {
1902        &self.enterprise
1903    }
1904
1905    /// Return the originating agent IPv4 address.
1906    #[must_use]
1907    pub const fn agent_addr(&self) -> [u8; 4] {
1908        self.agent_addr
1909    }
1910
1911    /// Return the decoded generic-trap value.
1912    #[must_use]
1913    pub const fn generic_trap(&self) -> GenericTrap {
1914        self.generic_trap
1915    }
1916
1917    /// Return the decoded specific-trap value.
1918    #[must_use]
1919    pub const fn specific_trap(&self) -> i32 {
1920        self.specific_trap
1921    }
1922
1923    /// Return the trap timestamp.
1924    #[must_use]
1925    pub const fn time_stamp(&self) -> u32 {
1926        self.time_stamp
1927    }
1928
1929    /// Return the trap variable bindings.
1930    #[must_use]
1931    pub fn varbinds(&self) -> &[VarBind] {
1932        &self.varbinds
1933    }
1934
1935    /// Check if this is an enterprise-specific trap.
1936    #[must_use]
1937    pub fn is_enterprise_specific(&self) -> bool {
1938        self.generic_trap == GenericTrap::EnterpriseSpecific
1939    }
1940
1941    /// Convert to `SNMPv2` trap OID (RFC 3584 Section 3).
1942    ///
1943    /// RFC 3584 defines how to translate `SNMPv1` trap information to `SNMPv2`
1944    /// snmpTrapOID.0 format:
1945    ///
1946    /// - For generic traps 0-5 (coldStart through egpNeighborLoss):
1947    ///   The trap OID is `snmpTraps.{generic_trap + 1}` (1.3.6.1.6.3.1.1.5.{1-6})
1948    ///
1949    /// - For enterprise-specific traps (`generic_trap` = 6):
1950    ///   The trap OID is `enterprise.0.specific_trap`
1951    ///
1952    /// Received nonnegative unknown generic-trap values use the arithmetic
1953    /// `snmpTraps.{generic_trap + 1}` extension implemented by net-snmp, even
1954    /// though RFC 3584 defines the standard mapping only for values 0 through
1955    /// 6. This compatibility applies only to decoded/receive-side conversion;
1956    /// [`TrapV1Notification::new`] and structured encoding reject unknown
1957    /// generic-trap values for direct origination.
1958    ///
1959    /// # Errors
1960    ///
1961    /// Returns [`Error::InvalidOid`] if:
1962    /// - `generic_trap` is `Unknown` with a negative value (undefined per RFC 1157)
1963    /// - `generic_trap` is `Unknown` with value `i32::MAX` (would overflow when adding 1)
1964    /// - `specific_trap < 0` for enterprise-specific traps (OID arcs must be non-negative)
1965    /// - the complete synthesized OID violates outbound OID constraints
1966    ///
1967    /// # Example
1968    ///
1969    /// ```rust
1970    /// use async_snmp::pdu::{TrapV1Pdu, GenericTrap};
1971    /// use async_snmp::oid;
1972    ///
1973    /// // Generic trap (linkDown = 2) -> snmpTraps.3
1974    /// let trap = TrapV1Pdu::from_raw_parts(
1975    ///     oid!(1, 3, 6, 1, 4, 1, 9999),
1976    ///     [192, 168, 1, 1],
1977    ///     GenericTrap::LinkDown,
1978    ///     0,
1979    ///     12345,
1980    ///     vec![],
1981    /// );
1982    /// assert_eq!(trap.v2_trap_oid().unwrap(), oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3));
1983    ///
1984    /// // Enterprise-specific trap -> enterprise.0.specific_trap
1985    /// let trap = TrapV1Pdu::from_raw_parts(
1986    ///     oid!(1, 3, 6, 1, 4, 1, 9999),
1987    ///     [192, 168, 1, 1],
1988    ///     GenericTrap::EnterpriseSpecific,
1989    ///     42,
1990    ///     12345,
1991    ///     vec![],
1992    /// );
1993    /// assert_eq!(trap.v2_trap_oid().unwrap(), oid!(1, 3, 6, 1, 4, 1, 9999, 0, 42));
1994    /// ```
1995    pub fn v2_trap_oid(&self) -> crate::Result<Oid> {
1996        if self.is_enterprise_specific() {
1997            if self.specific_trap < 0 {
1998                return Err(Error::InvalidOid("specific_trap cannot be negative".into()).boxed());
1999            }
2000            let mut arcs: Vec<u32> = self.enterprise.arcs().to_vec();
2001            arcs.push(0);
2002            arcs.push(self.specific_trap as u32);
2003            let oid = Oid::new(arcs);
2004            oid.validate_for_wire()?;
2005            Ok(oid)
2006        } else {
2007            let raw = self.generic_trap.as_i32();
2008            if raw < 0 {
2009                return Err(Error::InvalidOid("generic_trap cannot be negative".into()).boxed());
2010            }
2011            if raw == i32::MAX {
2012                return Err(Error::InvalidOid("generic_trap overflow".into()).boxed());
2013            }
2014            let trap_num = raw + 1;
2015            Ok(crate::oid!(1, 3, 6, 1, 6, 3, 1, 1, 5).child(trap_num as u32))
2016        }
2017    }
2018
2019    /// Convert to a v2 notification PDU (RFC 3584 Section 3.1).
2020    ///
2021    /// Performs the originator (non-proxy) conversion: only the mandatory
2022    /// sysUpTime.0 and snmpTrapOID.0 prefix followed by the original varbinds.
2023    /// Per RFC 3584 Section 3.1(4), the additional proxy varbinds
2024    /// (snmpTrapAddress.0, snmpTrapCommunity.0, snmpTrapEnterprise.0) are only
2025    /// appended when a proxy forwards a received trap.
2026    ///
2027    /// The `request_id` is set to 0. Assign an application-specific request ID
2028    /// before sending the PDU.
2029    ///
2030    /// # Errors
2031    ///
2032    /// Returns an error if the trap OID cannot be computed (see
2033    /// [`Self::v2_trap_oid`]) or if any copied variable binding cannot be
2034    /// encoded in an outbound SNMPv2 notification.
2035    pub fn to_v2_pdu(&self) -> crate::Result<NotificationPdu> {
2036        let trap_oid = self.v2_trap_oid()?;
2037        NotificationPdu::trap_v2(
2038            Version::V2c,
2039            0,
2040            self.time_stamp,
2041            &trap_oid,
2042            self.varbinds.clone(),
2043        )
2044    }
2045
2046    pub(crate) fn validate_outbound(&self) -> Result<()> {
2047        self.enterprise.validate_for_wire()?;
2048        if matches!(self.generic_trap, GenericTrap::Unknown(_)) {
2049            return Err(invalid_outbound(
2050                "unknown generic-trap values are receive-only",
2051            ));
2052        }
2053        validate_outbound_values(Version::V1, PduDirection::Notification, &self.varbinds)
2054    }
2055
2056    /// Encode to BER after applying outbound validation.
2057    pub(crate) fn encode(&self, buf: &mut EncodeBuf) -> Result<()> {
2058        self.validate_outbound()?;
2059        buf.push_constructed(tag::pdu::TRAP_V1, |buf| {
2060            encode_varbind_list(buf, &self.varbinds)?;
2061            buf.push_unsigned32(tag::application::TIMETICKS, self.time_stamp);
2062            buf.push_integer(self.specific_trap);
2063            buf.push_integer(self.generic_trap.as_i32());
2064            // NetworkAddress is APPLICATION 0 IMPLICIT IpAddress
2065            // IpAddress is APPLICATION 0 IMPLICIT OCTET STRING (SIZE (4))
2066            buf.push_bytes(&self.agent_addr);
2067            buf.push_length(4)?;
2068            buf.push_tag(tag::application::IP_ADDRESS);
2069            buf.push_oid(&self.enterprise)
2070        })
2071    }
2072
2073    /// Decode from BER (after tag has been peeked).
2074    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
2075        let mut pdu = decoder.read_constructed(tag::pdu::TRAP_V1)?;
2076
2077        // enterprise OBJECT IDENTIFIER
2078        let enterprise = pdu.read_oid()?;
2079
2080        // agent-addr NetworkAddress (IpAddress)
2081        let agent_tag = pdu.read_tag()?;
2082        if agent_tag != tag::application::IP_ADDRESS {
2083            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), expected = 0x40_u8, actual = agent_tag, kind = %DecodeErrorKind::UnexpectedTag {
2084                    expected: 0x40,
2085                    actual: agent_tag,
2086                } }, "decode error");
2087            return Err(pdu.malformed_at(
2088                pdu.local_offset() - 1,
2089                DecodeErrorKind::UnexpectedTag {
2090                    expected: tag::application::IP_ADDRESS,
2091                    actual: agent_tag,
2092                },
2093            ));
2094        }
2095        let agent_len = pdu.read_length()?;
2096        if agent_len != 4 {
2097            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), length = agent_len, kind = %DecodeErrorKind::InvalidIpAddressLength { length: agent_len } }, "decode error");
2098            return Err(
2099                pdu.malformed(DecodeErrorKind::InvalidIpAddressLength { length: agent_len })
2100            );
2101        }
2102        let agent_bytes = pdu.read_bytes(4)?;
2103        let agent_addr = [
2104            agent_bytes[0],
2105            agent_bytes[1],
2106            agent_bytes[2],
2107            agent_bytes[3],
2108        ];
2109
2110        // generic-trap and specific-trap are protocol Integer32 fields. Do not
2111        // apply the generic value-truncation compatibility behavior here.
2112        let generic_trap = GenericTrap::from_i32(pdu.read_bounded_integer(i32::MIN, i32::MAX)?);
2113        let specific_trap = pdu.read_bounded_integer(i32::MIN, i32::MAX)?;
2114
2115        // time-stamp TimeTicks
2116        let ts_tag = pdu.read_tag()?;
2117        if ts_tag != tag::application::TIMETICKS {
2118            tracing::debug!(target: "async_snmp::pdu", { offset = pdu.offset(), expected = 0x43_u8, actual = ts_tag, kind = %DecodeErrorKind::UnexpectedTag {
2119                    expected: 0x43,
2120                    actual: ts_tag,
2121                } }, "decode error");
2122            return Err(pdu.malformed_at(
2123                pdu.local_offset() - 1,
2124                DecodeErrorKind::UnexpectedTag {
2125                    expected: tag::application::TIMETICKS,
2126                    actual: ts_tag,
2127                },
2128            ));
2129        }
2130        let ts_len = pdu.read_length()?;
2131        let time_stamp = pdu.read_bounded_unsigned32_value(ts_len)?;
2132
2133        // variable-bindings
2134        let varbinds = decode_varbind_list(&mut pdu)?;
2135        if !pdu.is_empty() {
2136            return Err(pdu.malformed(DecodeErrorKind::TrailingData {
2137                remaining: pdu.remaining(),
2138            }));
2139        }
2140
2141        Ok(TrapV1Pdu {
2142            enterprise,
2143            agent_addr,
2144            generic_trap,
2145            specific_trap,
2146            time_stamp,
2147            varbinds,
2148        })
2149    }
2150}
2151
2152impl TrapV1Notification {
2153    /// Construct a validated outbound SNMPv1 Trap PDU.
2154    ///
2155    /// # Errors
2156    ///
2157    /// Unknown generic-trap values, receive-only values, values unavailable in
2158    /// SNMPv1, and invalid OIDs are rejected. `specific_trap` is encoded as the
2159    /// full protocol `Integer32` field without imposing semantic constraints
2160    /// that receivers such as net-snmp do not enforce.
2161    pub fn new(
2162        enterprise: Oid,
2163        agent_addr: [u8; 4],
2164        generic_trap: GenericTrap,
2165        specific_trap: i32,
2166        time_stamp: u32,
2167        varbinds: Vec<VarBind>,
2168    ) -> Result<Self> {
2169        if matches!(generic_trap, GenericTrap::Unknown(_)) {
2170            return Err(invalid_outbound(
2171                "unknown generic-trap values are receive-only",
2172            ));
2173        }
2174        let pdu = TrapV1Pdu::new(
2175            enterprise,
2176            agent_addr,
2177            generic_trap,
2178            specific_trap,
2179            time_stamp,
2180            varbinds,
2181        );
2182        pdu.validate_outbound()?;
2183        Ok(Self { pdu })
2184    }
2185
2186    /// Validate a decoded/raw SNMPv1 trap for outbound use.
2187    pub fn try_from_raw(pdu: TrapV1Pdu) -> Result<Self> {
2188        pdu.validate_outbound()?;
2189        Ok(Self { pdu })
2190    }
2191
2192    /// Encode the bare SNMPv1 Trap PDU.
2193    pub fn encode(&self, buf: &mut EncodeBuf) -> Result<()> {
2194        self.pdu.encode(buf)
2195    }
2196
2197    /// Return the enterprise OID.
2198    #[must_use]
2199    pub fn enterprise(&self) -> &Oid {
2200        &self.pdu.enterprise
2201    }
2202
2203    /// Return the originating agent IPv4 address.
2204    #[must_use]
2205    pub const fn agent_addr(&self) -> [u8; 4] {
2206        self.pdu.agent_addr
2207    }
2208
2209    /// Return the generic trap classification.
2210    #[must_use]
2211    pub const fn generic_trap(&self) -> GenericTrap {
2212        self.pdu.generic_trap
2213    }
2214
2215    /// Return the specific trap `Integer32` value.
2216    #[must_use]
2217    pub const fn specific_trap(&self) -> i32 {
2218        self.pdu.specific_trap
2219    }
2220
2221    /// Return the timestamp in hundredths of a second.
2222    #[must_use]
2223    pub const fn time_stamp(&self) -> u32 {
2224        self.pdu.time_stamp
2225    }
2226
2227    /// Return the trap variable bindings.
2228    #[must_use]
2229    pub fn varbinds(&self) -> &[VarBind] {
2230        &self.pdu.varbinds
2231    }
2232
2233    /// Return the decoded/raw representation used on the wire.
2234    #[must_use]
2235    pub const fn as_raw(&self) -> &TrapV1Pdu {
2236        &self.pdu
2237    }
2238
2239    /// Consume this notification into its decoded/raw representation.
2240    #[must_use]
2241    pub fn into_raw(self) -> TrapV1Pdu {
2242        self.pdu
2243    }
2244}
2245
2246impl From<TrapV1Notification> for TrapV1Pdu {
2247    fn from(value: TrapV1Notification) -> Self {
2248        value.into_raw()
2249    }
2250}
2251
2252#[cfg(test)]
2253mod tests {
2254    use super::*;
2255    use crate::DecodeConfig;
2256    use crate::oid;
2257
2258    fn compatibility_without_negative_bulk_normalization() -> DecodeConfig {
2259        DecodeConfig {
2260            normalize_negative_get_bulk_fields: false,
2261            ..DecodeConfig::DEFAULT
2262        }
2263    }
2264
2265    fn to_v1_trap(pdu: &Pdu, address: [u8; 4]) -> Result<TrapV1Pdu> {
2266        Ok(NotificationPdu::try_from_raw(Version::V2c, pdu.clone())?
2267            .to_v1_trap(address)?
2268            .into_raw())
2269    }
2270
2271    /// Test helper for encoding PDUs with arbitrary field values.
2272    ///
2273    /// Unlike `Pdu`, this allows encoding invalid values (negative `error_index`,
2274    /// out-of-bounds indices, etc.) for testing decoder validation.
2275    struct RawPdu {
2276        pdu_type: u8,
2277        request_id: i32,
2278        error_status: i32,
2279        error_index: i32,
2280        varbinds: Vec<VarBind>,
2281    }
2282
2283    impl RawPdu {
2284        fn response(
2285            request_id: i32,
2286            error_status: i32,
2287            error_index: i32,
2288            varbinds: Vec<VarBind>,
2289        ) -> Self {
2290            Self {
2291                pdu_type: PduType::Response.tag(),
2292                request_id,
2293                error_status,
2294                error_index,
2295                varbinds,
2296            }
2297        }
2298
2299        fn encode(&self) -> bytes::Bytes {
2300            let mut buf = EncodeBuf::new();
2301            buf.push_constructed(self.pdu_type, |buf| {
2302                encode_varbind_list(buf, &self.varbinds).unwrap();
2303                buf.push_integer(self.error_index);
2304                buf.push_integer(self.error_status);
2305                buf.push_integer(self.request_id);
2306                Ok(())
2307            })
2308            .unwrap();
2309            buf.finish()
2310        }
2311    }
2312
2313    /// Test helper for encoding GETBULK PDUs with arbitrary field values.
2314    struct RawBulkWirePdu {
2315        request_id: i32,
2316        non_repeaters: i32,
2317        max_repetitions: i32,
2318        varbinds: Vec<VarBind>,
2319    }
2320
2321    impl RawBulkWirePdu {
2322        fn new(
2323            request_id: i32,
2324            non_repeaters: i32,
2325            max_repetitions: i32,
2326            varbinds: Vec<VarBind>,
2327        ) -> Self {
2328            Self {
2329                request_id,
2330                non_repeaters,
2331                max_repetitions,
2332                varbinds,
2333            }
2334        }
2335
2336        fn encode(&self) -> bytes::Bytes {
2337            let mut buf = EncodeBuf::new();
2338            buf.push_constructed(tag::pdu::GET_BULK_REQUEST, |buf| {
2339                encode_varbind_list(buf, &self.varbinds).unwrap();
2340                buf.push_integer(self.max_repetitions);
2341                buf.push_integer(self.non_repeaters);
2342                buf.push_integer(self.request_id);
2343                Ok(())
2344            })
2345            .unwrap();
2346            buf.finish()
2347        }
2348    }
2349
2350    #[test]
2351    fn validated_requests_cover_versions_types_accessors_and_raw_conversion() {
2352        let name = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
2353        for version in [Version::V1, Version::V2c, Version::V3] {
2354            for mut request in [
2355                RequestPdu::get(version, 7, std::slice::from_ref(&name)).unwrap(),
2356                RequestPdu::get_next(version, 7, std::slice::from_ref(&name)).unwrap(),
2357                RequestPdu::set(
2358                    version,
2359                    7,
2360                    vec![VarBind::new(name.clone(), Value::Integer(4))],
2361                )
2362                .unwrap(),
2363            ] {
2364                assert_eq!(request.version(), version);
2365                assert_eq!(request.request_id(), 7);
2366                assert_eq!(request.varbinds().len(), 1);
2367                request.set_request_id(9);
2368                assert_eq!(request.request_id(), 9);
2369
2370                let raw = request.into_raw();
2371                let validated = RequestPdu::try_from_raw(version, raw.clone()).unwrap();
2372                assert_eq!(validated.as_raw(), &raw);
2373
2374                let mut buf = EncodeBuf::new();
2375                validated.encode(&mut buf).unwrap();
2376                let mut decoder = Decoder::new(buf.finish());
2377                assert_eq!(Pdu::decode(&mut decoder).unwrap(), raw);
2378            }
2379
2380            // RFC 3416 says retrieval-request values are ignored by the
2381            // receiver. Preserve any otherwise outbound-encodable value when
2382            // decoded data is deliberately validated for retransmission.
2383            for kind in [StandardPduType::GetRequest, StandardPduType::GetNextRequest] {
2384                let raw = Pdu::standard(
2385                    kind,
2386                    11,
2387                    0,
2388                    0,
2389                    vec![VarBind::new(name.clone(), Value::Integer(37))],
2390                );
2391                let request = RequestPdu::try_from_raw(version, raw.clone()).unwrap();
2392                let mut buf = EncodeBuf::new();
2393                request.encode(&mut buf).unwrap();
2394                let mut decoder = Decoder::new(buf.finish());
2395                assert_eq!(Pdu::decode(&mut decoder).unwrap(), raw);
2396            }
2397        }
2398
2399        assert!(RequestPdu::set(Version::V2c, 1, vec![VarBind::null(name.clone())]).is_err());
2400        assert!(
2401            RequestPdu::set(
2402                Version::V2c,
2403                1,
2404                vec![VarBind::new(name.clone(), Value::NoSuchObject)]
2405            )
2406            .is_err()
2407        );
2408        assert!(
2409            RequestPdu::set(
2410                Version::V1,
2411                1,
2412                vec![VarBind::new(name, Value::Counter64(1))]
2413            )
2414            .is_err()
2415        );
2416    }
2417
2418    #[test]
2419    fn validated_get_bulk_checks_version_ranges_values_and_mutation() {
2420        let name = oid!(1, 3, 6, 1);
2421        assert!(GetBulkPdu::new(Version::V1, 1, 0, 1, vec![VarBind::null(name.clone())]).is_err());
2422        assert!(
2423            GetBulkPdu::new(
2424                Version::V2c,
2425                1,
2426                MAX_GET_BULK_VALUE + 1,
2427                1,
2428                vec![VarBind::null(name.clone())]
2429            )
2430            .is_err()
2431        );
2432        for version in [Version::V2c, Version::V3] {
2433            let bulk = GetBulkPdu::new(
2434                version,
2435                1,
2436                0,
2437                1,
2438                vec![VarBind::new(name.clone(), Value::Integer(1))],
2439            )
2440            .unwrap();
2441            let expected = bulk.as_raw().clone();
2442            let mut buf = EncodeBuf::new();
2443            bulk.encode(&mut buf).unwrap();
2444            let mut decoder = Decoder::new(buf.finish());
2445            assert_eq!(Pdu::decode(&mut decoder).unwrap(), expected);
2446        }
2447
2448        let mut bulk = GetBulkPdu::new(Version::V3, 3, 1, 12, vec![VarBind::null(name)]).unwrap();
2449        assert_eq!(bulk.parameters(), (1, 12));
2450        assert_eq!(bulk.request_id(), 3);
2451        assert_eq!(bulk.varbinds().len(), 1);
2452        bulk.set_request_id(4);
2453        let raw = bulk.into_raw();
2454        assert_eq!(
2455            GetBulkPdu::try_from_raw(Version::V3, raw)
2456                .unwrap()
2457                .request_id(),
2458            4
2459        );
2460    }
2461
2462    #[test]
2463    fn validated_responses_tie_status_index_version_and_varbind_count() {
2464        let name = oid!(1, 3, 6, 1);
2465        let binding = VarBind::null(name.clone());
2466        assert!(ErrorIndex::new(0, 1).is_err());
2467        assert!(ErrorIndex::new(2, 1).is_err());
2468        assert!(ErrorIndex::new(i32::MAX as u32, usize::MAX).is_ok());
2469        assert!(ErrorIndex::new(i32::MAX as u32 + 1, usize::MAX).is_err());
2470        let index = ErrorIndex::new(1, 1).unwrap();
2471
2472        assert!(
2473            ResponsePdu::new(
2474                Version::V2c,
2475                1,
2476                OutboundErrorStatus::NoError,
2477                Some(index),
2478                vec![binding.clone()]
2479            )
2480            .is_err()
2481        );
2482        assert!(
2483            ResponsePdu::new(
2484                Version::V2c,
2485                1,
2486                OutboundErrorStatus::GenErr,
2487                None,
2488                vec![binding.clone()]
2489            )
2490            .is_err()
2491        );
2492        assert!(
2493            ResponsePdu::new(
2494                Version::V1,
2495                1,
2496                OutboundErrorStatus::NoAccess,
2497                Some(index),
2498                vec![binding.clone()]
2499            )
2500            .is_err()
2501        );
2502        assert!(ResponsePdu::too_big(Version::V2c, 1, vec![binding.clone()]).is_err());
2503        assert!(ResponsePdu::too_big(Version::V1, 1, vec![binding.clone()]).is_ok());
2504
2505        let mut response = ResponsePdu::new(
2506            Version::V3,
2507            8,
2508            OutboundErrorStatus::GenErr,
2509            Some(index),
2510            vec![binding],
2511        )
2512        .unwrap();
2513        assert_eq!(response.status(), OutboundErrorStatus::GenErr);
2514        assert_eq!(response.error_index(), Some(index));
2515        assert_eq!(response.varbinds().len(), 1);
2516        response.set_request_id(10);
2517        assert_eq!(response.request_id(), 10);
2518
2519        let unknown = Pdu::from_raw_parts(
2520            1,
2521            PduBody::Standard {
2522                pdu_type: StandardPduType::Response,
2523                error_status: 99,
2524                error_index: 0,
2525            },
2526            vec![],
2527        );
2528        assert_eq!(unknown.error_status_enum(), ErrorStatus::Unknown(99));
2529        assert!(ResponsePdu::try_from_raw(Version::V2c, unknown).is_err());
2530    }
2531
2532    #[test]
2533    fn response_value_rules_distinguish_versions_and_directions() {
2534        let name = oid!(1, 3, 6, 1);
2535        let exception = vec![VarBind::new(name.clone(), Value::NoSuchObject)];
2536        assert!(ResponsePdu::success(Version::V2c, 1, exception.clone()).is_ok());
2537        assert!(ResponsePdu::success(Version::V1, 1, exception).is_err());
2538        assert!(
2539            RequestPdu::set(
2540                Version::V2c,
2541                1,
2542                vec![VarBind::new(
2543                    name,
2544                    Value::Unknown {
2545                        tag: 0x48,
2546                        data: bytes::Bytes::from_static(b"raw"),
2547                    },
2548                )]
2549            )
2550            .is_err()
2551        );
2552    }
2553
2554    #[test]
2555    fn validated_notifications_generate_and_check_the_mandatory_prefix() {
2556        let trap_oid = oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1);
2557        let extra = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 5, 0), Value::Integer(1));
2558        assert!(
2559            NotificationPdu::trap_v2(Version::V1, 1, 10, &trap_oid, vec![extra.clone()]).is_err()
2560        );
2561        assert!(
2562            NotificationPdu::trap_v2(
2563                Version::V2c,
2564                1,
2565                10,
2566                &trap_oid,
2567                vec![VarBind::null(oid!(1, 3, 6, 1))]
2568            )
2569            .is_err()
2570        );
2571
2572        for (kind, expected) in [
2573            (StandardPduType::TrapV2, PduType::TrapV2),
2574            (StandardPduType::InformRequest, PduType::InformRequest),
2575        ] {
2576            let mut notification =
2577                NotificationPdu::new(Version::V3, kind, 4, 10, &trap_oid, vec![extra.clone()])
2578                    .unwrap();
2579            assert_eq!(notification.pdu_type(), expected);
2580            assert_eq!(notification.uptime(), 10);
2581            assert_eq!(notification.trap_oid(), &trap_oid);
2582            assert_eq!(notification.varbinds().len(), 3);
2583            notification.set_request_id(5);
2584            assert_eq!(notification.request_id(), 5);
2585
2586            let raw = notification.into_raw();
2587            let validated = NotificationPdu::try_from_raw(Version::V3, raw.clone()).unwrap();
2588            let mut buf = EncodeBuf::new();
2589            validated.encode(&mut buf).unwrap();
2590            let mut decoder = Decoder::new(buf.finish());
2591            assert_eq!(Pdu::decode(&mut decoder).unwrap(), raw);
2592        }
2593
2594        let malformed = Pdu::from_raw_parts(
2595            1,
2596            PduBody::Standard {
2597                pdu_type: StandardPduType::TrapV2,
2598                error_status: 0,
2599                error_index: 0,
2600            },
2601            vec![extra],
2602        );
2603        assert!(NotificationPdu::try_from_raw(Version::V2c, malformed).is_err());
2604    }
2605
2606    #[test]
2607    fn reports_are_v3_only_and_have_one_counter_binding() {
2608        let report_oid = oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 4, 0);
2609        assert!(ResponsePdu::report(1, vec![]).is_err());
2610        assert!(ResponsePdu::report(1, vec![VarBind::null(report_oid.clone())]).is_err());
2611        let report =
2612            ResponsePdu::report(1, vec![VarBind::new(report_oid, Value::Counter32(3))]).unwrap();
2613        assert_eq!(report.as_raw().pdu_type(), PduType::Report);
2614        assert!(ResponsePdu::try_from_raw(Version::V2c, report.as_raw().clone()).is_err());
2615        assert!(matches!(
2616            OutboundPdu::try_from_raw(Version::V3, report.into_raw()).unwrap(),
2617            OutboundPdu::Response(_)
2618        ));
2619    }
2620
2621    #[test]
2622    fn validated_v1_traps_check_fields_values_accessors_and_roundtrip() {
2623        let enterprise = oid!(1, 3, 6, 1, 4, 1, 9999);
2624        assert!(
2625            TrapV1Notification::new(
2626                enterprise.clone(),
2627                [127, 0, 0, 1],
2628                GenericTrap::Unknown(7),
2629                0,
2630                1,
2631                vec![]
2632            )
2633            .is_err()
2634        );
2635        let trap = TrapV1Notification::new(
2636            enterprise.clone(),
2637            [127, 0, 0, 1],
2638            GenericTrap::LinkDown,
2639            i32::MIN,
2640            100,
2641            vec![VarBind::null(oid!(1, 3, 6, 1))],
2642        )
2643        .unwrap();
2644        assert_eq!(trap.enterprise(), &enterprise);
2645        assert_eq!(trap.agent_addr(), [127, 0, 0, 1]);
2646        assert_eq!(trap.generic_trap(), GenericTrap::LinkDown);
2647        assert_eq!(trap.specific_trap(), i32::MIN);
2648        assert_eq!(trap.time_stamp(), 100);
2649        assert_eq!(trap.varbinds().len(), 1);
2650
2651        let mut buf = EncodeBuf::new();
2652        trap.encode(&mut buf).unwrap();
2653        let mut decoder = Decoder::new(buf.finish());
2654        assert_eq!(TrapV1Pdu::decode(&mut decoder).unwrap(), trap.into_raw());
2655
2656        // net-snmp accepts the complete Integer32 specific-trap field without
2657        // constraining it based on generic-trap, including this negative
2658        // enterprise-specific value.
2659        let trap = TrapV1Notification::new(
2660            enterprise,
2661            [127, 0, 0, 1],
2662            GenericTrap::EnterpriseSpecific,
2663            -1,
2664            100,
2665            vec![VarBind::null(oid!(1, 3, 6, 1))],
2666        )
2667        .unwrap();
2668        let mut buf = EncodeBuf::new();
2669        trap.encode(&mut buf).unwrap();
2670        let mut decoder = Decoder::new(buf.finish());
2671        assert_eq!(TrapV1Pdu::decode(&mut decoder).unwrap(), trap.into_raw());
2672    }
2673
2674    #[test]
2675    fn test_get_request_roundtrip() {
2676        let pdu = Pdu::get_request(12345, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
2677
2678        let mut buf = EncodeBuf::new();
2679        pdu.encode(&mut buf).unwrap();
2680        let bytes = buf.finish();
2681
2682        let mut decoder = Decoder::new(bytes);
2683        let decoded = Pdu::decode(&mut decoder).unwrap();
2684
2685        assert_eq!(decoded.pdu_type(), PduType::GetRequest);
2686        assert_eq!(decoded.request_id, 12345);
2687        assert_eq!(decoded.varbinds.len(), 1);
2688    }
2689
2690    #[test]
2691    fn test_getbulk_roundtrip() {
2692        let pdu =
2693            Pdu::get_bulk(12345, 0, 10, vec![VarBind::null(oid!(1, 3, 6, 1, 2, 1, 1))]).unwrap();
2694
2695        let mut buf = EncodeBuf::new();
2696        pdu.encode(&mut buf).unwrap();
2697        let bytes = buf.finish();
2698
2699        let mut decoder = Decoder::new(bytes);
2700        let decoded = Pdu::decode(&mut decoder).unwrap();
2701
2702        assert_eq!(decoded.request_id, 12345);
2703        assert_eq!(decoded.get_bulk_fields(), Some((0, 10)));
2704    }
2705
2706    #[test]
2707    fn get_bulk_constructor_checks_both_parameter_ranges() {
2708        let varbinds = || vec![VarBind::null(oid!(1, 3, 6, 1))];
2709
2710        for (non_repeaters, max_repetitions) in
2711            [(0, 0), (MAX_GET_BULK_VALUE, 0), (0, MAX_GET_BULK_VALUE)]
2712        {
2713            let pdu = Pdu::get_bulk(1, non_repeaters, max_repetitions, varbinds()).unwrap();
2714            assert_eq!(
2715                pdu.get_bulk_fields(),
2716                Some((non_repeaters, max_repetitions))
2717            );
2718        }
2719
2720        for (non_repeaters, max_repetitions) in
2721            [(MAX_GET_BULK_VALUE + 1, 0), (0, MAX_GET_BULK_VALUE + 1)]
2722        {
2723            let error = Pdu::get_bulk(1, non_repeaters, max_repetitions, varbinds()).unwrap_err();
2724            assert!(matches!(*error, Error::InvalidMessage(_)));
2725        }
2726    }
2727
2728    #[test]
2729    fn protocol_integer_fields_never_use_generic_truncation() {
2730        // request-id is encoded as 2^32. Generic value decoding can truncate
2731        // this to zero by policy, but protocol Integer32 fields must reject it.
2732        let encoded = bytes::Bytes::from_static(&[
2733            0xa0, 0x0f, 0x02, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x02, 0x01,
2734            0x00, 0x30, 0x00,
2735        ]);
2736        let mut decoder = Decoder::new(encoded);
2737        assert!(Pdu::decode(&mut decoder).is_err());
2738    }
2739
2740    #[test]
2741    fn test_trap_v1_roundtrip() {
2742        use crate::value::Value;
2743        use crate::varbind::VarBind;
2744
2745        let trap = TrapV1Pdu::new(
2746            oid!(1, 3, 6, 1, 4, 1, 9999), // enterprise OID
2747            [192, 168, 1, 1],             // agent address
2748            GenericTrap::LinkDown,
2749            0,
2750            1234_5678, // time stamp
2751            vec![VarBind::new(
2752                oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1),
2753                Value::Integer(1),
2754            )],
2755        );
2756
2757        let mut buf = EncodeBuf::new();
2758        trap.encode(&mut buf).unwrap();
2759        let bytes = buf.finish();
2760
2761        let mut decoder = Decoder::new(bytes);
2762        let decoded = TrapV1Pdu::decode(&mut decoder).unwrap();
2763
2764        assert_eq!(decoded.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999));
2765        assert_eq!(decoded.agent_addr, [192, 168, 1, 1]);
2766        assert_eq!(decoded.generic_trap, GenericTrap::LinkDown);
2767        assert_eq!(decoded.specific_trap, 0);
2768        assert_eq!(decoded.time_stamp, 1234_5678);
2769        assert_eq!(decoded.varbinds.len(), 1);
2770    }
2771
2772    #[test]
2773    fn trap_v1_decode_rejects_unconsumed_constructed_body() {
2774        let trap = TrapV1Pdu::new(
2775            oid!(1, 3, 6, 1, 4, 1, 9999),
2776            [192, 0, 2, 1],
2777            GenericTrap::ColdStart,
2778            0,
2779            1,
2780            vec![],
2781        );
2782        let mut buf = EncodeBuf::new();
2783        trap.encode(&mut buf).unwrap();
2784        let mut encoded = buf.finish().to_vec();
2785        assert_eq!(encoded[0], tag::pdu::TRAP_V1);
2786        assert!(encoded[1] < 0x80, "fixture uses short-form length");
2787        encoded[1] += 2;
2788        encoded.extend_from_slice(&[tag::universal::NULL, 0]);
2789
2790        let mut decoder = Decoder::new(bytes::Bytes::from(encoded));
2791        assert!(TrapV1Pdu::decode(&mut decoder).is_err());
2792    }
2793
2794    #[test]
2795    fn test_generic_pdu_decode_rejects_trap_v1_tag() {
2796        // The v1 Trap PDU (tag 0xA4) has a distinct wire layout and must only be
2797        // decoded via TrapV1Pdu. The generic Pdu decoder must reject the tag so a
2798        // v1 Trap cannot slip into a v2c/v3 (generic PDU) context.
2799        let trap = TrapV1Pdu::new(
2800            oid!(1, 3, 6, 1, 4, 1, 9999),
2801            [192, 168, 1, 1],
2802            GenericTrap::LinkDown,
2803            0,
2804            12345,
2805            vec![],
2806        );
2807        let mut buf = EncodeBuf::new();
2808        trap.encode(&mut buf).unwrap();
2809        let bytes = buf.finish();
2810
2811        let mut decoder = Decoder::new(bytes);
2812        let result = Pdu::decode(&mut decoder);
2813        assert!(
2814            result.is_err(),
2815            "generic Pdu::decode must reject TrapV1 tag, got {result:?}"
2816        );
2817    }
2818
2819    #[test]
2820    fn test_trap_v1_enterprise_specific() {
2821        let trap = TrapV1Pdu::new(
2822            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
2823            [10, 0, 0, 1],
2824            GenericTrap::EnterpriseSpecific,
2825            42, // specific trap number
2826            100,
2827            vec![],
2828        );
2829
2830        assert!(trap.is_enterprise_specific());
2831        assert_eq!(trap.generic_trap, GenericTrap::EnterpriseSpecific);
2832
2833        let mut buf = EncodeBuf::new();
2834        trap.encode(&mut buf).unwrap();
2835        let bytes = buf.finish();
2836
2837        let mut decoder = Decoder::new(bytes);
2838        let decoded = TrapV1Pdu::decode(&mut decoder).unwrap();
2839
2840        assert_eq!(decoded.specific_trap, 42);
2841    }
2842
2843    #[test]
2844    fn test_trap_v1_v2_trap_oid_generic_traps() {
2845        // Test all generic trap types translate to correct snmpTraps.X OIDs
2846        // RFC 3584 Section 3: snmpTraps.{generic_trap + 1}
2847
2848        let test_cases = [
2849            (GenericTrap::ColdStart, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)),
2850            (GenericTrap::WarmStart, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2)),
2851            (GenericTrap::LinkDown, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3)),
2852            (GenericTrap::LinkUp, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4)),
2853            (
2854                GenericTrap::AuthenticationFailure,
2855                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5),
2856            ),
2857            (
2858                GenericTrap::EgpNeighborLoss,
2859                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6),
2860            ),
2861        ];
2862
2863        for (generic_trap, expected_oid) in test_cases {
2864            let trap = TrapV1Pdu::new(
2865                oid!(1, 3, 6, 1, 4, 1, 9999),
2866                [192, 168, 1, 1],
2867                generic_trap,
2868                0,
2869                12345,
2870                vec![],
2871            );
2872            assert_eq!(
2873                trap.v2_trap_oid().unwrap(),
2874                expected_oid,
2875                "Failed for {generic_trap:?}"
2876            );
2877        }
2878    }
2879
2880    #[test]
2881    fn test_trap_v1_v2_trap_oid_enterprise_specific() {
2882        // RFC 3584 Section 3: enterprise.0.specific_trap
2883        let trap = TrapV1Pdu::new(
2884            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
2885            [192, 168, 1, 1],
2886            GenericTrap::EnterpriseSpecific,
2887            42,
2888            12345,
2889            vec![],
2890        );
2891
2892        // Expected: 1.3.6.1.4.1.9999.1.2.0.42
2893        assert_eq!(
2894            trap.v2_trap_oid().unwrap(),
2895            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42)
2896        );
2897    }
2898
2899    #[test]
2900    fn test_trap_v1_v2_trap_oid_enterprise_specific_zero() {
2901        // Edge case: specific_trap = 0
2902        let trap = TrapV1Pdu::new(
2903            oid!(1, 3, 6, 1, 4, 1, 1234),
2904            [10, 0, 0, 1],
2905            GenericTrap::EnterpriseSpecific,
2906            0,
2907            100,
2908            vec![],
2909        );
2910
2911        // Expected: 1.3.6.1.4.1.1234.0.0
2912        assert_eq!(
2913            trap.v2_trap_oid().unwrap(),
2914            oid!(1, 3, 6, 1, 4, 1, 1234, 0, 0)
2915        );
2916    }
2917
2918    #[test]
2919    fn test_pdu_to_response() {
2920        use crate::value::Value;
2921        use crate::varbind::VarBind;
2922
2923        let inform = Pdu::standard(
2924            crate::pdu::StandardPduType::InformRequest,
2925            99999,
2926            0,
2927            0,
2928            vec![
2929                VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), Value::TimeTicks(12345)),
2930                VarBind::new(
2931                    oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0),
2932                    Value::ObjectIdentifier(oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1)),
2933                ),
2934            ],
2935        );
2936
2937        let response = inform.to_response(Version::V2c).unwrap();
2938
2939        assert_eq!(response.pdu_type(), PduType::Response);
2940        assert_eq!(response.request_id, 99999);
2941        assert_eq!(response.error_status(), 0);
2942        assert_eq!(response.error_index(), 0);
2943        assert_eq!(response.varbinds.len(), 2);
2944    }
2945
2946    #[test]
2947    fn test_pdu_is_confirmed() {
2948        let get = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
2949        assert!(get.is_confirmed());
2950
2951        let inform = Pdu::standard(crate::pdu::StandardPduType::InformRequest, 1, 0, 0, vec![]);
2952        assert!(inform.is_confirmed());
2953
2954        let trap = Pdu::standard(crate::pdu::StandardPduType::TrapV2, 1, 0, 0, vec![]);
2955        assert!(!trap.is_confirmed());
2956        assert!(trap.is_notification());
2957    }
2958
2959    #[test]
2960    fn test_decode_accepts_negative_error_index() {
2961        // net-snmp does not validate error_index at parse time; validation code
2962        // that once existed in snmp_client.c is wrapped in #ifdef TEMPORARILY_DISABLED
2963        // and is never compiled. Buggy agents that send negative error_index values
2964        // are accepted by net-snmp and must be accepted here too, or users will
2965        // report "works with net-snmp but not your library".
2966        let raw = RawPdu::response(1, 0, -1, vec![VarBind::null(oid!(1, 3, 6, 1))]);
2967        let encoded = raw.encode();
2968
2969        let mut decoder = Decoder::new(encoded);
2970        let result = Pdu::decode(&mut decoder);
2971
2972        assert!(
2973            result.is_ok(),
2974            "negative error_index must be accepted to match net-snmp behavior, got {:?}",
2975            result.err()
2976        );
2977        assert_eq!(result.unwrap().error_index(), -1);
2978    }
2979
2980    #[test]
2981    fn test_decode_accepts_error_index_beyond_varbinds() {
2982        // net-snmp does not bounds-check error_index against the varbind list length.
2983        // RFC 3416 Section 3 defines error-index as INTEGER (0..max-bindings) and
2984        // annotates it "sometimes ignored"; it places no MUST/SHOULD obligation on
2985        // receivers to reject out-of-range values. Buggy agents that send an
2986        // error_index larger than the varbind count are accepted by net-snmp.
2987        let raw = RawPdu::response(1, 5, 5, vec![VarBind::null(oid!(1, 3, 6, 1))]);
2988        let encoded = raw.encode();
2989
2990        let mut decoder = Decoder::new(encoded);
2991        let result = Pdu::decode(&mut decoder);
2992
2993        assert!(
2994            result.is_ok(),
2995            "error_index beyond varbind count must be accepted to match net-snmp behavior, got {:?}",
2996            result.err()
2997        );
2998        assert_eq!(result.unwrap().error_index(), 5);
2999    }
3000
3001    #[test]
3002    fn test_decode_accepts_valid_error_index_zero() {
3003        // error_index=0 with no error is valid
3004        let raw = RawPdu::response(1, 0, 0, vec![VarBind::null(oid!(1, 3, 6, 1))]);
3005        let encoded = raw.encode();
3006
3007        let mut decoder = Decoder::new(encoded);
3008        let decoded = Pdu::decode(&mut decoder);
3009        assert!(decoded.is_ok(), "error_index=0 should be valid");
3010    }
3011
3012    #[test]
3013    fn decode_accepts_too_big_with_variable_bindings() {
3014        let raw = RawPdu::response(
3015            1,
3016            ErrorStatus::TooBig.as_i32(),
3017            0,
3018            vec![VarBind::null(oid!(1, 3, 6, 1))],
3019        );
3020
3021        let mut decoder = Decoder::new(raw.encode());
3022        let decoded = Pdu::decode(&mut decoder).expect("received PDUs remain permissive");
3023        assert_eq!(decoded.error_status(), ErrorStatus::TooBig.as_i32());
3024        assert_eq!(decoded.error_index(), 0);
3025        assert_eq!(decoded.varbinds.len(), 1);
3026    }
3027
3028    #[test]
3029    fn authorization_error_response_requires_zero_index() {
3030        let request = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
3031        let valid = request.to_error_response(Version::V2c, ErrorStatus::AuthorizationError, 0);
3032        assert!(valid.is_ok());
3033
3034        let invalid = request.to_error_response(Version::V2c, ErrorStatus::AuthorizationError, 1);
3035        assert!(invalid.is_err());
3036    }
3037
3038    #[test]
3039    fn test_decode_accepts_error_index_within_bounds() {
3040        // error_index=1 with 1 varbind is valid (1-based indexing)
3041        let raw = RawPdu::response(1, 5, 1, vec![VarBind::null(oid!(1, 3, 6, 1))]);
3042        let encoded = raw.encode();
3043
3044        let mut decoder = Decoder::new(encoded);
3045        let result = Pdu::decode(&mut decoder);
3046        assert!(
3047            result.is_ok(),
3048            "error_index=1 with 1 varbind should be valid"
3049        );
3050    }
3051
3052    #[test]
3053    fn test_decode_clamps_negative_non_repeaters() {
3054        // RFC 3416 Section 4.2.3: non-repeaters is INTEGER (0..2147483647).
3055        // A negative value from a buggy peer is normalized to 0 (net-snmp
3056        // snmp_agent.c behavior) rather than rejected.
3057        let raw = RawBulkWirePdu::new(1, -1, 10, vec![VarBind::null(oid!(1, 3, 6, 1))]);
3058        let encoded = raw.encode();
3059
3060        let mut decoder = Decoder::new(encoded.clone());
3061        let decoded = Pdu::decode(&mut decoder).expect("negative non_repeaters clamps to 0");
3062        assert_eq!(decoded.get_bulk_fields(), Some((0, 10)));
3063
3064        let mut strict = Decoder::new(encoded)
3065            .with_decode_config(compatibility_without_negative_bulk_normalization());
3066        let error = Pdu::decode(&mut strict).unwrap_err();
3067        assert!(matches!(*error, Error::Decode(_)));
3068    }
3069
3070    #[test]
3071    fn test_decode_clamps_negative_max_repetitions() {
3072        let raw = RawBulkWirePdu::new(1, 0, -5, vec![VarBind::null(oid!(1, 3, 6, 1))]);
3073        let encoded = raw.encode();
3074
3075        let mut decoder = Decoder::new(encoded.clone());
3076        let decoded = Pdu::decode(&mut decoder).expect("negative max_repetitions clamps to 0");
3077        assert_eq!(decoded.get_bulk_fields(), Some((0, 0)));
3078
3079        let mut strict = Decoder::new(encoded)
3080            .with_decode_config(compatibility_without_negative_bulk_normalization());
3081        let error = Pdu::decode(&mut strict).unwrap_err();
3082        assert!(matches!(*error, Error::Decode(_)));
3083    }
3084
3085    #[test]
3086    fn test_pdu_decode_getbulk_clamps_negative_non_repeaters_repro() {
3087        // Regression (audit F06): production GETBULK decode goes through the
3088        // generic Pdu::decode path (community.rs / v3.rs), which must clamp the
3089        // typed non-repeaters/max-repetitions to 0 for negatives.
3090        // Repro packet: GETBULK, request_id=1, non_repeaters=-1 (0xff),
3091        // max_repetitions=1, empty varbinds.
3092        let packet = [
3093            0xa5, 0x0b, 0x02, 0x01, 0x01, 0x02, 0x01, 0xff, 0x02, 0x01, 0x01, 0x30, 0x00,
3094        ];
3095        let encoded = bytes::Bytes::copy_from_slice(&packet);
3096        let mut decoder = Decoder::new(encoded.clone());
3097        let pdu = Pdu::decode(&mut decoder).expect("repro GETBULK packet must decode");
3098        assert_eq!(pdu.pdu_type(), PduType::GetBulkRequest);
3099        assert_eq!(pdu.request_id, 1);
3100        assert_eq!(pdu.get_bulk_fields(), Some((0, 1)));
3101
3102        let mut strict = Decoder::new(encoded)
3103            .with_decode_config(compatibility_without_negative_bulk_normalization());
3104        assert!(Pdu::decode(&mut strict).is_err());
3105    }
3106
3107    #[test]
3108    fn test_decode_accepts_valid_getbulk_params() {
3109        let raw = RawBulkWirePdu::new(1, 0, 10, vec![VarBind::null(oid!(1, 3, 6, 1))]);
3110        let encoded = raw.encode();
3111
3112        let mut decoder = Decoder::new(encoded);
3113        let result = Pdu::decode(&mut decoder);
3114        assert!(result.is_ok(), "valid GETBULK params should be accepted");
3115
3116        let pdu = result.unwrap();
3117        assert_eq!(pdu.get_bulk_fields(), Some((0, 10)));
3118    }
3119
3120    #[test]
3121    fn encode_rejects_out_of_range_get_bulk_fields_without_mutation() {
3122        for (non_repeaters, max_repetitions) in
3123            [(MAX_GET_BULK_VALUE + 1, 0), (0, MAX_GET_BULK_VALUE + 1)]
3124        {
3125            let pdu = Pdu {
3126                request_id: 1,
3127                body: PduBody::GetBulk {
3128                    non_repeaters,
3129                    max_repetitions,
3130                },
3131                varbinds: vec![VarBind::null(oid!(1, 3, 6, 1))],
3132            };
3133            let original = pdu.clone();
3134            let mut buf = EncodeBuf::new();
3135
3136            assert!(pdu.encode(&mut buf).is_err());
3137            assert!(buf.is_empty());
3138            assert_eq!(pdu, original);
3139        }
3140    }
3141
3142    #[test]
3143    fn test_encode_leaves_non_negative_non_repeaters_and_max_repetitions_unchanged() {
3144        let pdu = Pdu::get_bulk(1, 0, 10, vec![VarBind::null(oid!(1, 3, 6, 1))]).unwrap();
3145
3146        let mut buf = EncodeBuf::new();
3147        pdu.encode(&mut buf).unwrap();
3148        let bytes = buf.finish();
3149
3150        let mut decoder = Decoder::new(bytes);
3151        let decoded = Pdu::decode(&mut decoder).unwrap();
3152        assert_eq!(decoded.get_bulk_fields(), Some((0, 10)));
3153    }
3154
3155    #[test]
3156    fn compatible_decode_constructs_encodable_canonical_body() {
3157        let raw = RawBulkWirePdu::new(1, -1, -5, vec![VarBind::null(oid!(1, 3, 6, 1))]);
3158        let mut decoder = Decoder::new(raw.encode());
3159        let decoded = Pdu::decode(&mut decoder).unwrap();
3160        assert_eq!(decoded.get_bulk_fields(), Some((0, 0)));
3161
3162        let mut buf = EncodeBuf::new();
3163        decoded.encode(&mut buf).unwrap();
3164        let mut roundtrip_decoder = Decoder::new(buf.finish());
3165        let roundtrip = Pdu::decode(&mut roundtrip_decoder).unwrap();
3166        assert_eq!(roundtrip.get_bulk_fields(), Some((0, 0)));
3167    }
3168
3169    #[test]
3170    fn test_pdu_decode_getbulk_with_large_max_repetitions() {
3171        // GETBULK PDU with max_repetitions (25) > varbinds.len() (1)
3172        // This is the normal case for GETBULK requests.
3173        // The canonical Pdu::decode must not reject this valid max-repetitions value.
3174        let raw = RawBulkWirePdu::new(12345, 0, 25, vec![VarBind::null(oid!(1, 3, 6, 1, 2, 1, 1))]);
3175        let encoded = raw.encode();
3176
3177        let mut decoder = Decoder::new(encoded);
3178        let result = Pdu::decode(&mut decoder);
3179        assert!(
3180            result.is_ok(),
3181            "Pdu::decode should accept GETBULK with max_repetitions > varbinds.len(), got {:?}",
3182            result.err()
3183        );
3184
3185        let pdu = result.unwrap();
3186        assert_eq!(pdu.pdu_type(), PduType::GetBulkRequest);
3187        assert_eq!(pdu.request_id, 12345);
3188        assert_eq!(pdu.get_bulk_fields(), Some((0, 25)));
3189        assert_eq!(pdu.varbinds.len(), 1);
3190    }
3191
3192    #[test]
3193    fn test_getbulk_request_is_not_treated_as_error() {
3194        let pdu = Pdu::get_bulk(
3195            12345,
3196            2,
3197            10,
3198            vec![
3199                VarBind::null(oid!(1, 3, 6, 1, 2, 1, 1)),
3200                VarBind::null(oid!(1, 3, 6, 1, 2, 1, 2)),
3201            ],
3202        )
3203        .unwrap();
3204
3205        assert!(!pdu.is_error());
3206    }
3207
3208    #[test]
3209    fn test_response_with_error_status_is_treated_as_error() {
3210        let pdu = Pdu::response(
3211            12345,
3212            ErrorStatus::TooBig.as_i32(),
3213            1,
3214            vec![VarBind::null(oid!(1, 3, 6, 1, 2, 1, 1))],
3215        );
3216
3217        assert!(pdu.is_error());
3218    }
3219
3220    #[test]
3221    fn outbound_validation_rejects_nonzero_request_error_fields() {
3222        for (status, index) in [(1, 0), (0, 1), (1, 1)] {
3223            let pdu = Pdu::standard(
3224                StandardPduType::GetRequest,
3225                1,
3226                status,
3227                index,
3228                vec![VarBind::null(oid!(1, 3, 6, 1))],
3229            );
3230            assert!(pdu.encode(&mut EncodeBuf::new()).is_err());
3231        }
3232    }
3233
3234    #[test]
3235    fn outbound_validation_checks_response_status_and_index_combinations() {
3236        let varbinds = vec![VarBind::null(oid!(1, 3, 6, 1))];
3237        for (status, index) in [
3238            (0, 1),
3239            (1, 1),
3240            (-1, 0),
3241            (19, 0),
3242            (5, -1),
3243            (5, 0),
3244            (5, 2),
3245            (ErrorStatus::UndoFailed.as_i32(), 1),
3246        ] {
3247            let pdu = Pdu::response(1, status, index, varbinds.clone());
3248            assert!(
3249                pdu.encode(&mut EncodeBuf::new()).is_err(),
3250                "{status}/{index}"
3251            );
3252        }
3253
3254        let gen_err = Pdu::response(1, ErrorStatus::GenErr.as_i32(), 1, varbinds.clone());
3255        assert!(gen_err.encode(&mut EncodeBuf::new()).is_ok());
3256        let undo_failed = Pdu::response(1, ErrorStatus::UndoFailed.as_i32(), 0, varbinds.clone());
3257        assert!(undo_failed.encode(&mut EncodeBuf::new()).is_ok());
3258        assert!(
3259            undo_failed
3260                .encode_for(&mut EncodeBuf::new(), Version::V1, PduDirection::Response,)
3261                .is_err()
3262        );
3263    }
3264
3265    #[test]
3266    fn outbound_too_big_shape_depends_on_version() {
3267        let with_varbind = Pdu::response(
3268            1,
3269            ErrorStatus::TooBig.as_i32(),
3270            0,
3271            vec![VarBind::null(oid!(1, 3, 6, 1))],
3272        );
3273
3274        assert!(
3275            with_varbind
3276                .encode_for(&mut EncodeBuf::new(), Version::V1, PduDirection::Response)
3277                .is_ok()
3278        );
3279        for version in [Version::V2c, Version::V3] {
3280            assert!(
3281                with_varbind
3282                    .encode_for(&mut EncodeBuf::new(), version, PduDirection::Response)
3283                    .is_err(),
3284                "{version:?}"
3285            );
3286        }
3287        assert!(with_varbind.encode(&mut EncodeBuf::new()).is_err());
3288
3289        let empty = Pdu::response(1, ErrorStatus::TooBig.as_i32(), 0, vec![]);
3290        for version in [Version::V2c, Version::V3] {
3291            assert!(
3292                empty
3293                    .encode_for(&mut EncodeBuf::new(), version, PduDirection::Response)
3294                    .is_ok(),
3295                "{version:?}"
3296            );
3297        }
3298
3299        let nonzero_index = Pdu::response(
3300            1,
3301            ErrorStatus::TooBig.as_i32(),
3302            1,
3303            vec![VarBind::null(oid!(1, 3, 6, 1))],
3304        );
3305        for version in [Version::V1, Version::V2c, Version::V3] {
3306            assert!(
3307                nonzero_index
3308                    .encode_for(&mut EncodeBuf::new(), version, PduDirection::Response)
3309                    .is_err(),
3310                "{version:?}"
3311            );
3312        }
3313    }
3314
3315    #[test]
3316    fn outbound_validation_allows_exceptions_only_in_v2_or_v3_responses() {
3317        let varbinds = vec![VarBind::new(oid!(1, 3, 6, 1), Value::NoSuchObject)];
3318        let request = Pdu::get_request(1, &[]);
3319        let mut request = Pdu {
3320            varbinds: varbinds.clone(),
3321            ..request
3322        };
3323        let original = request.clone();
3324        assert!(request.encode(&mut EncodeBuf::new()).is_err());
3325        assert_eq!(request, original);
3326
3327        request.set_standard_pdu_type(StandardPduType::Response);
3328        assert!(request.encode(&mut EncodeBuf::new()).is_ok());
3329        assert!(
3330            request
3331                .encode_for(&mut EncodeBuf::new(), Version::V1, PduDirection::Response,)
3332                .is_err()
3333        );
3334    }
3335
3336    #[test]
3337    fn outbound_validation_rejects_receive_only_values_without_mutation() {
3338        for value in [
3339            Value::UInteger32(1),
3340            Value::Nsap(bytes::Bytes::from_static(b"nsap")),
3341            Value::Unknown {
3342                tag: 0x48,
3343                data: bytes::Bytes::from_static(b"raw"),
3344            },
3345        ] {
3346            let pdu = Pdu::set_request(1, vec![VarBind::new(oid!(1, 3, 6, 1), value)]);
3347            let original = pdu.clone();
3348            assert!(pdu.encode(&mut EncodeBuf::new()).is_err());
3349            assert_eq!(pdu, original);
3350        }
3351    }
3352
3353    #[test]
3354    fn pdu_type_hash() {
3355        use std::collections::HashSet;
3356        let mut set = HashSet::new();
3357        set.insert(PduType::GetRequest);
3358        set.insert(PduType::GetNextRequest);
3359        assert_eq!(set.len(), 2);
3360        assert!(set.contains(&PduType::GetRequest));
3361    }
3362
3363    // =========================================================================
3364    // V1 <-> V2 PDU conversion tests
3365    // =========================================================================
3366
3367    #[test]
3368    fn test_v1_to_v2_generic_trap() {
3369        use crate::value::Value;
3370        use crate::varbind::VarBind;
3371
3372        let trap = TrapV1Pdu::new(
3373            oid!(1, 3, 6, 1, 4, 1, 9999),
3374            [192, 168, 1, 1],
3375            GenericTrap::LinkDown,
3376            0,
3377            12345,
3378            vec![VarBind::new(
3379                oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1),
3380                Value::Integer(1),
3381            )],
3382        );
3383
3384        let pdu = trap.to_v2_pdu().unwrap();
3385
3386        assert_eq!(pdu.pdu_type(), PduType::TrapV2);
3387        assert_eq!(pdu.request_id(), 0);
3388        // sysUpTime.0 + snmpTrapOID.0 + 1 original varbind (no proxy varbinds)
3389        assert_eq!(pdu.varbinds().len(), 3);
3390
3391        // First varbind: sysUpTime.0
3392        assert_eq!(pdu.varbinds()[0].oid, oid!(1, 3, 6, 1, 2, 1, 1, 3, 0));
3393        assert_eq!(pdu.varbinds()[0].value, Value::TimeTicks(12345));
3394
3395        // Second varbind: snmpTrapOID.0 = snmpTraps.3 (linkDown)
3396        assert_eq!(pdu.varbinds()[1].oid, oid!(1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0));
3397        assert_eq!(
3398            pdu.varbinds()[1].value,
3399            Value::ObjectIdentifier(oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3))
3400        );
3401
3402        // Third: original varbind
3403        assert_eq!(pdu.varbinds()[2].oid, oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1));
3404    }
3405
3406    #[test]
3407    fn unknown_v1_generic_trap_seven_uses_net_snmp_arithmetic_mapping() {
3408        let trap = TrapV1Pdu::from_raw_parts(
3409            oid!(1, 3, 6, 1, 4, 1, 9999),
3410            [192, 0, 2, 1],
3411            GenericTrap::Unknown(7),
3412            0,
3413            1,
3414            vec![],
3415        );
3416
3417        assert_eq!(
3418            trap.v2_trap_oid().unwrap(),
3419            oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 8)
3420        );
3421        assert!(
3422            TrapV1Notification::new(
3423                oid!(1, 3, 6, 1, 4, 1, 9999),
3424                [192, 0, 2, 1],
3425                GenericTrap::Unknown(7),
3426                0,
3427                1,
3428                vec![],
3429            )
3430            .is_err()
3431        );
3432    }
3433
3434    #[test]
3435    fn test_v1_to_v2_no_proxy_varbinds() {
3436        let trap = TrapV1Pdu::new(
3437            oid!(1, 3, 6, 1, 4, 1, 9999),
3438            [192, 168, 1, 1],
3439            GenericTrap::ColdStart,
3440            0,
3441            100,
3442            vec![],
3443        );
3444
3445        let pdu = trap.to_v2_pdu().unwrap();
3446        // Only sysUpTime.0 and snmpTrapOID.0 - no proxy varbinds even with
3447        // non-zero agent_addr (RFC 3584 Section 3.1(4))
3448        assert_eq!(pdu.varbinds().len(), 2);
3449    }
3450
3451    #[test]
3452    fn test_v1_to_v2_enterprise_specific() {
3453        use crate::value::Value;
3454
3455        let trap = TrapV1Pdu::new(
3456            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
3457            [10, 0, 0, 1],
3458            GenericTrap::EnterpriseSpecific,
3459            42,
3460            5000,
3461            vec![],
3462        );
3463
3464        let pdu = trap.to_v2_pdu().unwrap();
3465
3466        // snmpTrapOID.0 should be enterprise.0.42
3467        assert_eq!(
3468            pdu.varbinds()[1].value,
3469            Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42))
3470        );
3471    }
3472
3473    #[test]
3474    fn v1_to_v2_rejects_invalid_synthesized_trap_oid() {
3475        let mut enterprise_arcs = vec![1, 3];
3476        enterprise_arcs.resize(crate::oid::MAX_OID_LEN - 1, 1);
3477        let trap = TrapV1Pdu::from_raw_parts(
3478            Oid::new(enterprise_arcs),
3479            [0, 0, 0, 0],
3480            GenericTrap::EnterpriseSpecific,
3481            1,
3482            0,
3483            vec![],
3484        );
3485
3486        assert!(trap.enterprise().validate_for_wire().is_ok());
3487        assert!(trap.v2_trap_oid().is_err());
3488        assert!(trap.to_v2_pdu().is_err());
3489    }
3490
3491    #[test]
3492    fn v1_to_v2_validates_every_copied_varbind() {
3493        let valid = VarBind::new(oid!(1, 3, 6, 1, 4, 1, 9999, 1), Value::Integer(1));
3494        let invalid_varbinds = [
3495            VarBind::new(Oid::empty(), Value::Integer(2)),
3496            VarBind::new(
3497                oid!(1, 3, 6, 1, 4, 1, 9999, 2),
3498                Value::ObjectIdentifier(Oid::empty()),
3499            ),
3500            VarBind::new(oid!(1, 3, 6, 1, 4, 1, 9999, 3), Value::Null),
3501            VarBind::new(
3502                oid!(1, 3, 6, 1, 4, 1, 9999, 4),
3503                Value::Unknown {
3504                    tag: 0x48,
3505                    data: bytes::Bytes::from_static(b"raw"),
3506                },
3507            ),
3508        ];
3509
3510        for invalid in invalid_varbinds {
3511            let trap = TrapV1Pdu::from_raw_parts(
3512                oid!(1, 3, 6, 1, 4, 1, 9999),
3513                [0, 0, 0, 0],
3514                GenericTrap::ColdStart,
3515                0,
3516                0,
3517                vec![valid.clone(), invalid],
3518            );
3519            assert!(trap.to_v2_pdu().is_err());
3520        }
3521    }
3522
3523    #[test]
3524    fn test_v2_to_v1_standard_trap() {
3525        use crate::value::Value;
3526        use crate::varbind::VarBind;
3527
3528        let pdu = Pdu::trap_v2(
3529            1,
3530            5000,
3531            &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3), // linkDown
3532            vec![VarBind::new(
3533                oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1),
3534                Value::Integer(1),
3535            )],
3536        );
3537
3538        let trap = to_v1_trap(&pdu, [10, 0, 0, 1]).unwrap();
3539
3540        assert_eq!(trap.generic_trap, GenericTrap::LinkDown);
3541        assert_eq!(trap.specific_trap, 0);
3542        assert_eq!(trap.time_stamp, 5000);
3543        assert_eq!(trap.agent_addr, [10, 0, 0, 1]);
3544        // Enterprise defaults to snmpTraps when no snmpTrapEnterprise.0 varbind
3545        assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5));
3546        assert_eq!(trap.varbinds.len(), 1);
3547    }
3548
3549    #[test]
3550    fn test_v2_to_v1_enterprise_specific_trap() {
3551        let pdu = Pdu::trap_v2(1, 100, &oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2, 0, 42), vec![]);
3552
3553        let trap = to_v1_trap(&pdu, [0, 0, 0, 0]).unwrap();
3554
3555        assert_eq!(trap.generic_trap, GenericTrap::EnterpriseSpecific);
3556        assert_eq!(trap.specific_trap, 42);
3557        assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2));
3558        assert_eq!(trap.time_stamp, 100);
3559    }
3560
3561    #[test]
3562    fn test_v2_to_v1_enterprise_specific_nonzero_penultimate() {
3563        // RFC 3584 Section 3.2: when next-to-last sub-id is non-zero,
3564        // enterprise is snmpTrapOID with only the last sub-id removed.
3565        let pdu = Pdu::trap_v2(1, 200, &oid!(1, 3, 6, 1, 4, 1, 9999, 1, 42), vec![]);
3566
3567        let trap = to_v1_trap(&pdu, [0, 0, 0, 0]).unwrap();
3568
3569        assert_eq!(trap.generic_trap, GenericTrap::EnterpriseSpecific);
3570        assert_eq!(trap.specific_trap, 42);
3571        // Next-to-last arc is 1 (non-zero), so only last arc stripped
3572        assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999, 1));
3573        assert_eq!(trap.time_stamp, 200);
3574    }
3575
3576    #[test]
3577    fn v2_to_v1_rejects_short_derived_enterprise_oids() {
3578        for trap_oid in [oid!(0, 1), oid!(0, 0, 1)] {
3579            let notification =
3580                NotificationPdu::trap_v2(Version::V2c, 1, 100, &trap_oid, vec![]).unwrap();
3581            assert!(notification.to_v1_trap([0, 0, 0, 0]).is_err());
3582        }
3583
3584        let boundary = NotificationPdu::trap_v2(Version::V2c, 1, 100, &oid!(0, 0, 0, 1), vec![])
3585            .unwrap()
3586            .to_v1_trap([0, 0, 0, 0])
3587            .unwrap();
3588        assert_eq!(boundary.enterprise(), &oid!(0, 0));
3589    }
3590
3591    #[test]
3592    fn arbitrary_standard_pdu_cannot_enter_notification_conversion() {
3593        let get = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
3594        assert!(NotificationPdu::try_from_raw(Version::V2c, get).is_err());
3595    }
3596
3597    #[test]
3598    fn test_v2_to_v1_snmp_traps_arc_out_of_range() {
3599        // RFC 3584 Section 3.2 rules (1), (3), (4): snmpTraps.x with x=0 or
3600        // x>6 is not a standard trap, so enterprise = OID minus last arc
3601        // (next-to-last arc 5 is non-zero), generic = 6, specific = last arc.
3602        for arc in [0u32, 7, 9] {
3603            let pdu = Pdu::trap_v2(1, 100, &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, arc), vec![]);
3604
3605            let trap = to_v1_trap(&pdu, [0, 0, 0, 0]).unwrap();
3606
3607            assert_eq!(trap.generic_trap, GenericTrap::EnterpriseSpecific);
3608            assert_eq!(trap.specific_trap, i32::try_from(arc).unwrap());
3609            assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5));
3610        }
3611    }
3612
3613    #[test]
3614    fn test_v2_to_v1_extracts_trap_address() {
3615        use crate::notification::oids;
3616        use crate::value::Value;
3617        use crate::varbind::VarBind;
3618
3619        let pdu = Pdu::trap_v2(
3620            1,
3621            0,
3622            &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1), // coldStart
3623            vec![VarBind::new(
3624                oids::snmp_trap_address(),
3625                Value::IpAddress([192, 168, 1, 1]),
3626            )],
3627        );
3628
3629        let trap = to_v1_trap(&pdu, [0, 0, 0, 0]).unwrap();
3630        assert_eq!(trap.agent_addr, [192, 168, 1, 1]);
3631        // RFC 3584 Section 3.2 rule (6): only sysUpTime.0 and snmpTrapOID.0
3632        // are excluded; snmpTrapAddress.0 is retained in the varbinds
3633        assert_eq!(trap.varbinds.len(), 1);
3634        assert_eq!(trap.varbinds[0].oid, oids::snmp_trap_address());
3635    }
3636
3637    #[test]
3638    fn test_v2_to_v1_extracts_trap_enterprise() {
3639        use crate::notification::oids;
3640        use crate::value::Value;
3641        use crate::varbind::VarBind;
3642
3643        let pdu = Pdu::trap_v2(
3644            1,
3645            0,
3646            &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1), // coldStart
3647            vec![VarBind::new(
3648                oids::snmp_trap_enterprise(),
3649                Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 9999)),
3650            )],
3651        );
3652
3653        let trap = to_v1_trap(&pdu, [0, 0, 0, 0]).unwrap();
3654        // Standard trap should use the enterprise from snmpTrapEnterprise.0
3655        assert_eq!(trap.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999));
3656        // RFC 3584 Section 3.2 rule (6): only sysUpTime.0 and snmpTrapOID.0
3657        // are excluded; snmpTrapEnterprise.0 is retained in the varbinds
3658        assert_eq!(trap.varbinds.len(), 1);
3659        assert_eq!(trap.varbinds[0].oid, oids::snmp_trap_enterprise());
3660    }
3661
3662    #[test]
3663    fn test_v2_to_v1_counter64_dropped() {
3664        use crate::value::Value;
3665        use crate::varbind::VarBind;
3666
3667        let pdu = Pdu::trap_v2(
3668            1,
3669            0,
3670            &oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1),
3671            vec![VarBind::new(
3672                oid!(1, 3, 6, 1, 2, 1, 1, 1, 0),
3673                Value::Counter64(12345),
3674            )],
3675        );
3676
3677        // Counter64 in any varbind means the trap cannot be represented in V1
3678        assert!(to_v1_trap(&pdu, [0, 0, 0, 0]).is_err());
3679    }
3680
3681    #[test]
3682    fn test_v2_to_v1_too_few_varbinds() {
3683        let pdu = Pdu::standard(crate::pdu::StandardPduType::TrapV2, 1, 0, 0, vec![]);
3684
3685        assert!(to_v1_trap(&pdu, [0, 0, 0, 0]).is_err());
3686    }
3687
3688    #[test]
3689    fn test_v1_v2_roundtrip_enterprise_specific() {
3690        use crate::value::Value;
3691        use crate::varbind::VarBind;
3692
3693        // Enterprise-specific traps preserve enterprise and specific_trap
3694        // through the OID encoding (enterprise.0.specific_trap), so these
3695        // fields survive a non-proxy v1->v2->v1 roundtrip. agent_addr is
3696        // lost (comes from default_addr on the v1 side).
3697        let original = TrapV1Pdu::new(
3698            oid!(1, 3, 6, 1, 4, 1, 9999, 1, 2),
3699            [192, 168, 1, 1],
3700            GenericTrap::EnterpriseSpecific,
3701            42,
3702            12345,
3703            vec![VarBind::new(
3704                oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1),
3705                Value::Integer(1),
3706            )],
3707        );
3708
3709        let v2 = original.to_v2_pdu().unwrap();
3710        let restored = to_v1_trap(v2.as_raw(), [0, 0, 0, 0]).unwrap();
3711
3712        assert_eq!(restored.enterprise, original.enterprise);
3713        assert_eq!(restored.generic_trap, original.generic_trap);
3714        assert_eq!(restored.specific_trap, original.specific_trap);
3715        assert_eq!(restored.time_stamp, original.time_stamp);
3716        assert_eq!(restored.varbinds.len(), original.varbinds.len());
3717        assert_eq!(restored.varbinds[0].oid, original.varbinds[0].oid);
3718        // agent_addr not preserved without proxy varbinds
3719        assert_eq!(restored.agent_addr, [0, 0, 0, 0]);
3720    }
3721
3722    #[test]
3723    fn test_v1_v2_roundtrip_standard_trap() {
3724        // Standard trap roundtrip preserves generic_trap and time_stamp.
3725        // Without proxy varbinds, enterprise falls back to snmpTraps and
3726        // agent_addr comes from default_addr.
3727        let original = TrapV1Pdu::new(
3728            oid!(1, 3, 6, 1, 4, 1, 9999),
3729            [10, 0, 0, 1],
3730            GenericTrap::WarmStart,
3731            0,
3732            500,
3733            vec![],
3734        );
3735
3736        let v2 = original.to_v2_pdu().unwrap();
3737        let restored = to_v1_trap(v2.as_raw(), [10, 0, 0, 1]).unwrap();
3738
3739        assert_eq!(restored.generic_trap, GenericTrap::WarmStart);
3740        assert_eq!(restored.specific_trap, 0);
3741        assert_eq!(restored.time_stamp, 500);
3742        assert_eq!(restored.agent_addr, [10, 0, 0, 1]); // from default_addr
3743        // Enterprise falls back to snmpTraps without snmpTrapEnterprise.0
3744        assert_eq!(restored.enterprise, oid!(1, 3, 6, 1, 6, 3, 1, 1, 5));
3745    }
3746
3747    #[test]
3748    fn test_v2_to_v1_all_generic_traps() {
3749        // Verify all 6 standard traps roundtrip correctly
3750        let traps = [
3751            (oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 1), GenericTrap::ColdStart),
3752            (oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 2), GenericTrap::WarmStart),
3753            (oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 3), GenericTrap::LinkDown),
3754            (oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 4), GenericTrap::LinkUp),
3755            (
3756                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 5),
3757                GenericTrap::AuthenticationFailure,
3758            ),
3759            (
3760                oid!(1, 3, 6, 1, 6, 3, 1, 1, 5, 6),
3761                GenericTrap::EgpNeighborLoss,
3762            ),
3763        ];
3764
3765        for (trap_oid, expected_generic) in traps {
3766            let pdu = Pdu::trap_v2(1, 100, &trap_oid, vec![]);
3767            let v1 = to_v1_trap(&pdu, [0, 0, 0, 0]).unwrap();
3768            assert_eq!(v1.generic_trap, expected_generic, "Failed for {trap_oid:?}");
3769            assert_eq!(v1.specific_trap, 0);
3770        }
3771    }
3772}