async_snmp/message/
community.rs

1//! Community-based SNMP message format (v1/v2c).
2//!
3//! V1 and V2c messages share the same structure:
4//! `SEQUENCE { version INTEGER, community OCTET STRING, pdu PDU }`
5//!
6//! The only difference is the version number (0 for v1, 1 for v2c).
7
8use crate::ber::{Decoder, EncodeBuf};
9use crate::error::{DecodeErrorKind, Error, Result};
10use crate::pdu::{GetBulkPdu, Pdu};
11use crate::version::Version;
12use bytes::Bytes;
13
14/// Community-based SNMP message (v1/v2c).
15///
16/// This unified type handles both SNMPv1 and SNMPv2c messages,
17/// which share identical structure but differ in version number.
18#[derive(Debug, Clone)]
19pub struct CommunityMessage {
20    /// SNMP version (V1 or V2c)
21    pub version: Version,
22    /// Community string for authentication
23    pub community: Bytes,
24    /// Protocol data unit
25    pub pdu: Pdu,
26}
27
28impl CommunityMessage {
29    /// Create a new community message.
30    ///
31    /// # Panics
32    /// Panics if version is V3 (use V3Message instead).
33    pub fn new(version: Version, community: impl Into<Bytes>, pdu: Pdu) -> Self {
34        assert!(
35            matches!(version, Version::V1 | Version::V2c),
36            "CommunityMessage only supports V1/V2c, not {:?}",
37            version
38        );
39        Self {
40            version,
41            community: community.into(),
42            pdu,
43        }
44    }
45
46    /// Create a V2c message (convenience constructor).
47    pub fn v2c(community: impl Into<Bytes>, pdu: Pdu) -> Self {
48        Self::new(Version::V2c, community, pdu)
49    }
50
51    /// Create a V1 message (convenience constructor).
52    pub fn v1(community: impl Into<Bytes>, pdu: Pdu) -> Self {
53        Self::new(Version::V1, community, pdu)
54    }
55
56    /// Encode to BER.
57    pub fn encode(&self) -> Bytes {
58        let mut buf = EncodeBuf::new();
59
60        buf.push_sequence(|buf| {
61            self.pdu.encode(buf);
62            buf.push_octet_string(&self.community);
63            buf.push_integer(self.version.as_i32());
64        });
65
66        buf.finish()
67    }
68
69    /// Decode from BER.
70    ///
71    /// Returns the message with the version parsed from the data.
72    pub fn decode(data: Bytes) -> Result<Self> {
73        let mut decoder = Decoder::new(data);
74        Self::decode_from(&mut decoder)
75    }
76
77    /// Decode from an existing decoder (used by Message dispatcher).
78    pub(crate) fn decode_from(decoder: &mut Decoder) -> Result<Self> {
79        let mut seq = decoder.read_sequence()?;
80
81        let version_num = seq.read_integer()?;
82        let version = Version::from_i32(version_num).ok_or_else(|| {
83            Error::decode(seq.offset(), DecodeErrorKind::UnknownVersion(version_num))
84        })?;
85
86        Self::decode_from_sequence(&mut seq, version)
87    }
88
89    /// Decode from a sequence decoder where version has already been read.
90    pub(crate) fn decode_from_sequence(seq: &mut Decoder, version: Version) -> Result<Self> {
91        if version == Version::V3 {
92            return Err(Error::decode(
93                seq.offset(),
94                DecodeErrorKind::UnknownVersion(3),
95            ));
96        }
97
98        let community = seq.read_octet_string()?;
99        let pdu = Pdu::decode(seq)?;
100
101        Ok(CommunityMessage {
102            version,
103            community,
104            pdu,
105        })
106    }
107
108    /// Consume and return the PDU.
109    pub fn into_pdu(self) -> Pdu {
110        self.pdu
111    }
112
113    /// Encode a GETBULK request message (v2c/v3 only).
114    ///
115    /// GETBULK is not supported in SNMPv1.
116    pub fn encode_bulk(version: Version, community: impl Into<Bytes>, pdu: &GetBulkPdu) -> Bytes {
117        debug_assert!(version != Version::V1, "GETBULK not supported in SNMPv1");
118
119        let community = community.into();
120        let mut buf = EncodeBuf::new();
121
122        buf.push_sequence(|buf| {
123            pdu.encode(buf);
124            buf.push_octet_string(&community);
125            buf.push_integer(version.as_i32());
126        });
127
128        buf.finish()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::oid;
136
137    #[test]
138    fn test_v1_roundtrip() {
139        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
140        let msg = CommunityMessage::v1(b"public".as_slice(), pdu);
141
142        let encoded = msg.encode();
143        let decoded = CommunityMessage::decode(encoded).unwrap();
144
145        assert_eq!(decoded.version, Version::V1);
146        assert_eq!(decoded.community.as_ref(), b"public");
147        assert_eq!(decoded.pdu.request_id, 42);
148    }
149
150    #[test]
151    fn test_v2c_roundtrip() {
152        let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
153        let msg = CommunityMessage::v2c(b"private".as_slice(), pdu);
154
155        let encoded = msg.encode();
156        let decoded = CommunityMessage::decode(encoded).unwrap();
157
158        assert_eq!(decoded.version, Version::V2c);
159        assert_eq!(decoded.community.as_ref(), b"private");
160        assert_eq!(decoded.pdu.request_id, 123);
161    }
162
163    #[test]
164    fn test_version_preserved() {
165        for version in [Version::V1, Version::V2c] {
166            let pdu = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
167            let msg = CommunityMessage::new(version, b"test".as_slice(), pdu);
168
169            let encoded = msg.encode();
170            let decoded = CommunityMessage::decode(encoded).unwrap();
171
172            assert_eq!(decoded.version, version);
173        }
174    }
175}