async_snmp/message/
v3.rs

1//! SNMPv3 message format (RFC 3412).
2//!
3//! V3 messages have a more complex structure than v1/v2c:
4//! ```text
5//! SEQUENCE {
6//!     INTEGER version (3)
7//!     SEQUENCE msgGlobalData {
8//!         INTEGER msgID
9//!         INTEGER msgMaxSize
10//!         OCTET STRING msgFlags (1 byte)
11//!         INTEGER msgSecurityModel
12//!     }
13//!     OCTET STRING msgSecurityParameters (opaque, USM-encoded)
14//!     msgData (ScopedPDU or encrypted OCTET STRING)
15//! }
16//! ```
17//!
18//! The msgData field is either:
19//! - A plaintext ScopedPDU (SEQUENCE) for noAuthNoPriv/authNoPriv
20//! - An encrypted OCTET STRING for authPriv (decrypts to ScopedPDU)
21
22use crate::ber::{Decoder, EncodeBuf};
23use crate::error::{DecodeErrorKind, Error, Result};
24use crate::pdu::Pdu;
25use bytes::Bytes;
26
27/// SNMPv3 security model identifiers.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[repr(i32)]
30pub enum SecurityModel {
31    /// User-based Security Model (RFC 3414)
32    Usm = 3,
33}
34
35impl SecurityModel {
36    /// Create from raw value.
37    pub fn from_i32(value: i32) -> Option<Self> {
38        match value {
39            3 => Some(Self::Usm),
40            _ => None,
41        }
42    }
43
44    /// Get the raw value.
45    pub fn as_i32(self) -> i32 {
46        self as i32
47    }
48}
49
50/// SNMPv3 security level.
51///
52/// The variants are ordered from least secure to most secure,
53/// supporting VACM-style level comparisons (e.g., `actual >= required`).
54#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
55pub enum SecurityLevel {
56    /// No authentication, no privacy
57    NoAuthNoPriv,
58    /// Authentication only
59    AuthNoPriv,
60    /// Authentication and privacy (encryption)
61    AuthPriv,
62}
63
64impl SecurityLevel {
65    /// Decode from msgFlags byte.
66    pub fn from_flags(flags: u8) -> Option<Self> {
67        let auth = flags & 0x01 != 0;
68        let priv_ = flags & 0x02 != 0;
69
70        match (auth, priv_) {
71            (false, false) => Some(Self::NoAuthNoPriv),
72            (true, false) => Some(Self::AuthNoPriv),
73            (true, true) => Some(Self::AuthPriv),
74            (false, true) => None, // Invalid: priv without auth
75        }
76    }
77
78    /// Encode to msgFlags byte (without reportable flag).
79    pub fn to_flags(self) -> u8 {
80        match self {
81            Self::NoAuthNoPriv => 0x00,
82            Self::AuthNoPriv => 0x01,
83            Self::AuthPriv => 0x03,
84        }
85    }
86
87    /// Check if authentication is required.
88    pub fn requires_auth(self) -> bool {
89        matches!(self, Self::AuthNoPriv | Self::AuthPriv)
90    }
91
92    /// Check if privacy (encryption) is required.
93    pub fn requires_priv(self) -> bool {
94        matches!(self, Self::AuthPriv)
95    }
96}
97
98/// Message flags (RFC 3412 Section 6.4).
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct MsgFlags {
101    /// Security level
102    pub security_level: SecurityLevel,
103    /// Whether a report PDU may be sent on error
104    pub reportable: bool,
105}
106
107impl MsgFlags {
108    /// Create new message flags.
109    pub fn new(security_level: SecurityLevel, reportable: bool) -> Self {
110        Self {
111            security_level,
112            reportable,
113        }
114    }
115
116    /// Decode from byte.
117    pub fn from_byte(byte: u8) -> Result<Self> {
118        let security_level = SecurityLevel::from_flags(byte)
119            .ok_or_else(|| Error::decode(0, DecodeErrorKind::InvalidMsgFlags))?;
120        let reportable = byte & 0x04 != 0;
121        Ok(Self {
122            security_level,
123            reportable,
124        })
125    }
126
127    /// Encode to byte.
128    pub fn to_byte(self) -> u8 {
129        let mut flags = self.security_level.to_flags();
130        if self.reportable {
131            flags |= 0x04;
132        }
133        flags
134    }
135}
136
137/// Message global data header (msgGlobalData).
138#[derive(Debug, Clone)]
139pub struct MsgGlobalData {
140    /// Message identifier for request/response correlation
141    pub msg_id: i32,
142    /// Maximum message size the sender can accept
143    pub msg_max_size: i32,
144    /// Message flags (security level + reportable)
145    pub msg_flags: MsgFlags,
146    /// Security model (always USM=3 for our implementation)
147    pub msg_security_model: SecurityModel,
148}
149
150impl MsgGlobalData {
151    /// Create new global data.
152    pub fn new(msg_id: i32, msg_max_size: i32, msg_flags: MsgFlags) -> Self {
153        Self {
154            msg_id,
155            msg_max_size,
156            msg_flags,
157            msg_security_model: SecurityModel::Usm,
158        }
159    }
160
161    /// Encode to buffer.
162    pub fn encode(&self, buf: &mut EncodeBuf) {
163        buf.push_sequence(|buf| {
164            buf.push_integer(self.msg_security_model.as_i32());
165            // msgFlags is a 1-byte OCTET STRING
166            buf.push_octet_string(&[self.msg_flags.to_byte()]);
167            buf.push_integer(self.msg_max_size);
168            buf.push_integer(self.msg_id);
169        });
170    }
171
172    /// Decode from decoder.
173    ///
174    /// Validates that:
175    /// - `msgID` is in range 0..2147483647 (RFC 3412 HeaderData)
176    /// - `msgMaxSize` is in range 484..2147483647 (RFC 3412 HeaderData)
177    /// - `msgSecurityModel` is a known value (currently only USM=3)
178    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
179        const MSG_MAX_SIZE_MINIMUM: i32 = 484;
180
181        let mut seq = decoder.read_sequence()?;
182
183        let msg_id = seq.read_integer()?;
184        let msg_max_size = seq.read_integer()?;
185
186        // RFC 3412 HeaderData: msgID INTEGER (0..2147483647)
187        if msg_id < 0 {
188            return Err(Error::decode(
189                seq.offset(),
190                DecodeErrorKind::InvalidMsgId { value: msg_id },
191            ));
192        }
193
194        // RFC 3412 HeaderData: msgMaxSize INTEGER (484..2147483647)
195        // Negative values indicate the sender encoded a value > 2^31-1
196        if msg_max_size < 0 {
197            return Err(Error::decode(
198                seq.offset(),
199                DecodeErrorKind::MsgMaxSizeTooLarge {
200                    value: msg_max_size,
201                },
202            ));
203        }
204
205        if msg_max_size < MSG_MAX_SIZE_MINIMUM {
206            return Err(Error::decode(
207                seq.offset(),
208                DecodeErrorKind::MsgMaxSizeTooSmall {
209                    value: msg_max_size,
210                    minimum: MSG_MAX_SIZE_MINIMUM,
211                },
212            ));
213        }
214
215        let flags_bytes = seq.read_octet_string()?;
216        if flags_bytes.len() != 1 {
217            return Err(Error::decode(
218                seq.offset(),
219                DecodeErrorKind::UnexpectedTag {
220                    expected: 1,
221                    actual: flags_bytes.len() as u8,
222                },
223            ));
224        }
225        let msg_flags = MsgFlags::from_byte(flags_bytes[0])?;
226
227        let msg_security_model_raw = seq.read_integer()?;
228        // Reject unknown security models per RFC 3412 Section 7.2
229        let msg_security_model =
230            SecurityModel::from_i32(msg_security_model_raw).ok_or_else(|| {
231                Error::decode(
232                    seq.offset(),
233                    DecodeErrorKind::UnknownSecurityModel(msg_security_model_raw),
234                )
235            })?;
236
237        Ok(Self {
238            msg_id,
239            msg_max_size,
240            msg_flags,
241            msg_security_model,
242        })
243    }
244}
245
246/// Scoped PDU (contextEngineID + contextName + PDU).
247#[derive(Debug, Clone)]
248pub struct ScopedPdu {
249    /// Context engine ID (typically same as authoritative engine ID)
250    pub context_engine_id: Bytes,
251    /// Context name (typically empty string)
252    pub context_name: Bytes,
253    /// The actual PDU
254    pub pdu: Pdu,
255}
256
257impl ScopedPdu {
258    /// Create a new scoped PDU.
259    pub fn new(
260        context_engine_id: impl Into<Bytes>,
261        context_name: impl Into<Bytes>,
262        pdu: Pdu,
263    ) -> Self {
264        Self {
265            context_engine_id: context_engine_id.into(),
266            context_name: context_name.into(),
267            pdu,
268        }
269    }
270
271    /// Create with empty context (most common case).
272    pub fn with_empty_context(pdu: Pdu) -> Self {
273        Self {
274            context_engine_id: Bytes::new(),
275            context_name: Bytes::new(),
276            pdu,
277        }
278    }
279
280    /// Encode to buffer.
281    pub fn encode(&self, buf: &mut EncodeBuf) {
282        buf.push_sequence(|buf| {
283            self.pdu.encode(buf);
284            buf.push_octet_string(&self.context_name);
285            buf.push_octet_string(&self.context_engine_id);
286        });
287    }
288
289    /// Encode to bytes.
290    pub fn encode_to_bytes(&self) -> Bytes {
291        let mut buf = EncodeBuf::new();
292        self.encode(&mut buf);
293        buf.finish()
294    }
295
296    /// Decode from decoder.
297    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
298        let mut seq = decoder.read_sequence()?;
299
300        let context_engine_id = seq.read_octet_string()?;
301        let context_name = seq.read_octet_string()?;
302        let pdu = Pdu::decode(&mut seq)?;
303
304        Ok(Self {
305            context_engine_id,
306            context_name,
307            pdu,
308        })
309    }
310}
311
312/// SNMPv3 message.
313#[derive(Debug, Clone)]
314pub struct V3Message {
315    /// Global data (header)
316    pub global_data: MsgGlobalData,
317    /// Security parameters (opaque, USM-encoded)
318    pub security_params: Bytes,
319    /// Message data - either plaintext ScopedPdu or encrypted bytes
320    pub data: V3MessageData,
321}
322
323/// Message data payload.
324#[derive(Debug, Clone)]
325pub enum V3MessageData {
326    /// Plaintext scoped PDU (noAuthNoPriv or authNoPriv)
327    Plaintext(ScopedPdu),
328    /// Encrypted scoped PDU (authPriv) - raw ciphertext
329    Encrypted(Bytes),
330}
331
332impl V3Message {
333    /// Create a new V3 message with plaintext data.
334    pub fn new(global_data: MsgGlobalData, security_params: Bytes, scoped_pdu: ScopedPdu) -> Self {
335        Self {
336            global_data,
337            security_params,
338            data: V3MessageData::Plaintext(scoped_pdu),
339        }
340    }
341
342    /// Create a new V3 message with encrypted data.
343    pub fn new_encrypted(
344        global_data: MsgGlobalData,
345        security_params: Bytes,
346        encrypted: Bytes,
347    ) -> Self {
348        Self {
349            global_data,
350            security_params,
351            data: V3MessageData::Encrypted(encrypted),
352        }
353    }
354
355    /// Get the scoped PDU if available (plaintext only).
356    pub fn scoped_pdu(&self) -> Option<&ScopedPdu> {
357        match &self.data {
358            V3MessageData::Plaintext(pdu) => Some(pdu),
359            V3MessageData::Encrypted(_) => None,
360        }
361    }
362
363    /// Consume and return the scoped PDU if available.
364    pub fn into_scoped_pdu(self) -> Option<ScopedPdu> {
365        match self.data {
366            V3MessageData::Plaintext(pdu) => Some(pdu),
367            V3MessageData::Encrypted(_) => None,
368        }
369    }
370
371    /// Get the PDU if available (convenience method).
372    pub fn pdu(&self) -> Option<&Pdu> {
373        self.scoped_pdu().map(|s| &s.pdu)
374    }
375
376    /// Consume and return the PDU.
377    pub fn into_pdu(self) -> Option<Pdu> {
378        self.into_scoped_pdu().map(|s| s.pdu)
379    }
380
381    /// Get the message ID.
382    pub fn msg_id(&self) -> i32 {
383        self.global_data.msg_id
384    }
385
386    /// Get the security level.
387    pub fn security_level(&self) -> SecurityLevel {
388        self.global_data.msg_flags.security_level
389    }
390
391    /// Encode to BER.
392    ///
393    /// Note: For authenticated messages, the caller must:
394    /// 1. Encode with placeholder auth params (12 zero bytes for HMAC-96)
395    /// 2. Compute HMAC over the entire encoded message
396    /// 3. Replace the placeholder with the actual HMAC
397    pub fn encode(&self) -> Bytes {
398        let mut buf = EncodeBuf::new();
399
400        buf.push_sequence(|buf| {
401            // msgData
402            match &self.data {
403                V3MessageData::Plaintext(scoped_pdu) => {
404                    scoped_pdu.encode(buf);
405                }
406                V3MessageData::Encrypted(ciphertext) => {
407                    buf.push_octet_string(ciphertext);
408                }
409            }
410
411            // msgSecurityParameters (as OCTET STRING)
412            buf.push_octet_string(&self.security_params);
413
414            // msgGlobalData
415            self.global_data.encode(buf);
416
417            // version
418            buf.push_integer(3);
419        });
420
421        buf.finish()
422    }
423
424    /// Decode from BER.
425    ///
426    /// For encrypted messages, returns `V3MessageData::Encrypted` with the raw ciphertext.
427    /// The caller must decrypt using USM before accessing the scoped PDU.
428    pub fn decode(data: Bytes) -> Result<Self> {
429        let mut decoder = Decoder::new(data);
430        let mut seq = decoder.read_sequence()?;
431
432        // Version
433        let version = seq.read_integer()?;
434        if version != 3 {
435            return Err(Error::decode(
436                seq.offset(),
437                DecodeErrorKind::UnknownVersion(version),
438            ));
439        }
440
441        Self::decode_from_sequence(&mut seq)
442    }
443
444    /// Decode from a sequence decoder where version has already been read.
445    pub(crate) fn decode_from_sequence(seq: &mut Decoder) -> Result<Self> {
446        // msgGlobalData
447        let global_data = MsgGlobalData::decode(seq)?;
448
449        // msgSecurityParameters (OCTET STRING containing USM params)
450        let security_params = seq.read_octet_string()?;
451
452        // msgData - either plaintext SEQUENCE or encrypted OCTET STRING
453        let data = if global_data.msg_flags.security_level.requires_priv() {
454            // Encrypted: expect OCTET STRING
455            let encrypted = seq.read_octet_string()?;
456            V3MessageData::Encrypted(encrypted)
457        } else {
458            // Plaintext: expect SEQUENCE (ScopedPDU)
459            let scoped_pdu = ScopedPdu::decode(seq)?;
460            V3MessageData::Plaintext(scoped_pdu)
461        };
462
463        Ok(Self {
464            global_data,
465            security_params,
466            data,
467        })
468    }
469
470    /// Create a discovery request message.
471    ///
472    /// This is sent to discover the engine ID and time of a remote SNMP engine.
473    /// Uses empty security parameters and no authentication.
474    pub fn discovery_request(msg_id: i32) -> Self {
475        let global_data = MsgGlobalData::new(
476            msg_id,
477            65507, // max UDP size
478            MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
479        );
480
481        // Empty USM security parameters for discovery
482        let security_params = crate::v3::UsmSecurityParams::empty().encode();
483
484        // Empty scoped PDU with Report request
485        let pdu = Pdu::get_request(0, &[]);
486        let scoped_pdu = ScopedPdu::with_empty_context(pdu);
487
488        Self::new(global_data, security_params, scoped_pdu)
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495    use crate::oid;
496
497    #[test]
498    fn test_security_level_flags() {
499        assert_eq!(SecurityLevel::NoAuthNoPriv.to_flags(), 0x00);
500        assert_eq!(SecurityLevel::AuthNoPriv.to_flags(), 0x01);
501        assert_eq!(SecurityLevel::AuthPriv.to_flags(), 0x03);
502
503        assert_eq!(
504            SecurityLevel::from_flags(0x00),
505            Some(SecurityLevel::NoAuthNoPriv)
506        );
507        assert_eq!(
508            SecurityLevel::from_flags(0x01),
509            Some(SecurityLevel::AuthNoPriv)
510        );
511        assert_eq!(
512            SecurityLevel::from_flags(0x03),
513            Some(SecurityLevel::AuthPriv)
514        );
515        assert_eq!(SecurityLevel::from_flags(0x02), None); // Invalid
516    }
517
518    #[test]
519    fn test_msg_flags_roundtrip() {
520        let flags = MsgFlags::new(SecurityLevel::AuthPriv, true);
521        let byte = flags.to_byte();
522        assert_eq!(byte, 0x07); // auth=1, priv=1, reportable=1
523
524        let decoded = MsgFlags::from_byte(byte).unwrap();
525        assert_eq!(decoded.security_level, SecurityLevel::AuthPriv);
526        assert!(decoded.reportable);
527    }
528
529    #[test]
530    fn test_msg_global_data_roundtrip() {
531        let global =
532            MsgGlobalData::new(12345, 1472, MsgFlags::new(SecurityLevel::AuthNoPriv, true));
533
534        let mut buf = EncodeBuf::new();
535        global.encode(&mut buf);
536        let encoded = buf.finish();
537
538        let mut decoder = Decoder::new(encoded);
539        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
540
541        assert_eq!(decoded.msg_id, 12345);
542        assert_eq!(decoded.msg_max_size, 1472);
543        assert_eq!(decoded.msg_flags.security_level, SecurityLevel::AuthNoPriv);
544        assert!(decoded.msg_flags.reportable);
545        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
546    }
547
548    #[test]
549    fn test_scoped_pdu_roundtrip() {
550        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
551        let scoped = ScopedPdu::new(b"engine".as_slice(), b"ctx".as_slice(), pdu);
552
553        let mut buf = EncodeBuf::new();
554        scoped.encode(&mut buf);
555        let encoded = buf.finish();
556
557        let mut decoder = Decoder::new(encoded);
558        let decoded = ScopedPdu::decode(&mut decoder).unwrap();
559
560        assert_eq!(decoded.context_engine_id.as_ref(), b"engine");
561        assert_eq!(decoded.context_name.as_ref(), b"ctx");
562        assert_eq!(decoded.pdu.request_id, 42);
563    }
564
565    #[test]
566    fn test_v3_message_plaintext_roundtrip() {
567        let global =
568            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
569        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
570        let scoped = ScopedPdu::with_empty_context(pdu);
571        let msg = V3Message::new(global, Bytes::from_static(b"usm-params"), scoped);
572
573        let encoded = msg.encode();
574        let decoded = V3Message::decode(encoded).unwrap();
575
576        assert_eq!(decoded.global_data.msg_id, 100);
577        assert_eq!(decoded.security_level(), SecurityLevel::NoAuthNoPriv);
578        assert_eq!(decoded.security_params.as_ref(), b"usm-params");
579
580        let scoped_pdu = decoded.scoped_pdu().unwrap();
581        assert_eq!(scoped_pdu.pdu.request_id, 42);
582    }
583
584    #[test]
585    fn test_v3_message_encrypted_roundtrip() {
586        let global = MsgGlobalData::new(200, 1472, MsgFlags::new(SecurityLevel::AuthPriv, false));
587        let msg = V3Message::new_encrypted(
588            global,
589            Bytes::from_static(b"usm-params"),
590            Bytes::from_static(b"encrypted-data"),
591        );
592
593        let encoded = msg.encode();
594        let decoded = V3Message::decode(encoded).unwrap();
595
596        assert_eq!(decoded.global_data.msg_id, 200);
597        assert_eq!(decoded.security_level(), SecurityLevel::AuthPriv);
598
599        match &decoded.data {
600            V3MessageData::Encrypted(data) => {
601                assert_eq!(data.as_ref(), b"encrypted-data");
602            }
603            V3MessageData::Plaintext(_) => panic!("expected encrypted data"),
604        }
605    }
606
607    #[test]
608    fn test_msg_global_data_rejects_msg_max_size_below_minimum() {
609        // Encode with invalid msgMaxSize (below 484)
610        let global = MsgGlobalData {
611            msg_id: 100,
612            msg_max_size: 400, // Below RFC 3412 minimum of 484
613            msg_flags: MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
614            msg_security_model: SecurityModel::Usm,
615        };
616
617        let mut buf = EncodeBuf::new();
618        global.encode(&mut buf);
619        let encoded = buf.finish();
620
621        let mut decoder = Decoder::new(encoded);
622        let result = MsgGlobalData::decode(&mut decoder);
623
624        assert!(result.is_err());
625        match result.unwrap_err() {
626            Error::Decode {
627                kind: DecodeErrorKind::MsgMaxSizeTooSmall { value, minimum },
628                ..
629            } => {
630                assert_eq!(value, 400);
631                assert_eq!(minimum, 484);
632            }
633            e => panic!("expected MsgMaxSizeTooSmall error, got {:?}", e),
634        }
635    }
636
637    #[test]
638    fn test_msg_global_data_accepts_msg_max_size_at_minimum() {
639        // 484 is exactly the RFC 3412 minimum
640        let global = MsgGlobalData::new(100, 484, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
641
642        let mut buf = EncodeBuf::new();
643        global.encode(&mut buf);
644        let encoded = buf.finish();
645
646        let mut decoder = Decoder::new(encoded);
647        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
648
649        assert_eq!(decoded.msg_max_size, 484);
650    }
651
652    #[test]
653    fn test_msg_global_data_rejects_unknown_security_model() {
654        // Manually build encoded data with unknown security model
655        // SEQUENCE { msg_id, msg_max_size, msgFlags, msgSecurityModel=99 }
656        let mut buf = EncodeBuf::new();
657        buf.push_sequence(|buf| {
658            buf.push_integer(99); // unknown security model
659            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
660            buf.push_integer(1472); // msg_max_size
661            buf.push_integer(100); // msg_id
662        });
663        let encoded = buf.finish();
664
665        let mut decoder = Decoder::new(encoded);
666        let result = MsgGlobalData::decode(&mut decoder);
667
668        assert!(result.is_err());
669        match result.unwrap_err() {
670            Error::Decode {
671                kind: DecodeErrorKind::UnknownSecurityModel(model),
672                ..
673            } => {
674                assert_eq!(model, 99);
675            }
676            e => panic!("expected UnknownSecurityModel error, got {:?}", e),
677        }
678    }
679
680    #[test]
681    fn test_msg_global_data_accepts_usm_security_model() {
682        // USM (3) should be accepted
683        let global =
684            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
685
686        let mut buf = EncodeBuf::new();
687        global.encode(&mut buf);
688        let encoded = buf.finish();
689
690        let mut decoder = Decoder::new(encoded);
691        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
692
693        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
694    }
695
696    // RFC 3412 bounds tests for msgID and msgMaxSize
697    //
698    // RFC 3412 HeaderData definition specifies:
699    //   msgID INTEGER (0..2147483647)
700    //   msgMaxSize INTEGER (484..2147483647)
701    //
702    // Values outside these ranges should be rejected.
703
704    #[test]
705    fn test_msg_global_data_rejects_negative_msg_id() {
706        // RFC 3412: msgID must be in range [0..2147483647]
707        // Negative values should be rejected
708        let mut buf = EncodeBuf::new();
709        buf.push_sequence(|buf| {
710            buf.push_integer(3); // USM security model
711            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
712            buf.push_integer(1472); // valid msg_max_size
713            buf.push_integer(-1); // negative msg_id
714        });
715        let encoded = buf.finish();
716
717        let mut decoder = Decoder::new(encoded);
718        let result = MsgGlobalData::decode(&mut decoder);
719
720        assert!(result.is_err());
721        match result.unwrap_err() {
722            Error::Decode {
723                kind: DecodeErrorKind::InvalidMsgId { value },
724                ..
725            } => {
726                assert_eq!(value, -1);
727            }
728            e => panic!("expected InvalidMsgId error, got {:?}", e),
729        }
730    }
731
732    #[test]
733    fn test_msg_global_data_rejects_negative_msg_max_size() {
734        // RFC 3412: msgMaxSize must be in range [484..2147483647]
735        // Negative values (from signed integer interpretation) should be rejected
736        let mut buf = EncodeBuf::new();
737        buf.push_sequence(|buf| {
738            buf.push_integer(3); // USM security model
739            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
740            buf.push_integer(-1); // negative msg_max_size (would be > 2^31-1 unsigned)
741            buf.push_integer(100); // valid msg_id
742        });
743        let encoded = buf.finish();
744
745        let mut decoder = Decoder::new(encoded);
746        let result = MsgGlobalData::decode(&mut decoder);
747
748        assert!(result.is_err());
749        match result.unwrap_err() {
750            Error::Decode {
751                kind: DecodeErrorKind::MsgMaxSizeTooLarge { value },
752                ..
753            } => {
754                assert_eq!(value, -1);
755            }
756            e => panic!("expected MsgMaxSizeTooLarge error, got {:?}", e),
757        }
758    }
759
760    #[test]
761    fn test_msg_global_data_accepts_msg_id_at_zero() {
762        // RFC 3412: msgID 0 is at the lower bound, should be accepted
763        let mut buf = EncodeBuf::new();
764        buf.push_sequence(|buf| {
765            buf.push_integer(3); // USM
766            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
767            buf.push_integer(1472); // valid msg_max_size
768            buf.push_integer(0); // msg_id at lower bound
769        });
770        let encoded = buf.finish();
771
772        let mut decoder = Decoder::new(encoded);
773        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
774
775        assert_eq!(decoded.msg_id, 0);
776    }
777
778    #[test]
779    fn test_msg_global_data_accepts_msg_id_at_maximum() {
780        // RFC 3412: msgID 2147483647 is at the upper bound, should be accepted
781        let mut buf = EncodeBuf::new();
782        buf.push_sequence(|buf| {
783            buf.push_integer(3); // USM
784            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
785            buf.push_integer(1472); // valid msg_max_size
786            buf.push_integer(i32::MAX); // msg_id at upper bound (2147483647)
787        });
788        let encoded = buf.finish();
789
790        let mut decoder = Decoder::new(encoded);
791        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
792
793        assert_eq!(decoded.msg_id, i32::MAX);
794    }
795
796    #[test]
797    fn test_msg_global_data_accepts_msg_max_size_at_maximum() {
798        // RFC 3412: msgMaxSize 2147483647 is at the upper bound, should be accepted
799        let mut buf = EncodeBuf::new();
800        buf.push_sequence(|buf| {
801            buf.push_integer(3); // USM
802            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
803            buf.push_integer(i32::MAX); // msg_max_size at upper bound (2147483647)
804            buf.push_integer(100); // valid msg_id
805        });
806        let encoded = buf.finish();
807
808        let mut decoder = Decoder::new(encoded);
809        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
810
811        assert_eq!(decoded.msg_max_size, i32::MAX);
812    }
813}