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    #[must_use]
106    pub fn is_ok(&self) -> bool {
107        matches!(self, SetResult::Ok)
108    }
109
110    /// Convert to an `ErrorStatus` code.
111    #[must_use]
112    pub fn to_error_status(&self) -> ErrorStatus {
113        match self {
114            SetResult::Ok => ErrorStatus::NoError,
115            SetResult::NoAccess => ErrorStatus::NoAccess,
116            SetResult::NotWritable => ErrorStatus::NotWritable,
117            SetResult::WrongType => ErrorStatus::WrongType,
118            SetResult::WrongLength => ErrorStatus::WrongLength,
119            SetResult::WrongEncoding => ErrorStatus::WrongEncoding,
120            SetResult::WrongValue => ErrorStatus::WrongValue,
121            SetResult::NoCreation => ErrorStatus::NoCreation,
122            SetResult::InconsistentValue => ErrorStatus::InconsistentValue,
123            SetResult::ResourceUnavailable => ErrorStatus::ResourceUnavailable,
124            SetResult::CommitFailed => ErrorStatus::CommitFailed,
125            SetResult::UndoFailed => ErrorStatus::UndoFailed,
126            SetResult::InconsistentName => ErrorStatus::InconsistentName,
127        }
128    }
129}
130
131/// Response to return from a handler.
132///
133/// This is typically built internally by the agent based on handler results.
134#[derive(Debug, Clone)]
135pub struct Response {
136    /// Variable bindings in the response
137    pub varbinds: Vec<VarBind>,
138    /// Error status (0 = no error)
139    pub error_status: ErrorStatus,
140    /// Error index (1-based index of problematic varbind, 0 if no error)
141    pub error_index: i32,
142}
143
144impl Response {
145    /// Create a successful response with the given varbinds.
146    #[must_use]
147    pub fn success(varbinds: Vec<VarBind>) -> Self {
148        Self {
149            varbinds,
150            error_status: ErrorStatus::NoError,
151            error_index: 0,
152        }
153    }
154
155    /// Create an error response.
156    #[must_use]
157    pub fn error(error_status: ErrorStatus, error_index: i32, varbinds: Vec<VarBind>) -> Self {
158        Self {
159            varbinds,
160            error_status,
161            error_index,
162        }
163    }
164}
165
166/// Result of a GET operation on a specific OID (RFC 3416).
167///
168/// This enum distinguishes between the RFC 3416-mandated exception types:
169/// - `Value`: The OID exists and has the given value
170/// - `NoSuchObject`: The OID's object type is not supported (agent doesn't implement this MIB)
171/// - `NoSuchInstance`: The object type exists but this specific instance doesn't
172///   (e.g., table row doesn't exist)
173///
174/// # Version Differences
175///
176/// - **`SNMPv1`**: Both exception types result in a `noSuchName` error response
177/// - **SNMPv2c/v3**: Returns the appropriate exception value in the response varbind
178///
179/// # Choosing `NoSuchObject` vs `NoSuchInstance`
180///
181/// | Situation | Variant |
182/// |-----------|---------|
183/// | OID prefix not recognized | [`NoSuchObject`](GetResult::NoSuchObject) |
184/// | Scalar object not implemented | [`NoSuchObject`](GetResult::NoSuchObject) |
185/// | Table column not implemented | [`NoSuchObject`](GetResult::NoSuchObject) |
186/// | Table row doesn't exist | [`NoSuchInstance`](GetResult::NoSuchInstance) |
187/// | Scalar has no value (optional) | [`NoSuchInstance`](GetResult::NoSuchInstance) |
188///
189/// # Example: Scalar Objects
190///
191/// ```rust
192/// use async_snmp::handler::GetResult;
193/// use async_snmp::{Value, oid};
194///
195/// fn get_scalar(oid: &async_snmp::Oid) -> GetResult {
196///     if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {  // sysDescr.0
197///         GetResult::Value(Value::OctetString("My SNMP Agent".into()))
198///     } else if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 2, 0) {  // sysObjectID.0
199///         GetResult::Value(Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999)))
200///     } else {
201///         GetResult::NoSuchObject
202///     }
203/// }
204/// ```
205///
206/// # Example: Table Objects
207///
208/// ```rust
209/// use async_snmp::handler::GetResult;
210/// use async_snmp::{Value, Oid, oid};
211///
212/// struct IfTable {
213///     entries: Vec<(u32, String)>,  // (index, description)
214/// }
215///
216/// impl IfTable {
217///     fn get(&self, oid: &Oid) -> GetResult {
218///         let if_descr_prefix = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2);
219///
220///         if !oid.starts_with(&if_descr_prefix) {
221///             return GetResult::NoSuchObject;  // Not our column
222///         }
223///
224///         // Extract index from OID (position after prefix)
225///         let arcs = oid.arcs();
226///         if arcs.len() != if_descr_prefix.len() + 1 {
227///             return GetResult::NoSuchInstance;  // Wrong index format
228///         }
229///
230///         let index = arcs[if_descr_prefix.len()];
231///         match self.entries.iter().find(|(i, _)| *i == index) {
232///             Some((_, desc)) => GetResult::Value(Value::OctetString(desc.clone().into())),
233///             None => GetResult::NoSuchInstance,  // Row doesn't exist
234///         }
235///     }
236/// }
237/// ```
238#[derive(Debug, Clone, PartialEq)]
239pub enum GetResult {
240    /// The OID exists and has this value.
241    Value(Value),
242    /// The object type is not implemented by this agent.
243    ///
244    /// Use this when the OID prefix (object type) is not recognized.
245    /// This typically means the handler doesn't implement this part of the MIB.
246    NoSuchObject,
247    /// The object type exists but this specific instance doesn't.
248    ///
249    /// Use this when the OID prefix is valid but the instance identifier
250    /// (e.g., table index) doesn't exist. This is common for table objects
251    /// where the row has been deleted or never existed.
252    NoSuchInstance,
253}
254
255impl GetResult {
256    /// Create a `GetResult` from an `Option<Value>`.
257    ///
258    /// This is a convenience method for migrating from the previous
259    /// `Option<Value>` interface. `None` is treated as `NoSuchObject`.
260    pub fn from_option(value: Option<Value>) -> Self {
261        match value {
262            Some(v) => GetResult::Value(v),
263            None => GetResult::NoSuchObject,
264        }
265    }
266}
267
268impl From<Value> for GetResult {
269    fn from(value: Value) -> Self {
270        GetResult::Value(value)
271    }
272}
273
274impl From<Option<Value>> for GetResult {
275    fn from(value: Option<Value>) -> Self {
276        GetResult::from_option(value)
277    }
278}
279
280/// Result of a GETNEXT operation (RFC 3416).
281///
282/// GETNEXT retrieves the lexicographically next OID after the requested one.
283/// This is the foundation of SNMP walking (iterating through MIB subtrees)
284/// and is also used internally by GETBULK.
285///
286/// # Version Differences
287///
288/// - **`SNMPv1`**: `EndOfMibView` results in a `noSuchName` error response
289/// - **SNMPv2c/v3**: Returns the `endOfMibView` exception value in the response
290///
291/// # Lexicographic Ordering
292///
293/// OIDs are compared arc-by-arc as unsigned integers:
294/// - `1.3.6.1.2` < `1.3.6.1.2.1` (shorter is less than longer with same prefix)
295/// - `1.3.6.1.2.1` < `1.3.6.1.3` (compare at first differing arc)
296/// - `1.3.6.1.10` > `1.3.6.1.9` (numeric comparison, not lexicographic string)
297///
298/// # Example
299///
300/// ```rust
301/// use async_snmp::handler::GetNextResult;
302/// use async_snmp::{Value, VarBind, Oid, oid};
303///
304/// struct SimpleTable {
305///     oids: Vec<(Oid, Value)>,  // Must be sorted!
306/// }
307///
308/// impl SimpleTable {
309///     fn get_next(&self, after: &Oid) -> GetNextResult {
310///         // Find first OID that is strictly greater than 'after'
311///         for (oid, value) in &self.oids {
312///             if oid > after {
313///                 return GetNextResult::Value(VarBind::new(oid.clone(), value.clone()));
314///             }
315///         }
316///         GetNextResult::EndOfMibView
317///     }
318/// }
319///
320/// let table = SimpleTable {
321///     oids: vec![
322///         (oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::OctetString("sysDescr".into())),
323///         (oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), Value::TimeTicks(12345)),
324///     ],
325/// };
326///
327/// // Before first OID - returns first
328/// let result = table.get_next(&oid!(1, 3, 6, 1, 2, 1, 1, 0));
329/// assert!(result.is_value());
330///
331/// // After last OID - returns EndOfMibView
332/// let result = table.get_next(&oid!(1, 3, 6, 1, 2, 1, 1, 3, 0));
333/// assert!(result.is_end_of_mib_view());
334/// ```
335#[derive(Debug, Clone, PartialEq)]
336pub enum GetNextResult {
337    /// The next OID/value pair in the MIB tree.
338    ///
339    /// The returned OID must be strictly greater than the input OID.
340    Value(VarBind),
341    /// No more OIDs after the given one (end of MIB view).
342    ///
343    /// Return this when the requested OID is at or past the last OID
344    /// in your handler's subtree.
345    EndOfMibView,
346}
347
348impl GetNextResult {
349    /// Create a `GetNextResult` from an `Option<VarBind>`.
350    ///
351    /// This is a convenience method for migrating from the previous
352    /// `Option<VarBind>` interface. `None` is treated as `EndOfMibView`.
353    pub fn from_option(value: Option<VarBind>) -> Self {
354        match value {
355            Some(vb) => GetNextResult::Value(vb),
356            None => GetNextResult::EndOfMibView,
357        }
358    }
359
360    /// Returns `true` if this is a value result.
361    pub fn is_value(&self) -> bool {
362        matches!(self, GetNextResult::Value(_))
363    }
364
365    /// Returns `true` if this is end of MIB view.
366    pub fn is_end_of_mib_view(&self) -> bool {
367        matches!(self, GetNextResult::EndOfMibView)
368    }
369
370    /// Converts to an `Option<VarBind>`.
371    pub fn into_option(self) -> Option<VarBind> {
372        match self {
373            GetNextResult::Value(vb) => Some(vb),
374            GetNextResult::EndOfMibView => None,
375        }
376    }
377}
378
379impl From<VarBind> for GetNextResult {
380    fn from(vb: VarBind) -> Self {
381        GetNextResult::Value(vb)
382    }
383}
384
385impl From<Option<VarBind>> for GetNextResult {
386    fn from(value: Option<VarBind>) -> Self {
387        GetNextResult::from_option(value)
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::oid;
395
396    #[test]
397    fn test_response_success() {
398        let response = Response::success(vec![VarBind::new(oid!(1, 3, 6, 1), Value::Integer(1))]);
399        assert_eq!(response.error_status, ErrorStatus::NoError);
400        assert_eq!(response.error_index, 0);
401        assert_eq!(response.varbinds.len(), 1);
402    }
403
404    #[test]
405    fn test_response_error() {
406        let response = Response::error(
407            ErrorStatus::NoSuchName,
408            1,
409            vec![VarBind::new(oid!(1, 3, 6, 1), Value::Null)],
410        );
411        assert_eq!(response.error_status, ErrorStatus::NoSuchName);
412        assert_eq!(response.error_index, 1);
413    }
414
415    #[test]
416    fn test_get_result_from_option() {
417        let result = GetResult::from_option(Some(Value::Integer(42)));
418        assert!(matches!(result, GetResult::Value(Value::Integer(42))));
419
420        let result = GetResult::from_option(None);
421        assert!(matches!(result, GetResult::NoSuchObject));
422    }
423
424    #[test]
425    fn test_get_result_from_value() {
426        let result: GetResult = Value::Integer(42).into();
427        assert!(matches!(result, GetResult::Value(Value::Integer(42))));
428    }
429
430    #[test]
431    fn test_get_next_result_from_option() {
432        let vb = VarBind::new(oid!(1, 3, 6, 1), Value::Integer(42));
433        let result = GetNextResult::from_option(Some(vb.clone()));
434        assert!(result.is_value());
435        assert_eq!(result.into_option(), Some(vb));
436
437        let result = GetNextResult::from_option(None);
438        assert!(result.is_end_of_mib_view());
439        assert_eq!(result.into_option(), None);
440    }
441
442    #[test]
443    fn test_get_next_result_from_varbind() {
444        let vb = VarBind::new(oid!(1, 3, 6, 1), Value::Integer(42));
445        let result: GetNextResult = vb.clone().into();
446        assert!(result.is_value());
447        if let GetNextResult::Value(inner) = result {
448            assert_eq!(inner.oid, oid!(1, 3, 6, 1));
449        }
450    }
451
452    #[test]
453    fn test_set_result_to_error_status() {
454        assert_eq!(SetResult::Ok.to_error_status(), ErrorStatus::NoError);
455        assert_eq!(SetResult::NoAccess.to_error_status(), ErrorStatus::NoAccess);
456        assert_eq!(
457            SetResult::NotWritable.to_error_status(),
458            ErrorStatus::NotWritable
459        );
460        assert_eq!(
461            SetResult::WrongType.to_error_status(),
462            ErrorStatus::WrongType
463        );
464        assert_eq!(
465            SetResult::CommitFailed.to_error_status(),
466            ErrorStatus::CommitFailed
467        );
468    }
469
470    #[test]
471    fn test_set_result_is_ok() {
472        assert!(SetResult::Ok.is_ok());
473        assert!(!SetResult::NoAccess.is_ok());
474        assert!(!SetResult::NotWritable.is_ok());
475    }
476}