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        if version == Version::V3 {
87            return Err(Error::decode(
88                seq.offset(),
89                DecodeErrorKind::UnknownVersion(3),
90            ));
91        }
92
93        let community = seq.read_octet_string()?;
94        let pdu = Pdu::decode(&mut seq)?;
95
96        Ok(CommunityMessage {
97            version,
98            community,
99            pdu,
100        })
101    }
102
103    /// Consume and return the PDU.
104    pub fn into_pdu(self) -> Pdu {
105        self.pdu
106    }
107
108    /// Encode a GETBULK request message (v2c/v3 only).
109    ///
110    /// GETBULK is not supported in SNMPv1.
111    pub fn encode_bulk(version: Version, community: impl Into<Bytes>, pdu: &GetBulkPdu) -> Bytes {
112        debug_assert!(version != Version::V1, "GETBULK not supported in SNMPv1");
113
114        let community = community.into();
115        let mut buf = EncodeBuf::new();
116
117        buf.push_sequence(|buf| {
118            pdu.encode(buf);
119            buf.push_octet_string(&community);
120            buf.push_integer(version.as_i32());
121        });
122
123        buf.finish()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::oid;
131
132    #[test]
133    fn test_v1_roundtrip() {
134        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
135        let msg = CommunityMessage::v1(b"public".as_slice(), pdu);
136
137        let encoded = msg.encode();
138        let decoded = CommunityMessage::decode(encoded).unwrap();
139
140        assert_eq!(decoded.version, Version::V1);
141        assert_eq!(decoded.community.as_ref(), b"public");
142        assert_eq!(decoded.pdu.request_id, 42);
143    }
144
145    #[test]
146    fn test_v2c_roundtrip() {
147        let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
148        let msg = CommunityMessage::v2c(b"private".as_slice(), pdu);
149
150        let encoded = msg.encode();
151        let decoded = CommunityMessage::decode(encoded).unwrap();
152
153        assert_eq!(decoded.version, Version::V2c);
154        assert_eq!(decoded.community.as_ref(), b"private");
155        assert_eq!(decoded.pdu.request_id, 123);
156    }
157
158    #[test]
159    fn test_version_preserved() {
160        for version in [Version::V1, Version::V2c] {
161            let pdu = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
162            let msg = CommunityMessage::new(version, b"test".as_slice(), pdu);
163
164            let encoded = msg.encode();
165            let decoded = CommunityMessage::decode(encoded).unwrap();
166
167            assert_eq!(decoded.version, version);
168        }
169    }
170}