Skip to main content

async_snmp/handler/
results.rs

1//! Result types for MIB handler operations.
2//!
3//! This module provides the result types returned by [`MibHandler`](super::MibHandler)
4//! methods:
5//!
6//! - [`GetResult`] - Result of a GET operation
7//! - [`GetNextResult`] - Result of a GETNEXT operation
8//! - [`SetResult`] - Result of SET test/commit phases
9//! - [`Response`] - Internal response type (typically not used directly)
10
11use crate::error::ErrorStatus;
12use crate::value::Value;
13use crate::varbind::VarBind;
14
15/// Result of a SET operation phase (RFC 3416).
16///
17/// This enum is used by the multi-phase SET protocol:
18/// - [`MibHandler::test_set`](super::MibHandler::test_set): Returns `Ok` if the SET would succeed
19/// - [`MibHandler::commit_set`](super::MibHandler::commit_set): Returns `Ok` if the change was applied
20/// - [`MibHandler::undo_set`](super::MibHandler::undo_set): Returns `Ok` on successful rollback
21/// - [`MibHandler::free_set`](super::MibHandler::free_set): Returns `()` (cleanup is best-effort)
22///
23/// # Choosing the Right Error
24///
25/// | Situation | Variant |
26/// |-----------|---------|
27/// | SET succeeded | [`Ok`](SetResult::Ok) |
28/// | User lacks permission | [`NoAccess`](SetResult::NoAccess) |
29/// | Object is read-only by design | [`NotWritable`](SetResult::NotWritable) |
30/// | Wrong ASN.1 type (e.g., String for Integer) | [`WrongType`](SetResult::WrongType) |
31/// | Value too long/short | [`WrongLength`](SetResult::WrongLength) |
32/// | Value encoding error | [`WrongEncoding`](SetResult::WrongEncoding) |
33/// | Semantic validation failed | [`WrongValue`](SetResult::WrongValue) |
34/// | Cannot create table row | [`NoCreation`](SetResult::NoCreation) |
35/// | Values conflict within request | [`InconsistentValue`](SetResult::InconsistentValue) |
36/// | Out of memory, lock contention | [`ResourceUnavailable`](SetResult::ResourceUnavailable) |
37///
38/// # Example
39///
40/// ```rust
41/// use async_snmp::handler::SetResult;
42/// use async_snmp::Value;
43///
44/// fn validate_admin_status(value: &Value) -> SetResult {
45///     match value {
46///         Value::Integer(v) if *v == 1 || *v == 2 => SetResult::Ok, // up(1) or down(2)
47///         Value::Integer(_) => SetResult::WrongValue, // Invalid admin status
48///         _ => SetResult::WrongType, // Must be Integer
49///     }
50/// }
51///
52/// assert_eq!(validate_admin_status(&Value::Integer(1)), SetResult::Ok);
53/// assert_eq!(validate_admin_status(&Value::Integer(99)), SetResult::WrongValue);
54/// assert_eq!(validate_admin_status(&Value::OctetString("up".into())), SetResult::WrongType);
55/// ```
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum SetResult {
58    /// Operation succeeded.
59    Ok,
60    /// Access denied (security/authorization failure).
61    ///
62    /// Use this when the request lacks sufficient access rights to modify
63    /// the object, based on the security context (user, community, etc.).
64    /// Maps to RFC 3416 error status code 6 (noAccess).
65    NoAccess,
66    /// Object is inherently read-only (not writable by design).
67    ///
68    /// Use this when the object cannot be modified regardless of who
69    /// is making the request. Maps to RFC 3416 error status code 17 (notWritable).
70    NotWritable,
71    /// Value has wrong ASN.1 type for this OID.
72    ///
73    /// Use when the provided value type doesn't match the expected type
74    /// (e.g., OctetString provided for an Integer object).
75    WrongType,
76    /// Value has wrong length for this OID.
77    ///
78    /// Use when the value length violates constraints (e.g., DisplayString
79    /// longer than 255 characters).
80    WrongLength,
81    /// Value encoding is incorrect.
82    WrongEncoding,
83    /// Value is not valid for this OID (semantic check failed).
84    ///
85    /// Use when the value type is correct but the value itself is invalid
86    /// (e.g., negative value for an unsigned counter, or value outside
87    /// an enumeration's valid range).
88    WrongValue,
89    /// Cannot create new row (table doesn't support row creation).
90    NoCreation,
91    /// Value is inconsistent with other values in the same SET.
92    InconsistentValue,
93    /// Resource unavailable (memory, locks, etc.).
94    ResourceUnavailable,
95    /// Commit failed (internal error during apply).
96    CommitFailed,
97    /// Undo failed (internal error during rollback).
98    UndoFailed,
99    /// Row name is inconsistent with existing data.
100    InconsistentName,
101}
102
103impl SetResult {
104    /// Check if this result indicates success.
105    pub fn is_ok(&self) -> bool {
106        matches!(self, SetResult::Ok)
107    }
108
109    /// Convert to an ErrorStatus code.
110    pub fn to_error_status(&self) -> ErrorStatus {
111        match self {
112            SetResult::Ok => ErrorStatus::NoError,
113            SetResult::NoAccess => ErrorStatus::NoAccess,
114            SetResult::NotWritable => ErrorStatus::NotWritable,
115            SetResult::WrongType => ErrorStatus::WrongType,
116            SetResult::WrongLength => ErrorStatus::WrongLength,
117            SetResult::WrongEncoding => ErrorStatus::WrongEncoding,
118            SetResult::WrongValue => ErrorStatus::WrongValue,
119            SetResult::NoCreation => ErrorStatus::NoCreation,
120            SetResult::InconsistentValue => ErrorStatus::InconsistentValue,
121            SetResult::ResourceUnavailable => ErrorStatus::ResourceUnavailable,
122            SetResult::CommitFailed => ErrorStatus::CommitFailed,
123            SetResult::UndoFailed => ErrorStatus::UndoFailed,
124            SetResult::InconsistentName => ErrorStatus::InconsistentName,
125        }
126    }
127}
128
129/// Response to return from a handler.
130///
131/// This is typically built internally by the agent based on handler results.
132#[derive(Debug, Clone)]
133pub struct Response {
134    /// Variable bindings in the response
135    pub varbinds: Vec<VarBind>,
136    /// Error status (0 = no error)
137    pub error_status: ErrorStatus,
138    /// Error index (1-based index of problematic varbind, 0 if no error)
139    pub error_index: i32,
140}
141
142impl Response {
143    /// Create a successful response with the given varbinds.
144    pub fn success(varbinds: Vec<VarBind>) -> Self {
145        Self {
146            varbinds,
147            error_status: ErrorStatus::NoError,
148            error_index: 0,
149        }
150    }
151
152    /// Create an error response.
153    pub fn error(error_status: ErrorStatus, error_index: i32, varbinds: Vec<VarBind>) -> Self {
154        Self {
155            varbinds,
156            error_status,
157            error_index,
158        }
159    }
160}
161
162/// Result of a GET operation on a specific OID (RFC 3416).
163///
164/// This enum distinguishes between the RFC 3416-mandated exception types:
165/// - `Value`: The OID exists and has the given value
166/// - `NoSuchObject`: The OID's object type is not supported (agent doesn't implement this MIB)
167/// - `NoSuchInstance`: The object type exists but this specific instance doesn't
168///   (e.g., table row doesn't exist)
169///
170/// # Version Differences
171///
172/// - **SNMPv1**: Both exception types result in a `noSuchName` error response
173/// - **SNMPv2c/v3**: Returns the appropriate exception value in the response varbind
174///
175/// # Choosing NoSuchObject vs NoSuchInstance
176///
177/// | Situation | Variant |
178/// |-----------|---------|
179/// | OID prefix not recognized | [`NoSuchObject`](GetResult::NoSuchObject) |
180/// | Scalar object not implemented | [`NoSuchObject`](GetResult::NoSuchObject) |
181/// | Table column not implemented | [`NoSuchObject`](GetResult::NoSuchObject) |
182/// | Table row doesn't exist | [`NoSuchInstance`](GetResult::NoSuchInstance) |
183/// | Scalar has no value (optional) | [`NoSuchInstance`](GetResult::NoSuchInstance) |
184///
185/// # Example: Scalar Objects
186///
187/// ```rust
188/// use async_snmp::handler::GetResult;
189/// use async_snmp::{Value, oid};
190///
191/// fn get_scalar(oid: &async_snmp::Oid) -> GetResult {
192///     if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {  // sysDescr.0
193///         GetResult::Value(Value::OctetString("My SNMP Agent".into()))
194///     } else if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 2, 0) {  // sysObjectID.0
195///         GetResult::Value(Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999)))
196///     } else {
197///         GetResult::NoSuchObject
198///     }
199/// }
200/// ```
201///
202/// # Example: Table Objects
203///
204/// ```rust
205/// use async_snmp::handler::GetResult;
206/// use async_snmp::{Value, Oid, oid};
207///
208/// struct IfTable {
209///     entries: Vec<(u32, String)>,  // (index, description)
210/// }
211///
212/// impl IfTable {
213///     fn get(&self, oid: &Oid) -> GetResult {
214///         let if_descr_prefix = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2);
215///
216///         if !oid.starts_with(&if_descr_prefix) {
217///             return GetResult::NoSuchObject;  // Not our column
218///         }
219///
220///         // Extract index from OID (position after prefix)
221///         let arcs = oid.arcs();
222///         if arcs.len() != if_descr_prefix.len() + 1 {
223///             return GetResult::NoSuchInstance;  // Wrong index format
224///         }
225///
226///         let index = arcs[if_descr_prefix.len()];
227///         match self.entries.iter().find(|(i, _)| *i == index) {
228///             Some((_, desc)) => GetResult::Value(Value::OctetString(desc.clone().into())),
229///             None => GetResult::NoSuchInstance,  // Row doesn't exist
230///         }
231///     }
232/// }
233/// ```
234#[derive(Debug, Clone, PartialEq)]
235pub enum GetResult {
236    /// The OID exists and has this value.
237    Value(Value),
238    /// The object type is not implemented by this agent.
239    ///
240    /// Use this when the OID prefix (object type) is not recognized.
241    /// This typically means the handler doesn't implement this part of the MIB.
242    NoSuchObject,
243    /// The object type exists but this specific instance doesn't.
244    ///
245    /// Use this when the OID prefix is valid but the instance identifier
246    /// (e.g., table index) doesn't exist. This is common for table objects
247    /// where the row has been deleted or never existed.
248    NoSuchInstance,
249}
250
251impl GetResult {
252    /// Create a `GetResult` from an `Option<Value>`.
253    ///
254    /// This is a convenience method for migrating from the previous
255    /// `Option<Value>` interface. `None` is treated as `NoSuchObject`.
256    pub fn from_option(value: Option<Value>) -> Self {
257        match value {
258            Some(v) => GetResult::Value(v),
259            None => GetResult::NoSuchObject,
260        }
261    }
262}
263
264impl From<Value> for GetResult {
265    fn from(value: Value) -> Self {
266        GetResult::Value(value)
267    }
268}
269
270impl From<Option<Value>> for GetResult {
271    fn from(value: Option<Value>) -> Self {
272        GetResult::from_option(value)
273    }
274}
275
276/// Result of a GETNEXT operation (RFC 3416).
277///
278/// GETNEXT retrieves the lexicographically next OID after the requested one.
279/// This is the foundation of SNMP walking (iterating through MIB subtrees)
280/// and is also used internally by GETBULK.
281///
282/// # Version Differences
283///
284/// - **SNMPv1**: `EndOfMibView` results in a `noSuchName` error response
285/// - **SNMPv2c/v3**: Returns the `endOfMibView` exception value in the response
286///
287/// # Lexicographic Ordering
288///
289/// OIDs are compared arc-by-arc as unsigned integers:
290/// - `1.3.6.1.2` < `1.3.6.1.2.1` (shorter is less than longer with same prefix)
291/// - `1.3.6.1.2.1` < `1.3.6.1.3` (compare at first differing arc)
292/// - `1.3.6.1.10` > `1.3.6.1.9` (numeric comparison, not lexicographic string)
293///
294/// # Example
295///
296/// ```rust
297/// use async_snmp::handler::GetNextResult;
298/// use async_snmp::{Value, VarBind, Oid, oid};
299///
300/// struct SimpleTable {
301///     oids: Vec<(Oid, Value)>,  // Must be sorted!
302/// }
303///
304/// impl SimpleTable {
305///     fn get_next(&self, after: &Oid) -> GetNextResult {
306///         // Find first OID that is strictly greater than 'after'
307///         for (oid, value) in &self.oids {
308///             if oid > after {
309///                 return GetNextResult::Value(VarBind::new(oid.clone(), value.clone()));
310///             }
311///         }
312///         GetNextResult::EndOfMibView
313///     }
314/// }
315///
316/// let table = SimpleTable {
317///     oids: vec![
318///         (oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::OctetString("sysDescr".into())),
319///         (oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), Value::TimeTicks(12345)),
320///     ],
321/// };
322///
323/// // Before first OID - returns first
324/// let result = table.get_next(&oid!(1, 3, 6, 1, 2, 1, 1, 0));
325/// assert!(result.is_value());
326///
327/// // After last OID - returns EndOfMibView
328/// let result = table.get_next(&oid!(1, 3, 6, 1, 2, 1, 1, 3, 0));
329/// assert!(result.is_end_of_mib_view());
330/// ```
331#[derive(Debug, Clone, PartialEq)]
332pub enum GetNextResult {
333    /// The next OID/value pair in the MIB tree.
334    ///
335    /// The returned OID must be strictly greater than the input OID.
336    Value(VarBind),
337    /// No more OIDs after the given one (end of MIB view).
338    ///
339    /// Return this when the requested OID is at or past the last OID
340    /// in your handler's subtree.
341    EndOfMibView,
342}
343
344impl GetNextResult {
345    /// Create a `GetNextResult` from an `Option<VarBind>`.
346    ///
347    /// This is a convenience method for migrating from the previous
348    /// `Option<VarBind>` interface. `None` is treated as `EndOfMibView`.
349    pub fn from_option(value: Option<VarBind>) -> Self {
350        match value {
351            Some(vb) => GetNextResult::Value(vb),
352            None => GetNextResult::EndOfMibView,
353        }
354    }
355
356    /// Returns `true` if this is a value result.
357    pub fn is_value(&self) -> bool {
358        matches!(self, GetNextResult::Value(_))
359    }
360
361    /// Returns `true` if this is end of MIB view.
362    pub fn is_end_of_mib_view(&self) -> bool {
363        matches!(self, GetNextResult::EndOfMibView)
364    }
365
366    /// Converts to an `Option<VarBind>`.
367    pub fn into_option(self) -> Option<VarBind> {
368        match self {
369            GetNextResult::Value(vb) => Some(vb),
370            GetNextResult::EndOfMibView => None,
371        }
372    }
373}
374
375impl From<VarBind> for GetNextResult {
376    fn from(vb: VarBind) -> Self {
377        GetNextResult::Value(vb)
378    }
379}
380
381impl From<Option<VarBind>> for GetNextResult {
382    fn from(value: Option<VarBind>) -> Self {
383        GetNextResult::from_option(value)
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use crate::oid;
391
392    #[test]
393    fn test_response_success() {
394        let response = Response::success(vec![VarBind::new(oid!(1, 3, 6, 1), Value::Integer(1))]);
395        assert_eq!(response.error_status, ErrorStatus::NoError);
396        assert_eq!(response.error_index, 0);
397        assert_eq!(response.varbinds.len(), 1);
398    }
399
400    #[test]
401    fn test_response_error() {
402        let response = Response::error(
403            ErrorStatus::NoSuchName,
404            1,
405            vec![VarBind::new(oid!(1, 3, 6, 1), Value::Null)],
406        );
407        assert_eq!(response.error_status, ErrorStatus::NoSuchName);
408        assert_eq!(response.error_index, 1);
409    }
410
411    #[test]
412    fn test_get_result_from_option() {
413        let result = GetResult::from_option(Some(Value::Integer(42)));
414        assert!(matches!(result, GetResult::Value(Value::Integer(42))));
415
416        let result = GetResult::from_option(None);
417        assert!(matches!(result, GetResult::NoSuchObject));
418    }
419
420    #[test]
421    fn test_get_result_from_value() {
422        let result: GetResult = Value::Integer(42).into();
423        assert!(matches!(result, GetResult::Value(Value::Integer(42))));
424    }
425
426    #[test]
427    fn test_get_next_result_from_option() {
428        let vb = VarBind::new(oid!(1, 3, 6, 1), Value::Integer(42));
429        let result = GetNextResult::from_option(Some(vb.clone()));
430        assert!(result.is_value());
431        assert_eq!(result.into_option(), Some(vb));
432
433        let result = GetNextResult::from_option(None);
434        assert!(result.is_end_of_mib_view());
435        assert_eq!(result.into_option(), None);
436    }
437
438    #[test]
439    fn test_get_next_result_from_varbind() {
440        let vb = VarBind::new(oid!(1, 3, 6, 1), Value::Integer(42));
441        let result: GetNextResult = vb.clone().into();
442        assert!(result.is_value());
443        if let GetNextResult::Value(inner) = result {
444            assert_eq!(inner.oid, oid!(1, 3, 6, 1));
445        }
446    }
447
448    #[test]
449    fn test_set_result_to_error_status() {
450        assert_eq!(SetResult::Ok.to_error_status(), ErrorStatus::NoError);
451        assert_eq!(SetResult::NoAccess.to_error_status(), ErrorStatus::NoAccess);
452        assert_eq!(
453            SetResult::NotWritable.to_error_status(),
454            ErrorStatus::NotWritable
455        );
456        assert_eq!(
457            SetResult::WrongType.to_error_status(),
458            ErrorStatus::WrongType
459        );
460        assert_eq!(
461            SetResult::CommitFailed.to_error_status(),
462            ErrorStatus::CommitFailed
463        );
464    }
465
466    #[test]
467    fn test_set_result_is_ok() {
468        assert!(SetResult::Ok.is_ok());
469        assert!(!SetResult::NoAccess.is_ok());
470        assert!(!SetResult::NotWritable.is_ok());
471    }
472}