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