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